Lab 09: Collections — List, Set, Map, Queue

Objective

Use Java's Collections Framework — ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap, and Queue — choose the right collection for each use case, and understand performance trade-offs.

Background

The Java Collections Framework provides ready-made implementations of lists, sets, maps, and queues. Choosing the right collection is one of the most impactful decisions you can make: HashSet.contains() is O(1) while ArrayList.contains() is O(n). TreeMap keeps keys sorted automatically. Understanding these trade-offs separates junior from senior developers.

Time

45 minutes

Prerequisites

  • Lab 06 (OOP — Classes)

  • Lab 08 (Interfaces)

Tools

  • Java 21 (Eclipse Temurin)

  • Docker image: innozverse-java:latest


Lab Instructions

Step 1: List — ArrayList vs LinkedList

💡 ArrayList vs LinkedList: Use ArrayList for almost everything — it has better cache performance and random access is O(1). Use LinkedList only when you're adding/removing from both ends frequently (though ArrayDeque is usually better for that). The "use LinkedList for frequent insertions" advice is outdated for most cases.

📸 Verified Output:


Step 2: Set — HashSet, LinkedHashSet, TreeSet

💡 Choose your Set: HashSet for fast membership tests (O(1)), LinkedHashSet when insertion order matters, TreeSet for sorted iteration or range queries (headSet, tailSet, subSet). Never use a List to check "does this item exist" at scale — that's O(n) vs O(1) for HashSet.

📸 Verified Output:


Step 3: Map — HashMap, LinkedHashMap, TreeMap

💡 merge(key, value, fn) is the cleanest way to count occurrences. computeIfAbsent(key, fn) creates the value only if the key is absent — perfect for grouping. Both eliminate verbose containsKey() + put() patterns. Map.Entry.comparingByValue() sorts by value in a single expression.

📸 Verified Output:


Step 4: Queue, Deque, PriorityQueue

💡 ArrayDeque is the right choice for both stacks and queues — faster than LinkedList and Stack. PriorityQueue is a min-heap: poll() always returns the smallest element. Reverse the comparator for a max-heap (like a task scheduler where higher priority = runs first).

📸 Verified Output:


Step 5: Iterating Collections

💡 Never use list.remove(item) inside a for-each loop — it throws ConcurrentModificationException. Use Iterator.remove(), removeIf(), or collect items to remove and call removeAll() afterward. removeIf with a lambda is the cleanest modern approach.

📸 Verified Output:


Step 6: Collections Utility Class

💡 Collections.unmodifiableList() wraps a list in a view that throws on mutation. For truly immutable lists, use List.of() (no view — a different implementation). Collections.synchronizedList() adds basic thread safety but doesn't protect compound operations — prefer CopyOnWriteArrayList for concurrent reads.

📸 Verified Output:


Step 7: Performance Comparison

💡 This benchmark shows why data structure choice matters. HashSet.contains() is O(1) — it computes the hash and checks one bucket. ArrayList.contains() is O(n) — it checks every element. At 100K elements, this is a 100–1000× difference. Always use the right tool.

📸 Verified Output:

(times vary by machine; relative difference is consistent)


Step 8: Real-World — Student Grade Book

💡 LinkedHashMap preserves insertion order — students appear in the order they were added. The stream pipeline then sorts by GPA descending. Combining Map for O(1) student lookup, List for ordered grades, and streams for reporting is idiomatic modern Java.

📸 Verified Output:


Verification

Summary

You've worked with ArrayList, LinkedHashSet, TreeSet, HashMap, TreeMap, ArrayDeque, PriorityQueue, and Collections utilities. The key takeaway: choose the collection that matches your access pattern — set for uniqueness, map for key lookup, sorted variants for ordered iteration, and queue/deque for processing order.

Further Reading

Last updated