Saltar a contenido

06 - Collections

What this session is

About an hour. You'll learn Java's standard collections - List, Set, Map - plus arrays (which Java has but you'll use rarely), the modern factory methods (List.of, Map.of), and how to iterate them. You'll also see a first hint of generics (List<String>) - the full story is in page 09.

Arrays: the low-level thing you'll rarely use

int[] nums = {1, 2, 3, 4, 5};
System.out.println(nums.length);     // 5
System.out.println(nums[0]);          // 1
System.out.println(nums[4]);          // 5
nums[0] = 99;
System.out.println(nums[0]);          // 99

Arrays in Java are fixed-size - int[5] always holds 5 ints; you can't grow it. The size is set when you create the array.

You'll meet arrays as String[] args in main, when working with low-level libraries, and in performance-critical code. For everyday "list of things," use List (next section), not arrays.

Lists: ordered, growable

import java.util.List;
import java.util.ArrayList;

List<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("cherry");

System.out.println(fruits);              // [apple, banana, cherry]
System.out.println(fruits.size());       // 3
System.out.println(fruits.get(0));       // apple
System.out.println(fruits.get(2));       // cherry

What's new:

  • import java.util.List; and import java.util.ArrayList; - pull in the types.
  • List<String> - "a list of strings." The <String> is a type parameter (generic) - it says what kind of thing the list holds. Without it, the list would hold Object and you'd have to cast on every access. We'll cover generics properly in page 09; for now, always include the type parameter.
  • new ArrayList<>() - create a new ArrayList. The empty <> (called "diamond") lets the compiler infer the type from the variable's declaration.
  • .add(value) - append.
  • .get(index) - read by index (0-based).
  • .size() - count.

List is an interface - a contract that says "this is something you can add to and get from." ArrayList is one implementation. There's also LinkedList (rarely worth the trouble), Vector (legacy - don't use). Always declare as List<T> and instantiate as new ArrayList<>().

Immutable list factories (modern Java)

For lists that won't change, use List.of(...):

List<String> colors = List.of("red", "green", "blue");
System.out.println(colors);     // [red, green, blue]

// colors.add("yellow");        // UnsupportedOperationException at runtime

List.of returns an immutable list - fast, safe, no accidental mutation. Use it for constants and config.

Iterating: the three ways

Way 1 - enhanced for (most common):

for (String fruit : fruits) {
    System.out.println(fruit);
}

Way 2 - indexed for (when you need the index):

for (int i = 0; i < fruits.size(); i++) {
    System.out.println(i + ": " + fruits.get(i));
}

Way 3 - streams (modern, functional):

fruits.stream()
      .filter(f -> f.startsWith("b"))
      .map(String::toUpperCase)
      .forEach(System.out::println);

Streams take a collection through a pipeline of operations: filter keeps matching items, map transforms each, forEach consumes. Useful for "process this collection through several transforms." Full coverage in the senior path; for now, recognize them.

f -> f.startsWith("b") is a lambda - a tiny inline function. Reads as "given f, return whether f starts with b." String::toUpperCase is a method reference - shorthand for s -> s.toUpperCase(). Both are modern Java (8+).

Maps: key → value lookups

import java.util.Map;
import java.util.HashMap;

Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Chioma", 35);

System.out.println(ages.get("Alice"));    // 30
System.out.println(ages.size());          // 3
System.out.println(ages.containsKey("Bob"));   // true

Note: Map<String, Integer> - Integer, not int. Map values cannot be primitives; they have to be reference types. Java uses wrapper classes (Integer, Double, Boolean) when a primitive needs to live in a collection. The conversion happens automatically - called autoboxing - but it's worth knowing.

Modify:

ages.put("Dimeji", 40);          // add or update
ages.remove("Bob");              // remove
int aliceAge = ages.getOrDefault("Alice", 0);    // safe lookup with default

Iterate:

