Collections & Streams
List, Set and Map, generics, and the Streams API for filtering, mapping and reducing data.
The Collections Framework
Java's java.util package gives you battle-tested, generic data structures instead of hand-rolling your own.
import java.util.*;
List<String> names = new ArrayList<>(); // ordered, allows duplicates, index access
names.add("Ali");
names.add("Bilal");
names.add("Ali"); // duplicates allowed
Set<String> uniqueNames = new HashSet<>(names); // no duplicates, no guaranteed order
Map<String, Integer> ages = new HashMap<>(); // key -> value pairs
ages.put("Ali", 22);
ages.put("Bilal", 25);
System.out.println(ages.get("Ali")); // 22
Generics
List<String> means "a list that only holds Strings" — enforced at compile time, so you get a compiler error instead of a runtime ClassCastException if you try to insert the wrong type.
List<Integer> scores = new ArrayList<>();
scores.add(95);
// scores.add("oops"); // compile error — caught before the program ever runs
Choosing the right collection
| Need | Use |
|---|---|
| Ordered, index-based access, duplicates OK | ArrayList |
| Fast add/remove at both ends | ArrayDeque |
| No duplicates, don't care about order | HashSet |
| No duplicates, sorted order | TreeSet |
| Key → value lookups | HashMap |
| Key → value, insertion order preserved | LinkedHashMap |
| Key → value, sorted by key | TreeMap |
Iterating
List<Integer> nums = List.of(1, 2, 3, 4, 5);
for (int n : nums) {
System.out.println(n);
}
nums.forEach(n -> System.out.println(n)); // functional style
The Streams API
Streams let you express what transformation you want, not how to loop and accumulate manually — filtering, mapping and reducing in a declarative pipeline.
import java.util.List;
import java.util.stream.Collectors;
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evenSquares = nums.stream()
.filter(n -> n % 2 == 0) // keep even numbers
.map(n -> n * n) // square each one
.collect(Collectors.toList());
System.out.println(evenSquares); // [4, 16, 36, 64, 100]
int sum = nums.stream()
.mapToInt(Integer::intValue)
.sum();
System.out.println(sum); // 55
List<String> names = List.of("Ali", "Zara", "Bilal", "Ayesha");
String result = names.stream()
.filter(n -> n.length() > 3)
.sorted()
.collect(Collectors.joining(", "));
System.out.println(result); // Ayesha, Bilal, Zara
A stream is lazy — nothing runs until a terminal operation (collect, sum, forEach, count, ...) is called. Intermediate operations (filter, map, sorted) just build up the pipeline description.
Common mistakes
- Trying to reuse a stream after a terminal operation has already consumed it — streams are single-use.
- Using
HashMapwhen insertion order matters (useLinkedHashMapinstead). - Forgetting that
Collectors.toList()returns an unmodifiable-ish list in some contexts — don't assume you can always mutate the result.
Interview questions
Q: What's the difference between List, Set and Map?
List is an ordered, index-accessible collection that allows duplicates. Set holds unique elements with no index access. Map stores key-value pairs with fast key-based lookup.
Q: Are Java streams lazy or eager?
Lazy. Intermediate operations like filter and map just describe the pipeline; nothing actually executes until a terminal operation (collect, forEach, sum, etc.) triggers it.
Q: Can you reuse a Stream object twice?
No — once a terminal operation runs, the stream is considered consumed and throws IllegalStateException if reused. Create a fresh stream from the source collection instead.