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.

Java
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.

Java
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

Java
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.

Java
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]
Java
int sum = nums.stream()
    .mapToInt(Integer::intValue)
    .sum();

System.out.println(sum); // 55
Java
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 HashMap when insertion order matters (use LinkedHashMap instead).
  • 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.