for (Map.Entry<String, Integer> entry : ages.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

// Or just keys:
for (String name : ages.keySet()) { System.out.println(name); }

// Or just values:
for (int age : ages.values()) { System.out.println(age); }

Map factories

Map<String, Integer> ages = Map.of(
    "Alice", 30,
    "Bob", 25,
    "Chioma", 35
);

Immutable. For more than ~10 entries, use Map.ofEntries(Map.entry(k, v), ...).

Sets: unique values

import java.util.Set;
import java.util.HashSet;

Set<String> visited = new HashSet<>();
visited.add("home");
visited.add("work");
visited.add("home");          // duplicate - ignored

System.out.println(visited);          // [home, work] - order varies
System.out.println(visited.size());   // 2
System.out.println(visited.contains("work"));    // true

Sets hold unique values. Adding a duplicate is silently ignored. Useful for: - Membership checks (contains is fast - much faster than scanning a list). - De-duplication (new HashSet<>(myList) gives unique values). - Set math (addAll, retainAll, removeAll for union/intersection/difference).

Immutable factory: Set.of("home", "work").

Quick comparison

Type Syntax Ordered? Mutable? Duplicates? Use when
Array int[] x = {1,2,3} yes yes (size fixed) yes low-level, rare
List List<T> yes (insertion) yes yes growable ordered collection
Set Set<T> no yes no membership, unique values
Map Map<K,V> no yes keys unique lookup by key

A worked example

import java.util.List;
import java.util.Map;
import java.util.HashMap;

public class WordCount {
    public static void main(String[] args) {
        List<String> words = List.of(
            "the", "quick", "brown", "fox", "jumps",
            "over", "the", "lazy", "dog", "the", "end"
        );

        Map<String, Integer> counts = new HashMap<>();
        for (String word : words) {
            counts.merge(word, 1, Integer::sum);
        }

        counts.forEach((word, count) ->
            System.out.println(word + ": " + count));
    }
}

New idiom: counts.merge(word, 1, Integer::sum). Reads as: "for the entry under word, if it doesn't exist set it to 1; if it does, combine the existing value and 1 using Integer::sum." A common pattern for counting.

counts.forEach(...) takes a lambda receiving each key/value pair.

Output (order varies - HashMap doesn't guarantee order):

the: 3
quick: 1
brown: 1
...

For insertion-order iteration, use LinkedHashMap. For sorted iteration, use TreeMap. Both work as Maps and have the same API.

Exercise

In a new file WordCount.java:

Write a program that counts how many times each word appears in a sentence.

  1. Hardcode this sentence: "the quick brown fox jumps over the lazy dog the end".

  2. Split it into words:

    String[] words = sentence.split(" ");
    
    .split on a String returns an array. (Use .split("\\s+") for any whitespace - that's a regex.)

  3. Build a Map<String, Integer> of counts (use the merge pattern above).

  4. Print each word and its count.

Stretch: print sorted by count (most-frequent first). Hint: convert to a list of Map.Entry, sort with a comparator:

import java.util.ArrayList;
import java.util.Comparator;
var sorted = new ArrayList<>(counts.entrySet());
sorted.sort(Comparator.comparingInt(Map.Entry::getValue).reversed());
for (var entry : sorted) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

(Comparator is new; we'll meet it more in real code. For now, treat it as "the sort-key recipe.")

What you might wonder

"Why Integer not int in Map<String, Integer>?" Generics work only on reference types, not primitives. Java auto-converts between int and Integer (autoboxing) so you don't normally notice. The cost: a small allocation per boxed value.

"What's List vs ArrayList?" List is the interface (contract). ArrayList is the implementation. Declare variables and parameters as List<T> so you can swap implementations later. Instantiate with the specific implementation: new ArrayList<>().

"What's the difference between HashMap and LinkedHashMap and TreeMap?" All three implement Map. HashMap - fastest, no order guarantees. LinkedHashMap - preserves insertion order. TreeMap - keeps keys sorted. Pick by what you need.

"What about Stack, Queue, Deque?" - Stack (legacy, don't use). For LIFO, use Deque<T> via new ArrayDeque<>(). - Queue<T> - FIFO; use new ArrayDeque<>() or new LinkedList<>(). - Deque<T> - double-ended; flexible. Useful when you need either end.

Done

You can now: - Recognize arrays (rare in your code, common in others'); use lists instead. - Build List<T>s with ArrayList or List.of. - Build Map<K,V>s with HashMap or Map.of. - Build Set<T>s with HashSet or Set.of. - Iterate all three with enhanced for, indexed for, or streams. - Use the merge idiom for counting. - Distinguish List (interface) from ArrayList (implementation).

Next page: how Java handles things going wrong - exceptions.

Next: Exceptions →

Comments