← thecodex.expert · The Codex Family of Knowledge
Java

Collections and Generics in Java

The right data structure for every job — with documented complexity, type safety, and a Stream pipeline API.

Java 21 LTSOpenJDKLast verified:
Canonical Definition

The Java Collections Framework provides type-safe data structures via generics — List (ArrayList for O(1) random access), Map (HashMap for O(1) average lookup), Set (HashSet for O(1) membership), and Queue/Deque — all with documented complexity guarantees and a Stream API for functional pipelines.

🟩 Beginner
One sentence

Java Collections give you the right data structure for every job — List for ordered sequences, Set for unique values, Map for key-value pairs — all backed by well-understood algorithms with documented Big O characteristics.

List

Javalists.java
import java.util.*;

// ArrayList — dynamic array. O(1) get by index, O(1) amortised add
List<String> names = new ArrayList<>();
names.add("Priya");
names.add("Rahul");
names.add("Anita");

names.get(0);            // "Priya"
names.size();            // 3
names.contains("Rahul"); // true
names.remove("Rahul");   // removes first occurrence

// Iterate
for (String name : names) System.out.println(name);

// Immutable list (Java 9+)
List<String> fixed = List.of("a", "b", "c");   // throws on add/remove

// LinkedList — O(1) add/remove at head/tail, O(n) get by index
LinkedList<Integer> queue = new LinkedList<>();
queue.addFirst(1);
queue.addLast(2);
queue.removeFirst();   // deque operations

// Sort
Collections.sort(names);
names.sort(Comparator.naturalOrder());
names.sort(Comparator.comparingInt(String::length).reversed());

Map

Javamaps.java
// HashMap — O(1) average get/put, no guaranteed order
Map<String, Integer> scores = new HashMap<>();
scores.put("Priya", 95);
scores.put("Rahul", 88);

scores.get("Priya");                       // 95
scores.getOrDefault("Zara", 0);           // 0 — no KeyError
scores.containsKey("Rahul");               // true
scores.putIfAbsent("Anita", 72);
scores.computeIfAbsent("Zara", k -> 0);   // convenient, but not thread-safe — only ConcurrentHashMap's computeIfAbsent is atomic

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

// LinkedHashMap — insertion-ordered
Map<String, Integer> ordered = new LinkedHashMap<>();

// TreeMap — sorted by key, O(log n)
Map<String, Integer> sorted = new TreeMap<>();

// Immutable (Java 9+)
Map<String, Integer> fixed = Map.of("a", 1, "b", 2);
Map<String, Integer> big   = Map.ofEntries(
    Map.entry("Priya", 95),
    Map.entry("Rahul", 88)
);

Set, Queue, and choosing

Javacollections_misc.java
// HashSet — O(1) add/contains/remove, no order
Set<String> visited = new HashSet<>();
visited.add("page1");
visited.contains("page1");   // true

// TreeSet — sorted, O(log n)
Set<Integer> primes = new TreeSet<>(List.of(2, 3, 5, 7, 11));

// PriorityQueue — min-heap by default
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5); pq.offer(1); pq.offer(3);
pq.poll();   // 1 — always removes smallest

// ArrayDeque — stack and queue in one (prefer over Stack and LinkedList)
ArrayDeque<String> deque = new ArrayDeque<>();
deque.push("a");    // stack push
deque.pop();        // stack pop
deque.offer("b");   // queue enqueue
deque.poll();       // queue dequeue
🔵 Intermediate

Generics

Generics make collections type-safe: List<String> can only hold Strings — the compiler rejects attempts to add an Integer. Without generics, you'd need to cast on every get() and could get ClassCastException at runtime. Generics are erased at compile time — List<String> and List<Integer> are the same class at runtime. The type information only exists in source code and bytecode metadata.

Javagenerics.java
// Generic class
public class Pair<A, B> {
    private final A first;
    private final B second;

    public Pair(A first, B second) {
        this.first = first; this.second = second;
    }
    public A getFirst()  { return first; }
    public B getSecond() { return second; }

    @Override public String toString() {
        return "(" + first + ", " + second + ")";
    }
}

Pair<String, Integer> p = new Pair<>("Priya", 95);

// Generic method
public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}
max(3, 7);       // 7
max("apple", "banana");  // "banana"

// Wildcards
void printList(List<?> list) {           // any type
    list.forEach(System.out::println);
}
void addNumbers(List<? super Integer> list) {  // Integer or supertype
    list.add(42);
}
double sumList(List<? extends Number> list) {  // Number or subtype
    return list.stream().mapToDouble(Number::doubleValue).sum();
}

Streams and collection pipelines

Javastreams_preview.java
import java.util.stream.*;

List<String> names = List.of("Priya", "Rahul", "Anita", "Priya");

// Stream pipeline — lazy, functional, not modifying the original
List<String> unique = names.stream()
    .distinct()
    .filter(n -> n.length() > 4)
    .map(String::toUpperCase)
    .sorted()
    .collect(Collectors.toList());
// [ANITA, PRIYA, RAHUL]

// Collectors
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));

long count  = names.stream().filter(n -> n.startsWith("P")).count();
Optional<String> first = names.stream().findFirst();
String joined = names.stream().collect(Collectors.joining(", "));
Commonly confused
Type erasure means generics only exist at compile time. List<String> and List<Integer> are the same class (java.util.ArrayList) at runtime. You cannot do list instanceof List<String> — the type parameter is erased. This is why you see "unchecked cast" warnings when mixing generics with reflection.
Streams do not modify the source collection. list.stream().filter(...) creates a new stream pipeline — it does not filter the original list. The source is unchanged. To get a new collection, collect: .collect(Collectors.toList()).
ArrayList vs LinkedList: almost always use ArrayList. Despite LinkedList's theoretical O(1) head insert, ArrayList's cache-friendly memory layout makes it faster in practice for most operations. Only use LinkedList when you frequently insert/remove from both ends and never access by index.
How this connects
🔴 Expert

Type erasure and heap pollution

Generic type information is erased by the Java compiler (JLS §4.6). At runtime, List<String> is just List. This was a deliberate design choice for backward compatibility with pre-generics Java code. The consequences: you cannot create generic arrays (new T[10] is illegal), you cannot use instanceof with parameterised types (x instanceof List<String>), and reflective operations work on the raw type. "Heap pollution" occurs when a variable of a parameterised type refers to an object that is not of that type — usually caused by unchecked casts and raw type usage. The @SafeVarargs annotation suppresses warnings for methods that are guaranteed not to cause heap pollution.

Source confidence: HighLast verified: Primary source: Java SE Documentation · docs.oracle.com/en/java/

Sources

1
JLS §4.5 — Parameterised Types. docs.oracle.com/javase/specs/jls/se21/html/jls-4.html#jls-4.5.
2
Java SE 21 API — java.util. docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/package-summary.html.
3
Java SE 21 — Stream API. docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Stream.html.
4
Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley.