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;andimport 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 holdObjectand 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):
Way 2 - indexed for (when you need the index):
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¶
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):
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.
-
Hardcode this sentence:
"the quick brown fox jumps over the lazy dog the end". -
Split it into words:
.spliton aStringreturns an array. (Use.split("\\s+")for any whitespace - that's a regex.) -
Build a
Map<String, Integer>of counts (use themergepattern above). -
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.