The Collections Framework
See how List, Set, Queue and Map fit together in java.util, why you declare the interface type, and the habits that keep shared collections safe.
List<Order> orders = new ArrayList<>();
Set<String> roles = new HashSet<>();
Map<String, User> byId = new HashMap<>();
Deque<Job> jobs = new ArrayDeque<>();The collections framework is the set of interfaces and classes in java.util that hold groups of objects. Almost every back-end method touches one: a repository returns a List of rows, a cache is a Map, a set of granted roles is a Set, and a JSON array or object becomes a List or Map when a library binds it.
Interfaces and implementations
The framework separates what a collection does from how it stores data:
Collectionis the root for groups of elements, and it extendsIterable, which is what makes the enhancedforloop work.Listkeeps order and allows duplicates (ArrayList).Setrejects duplicates (HashSet,LinkedHashSet,TreeSet).QueueandDequehand elements out from the ends (ArrayDeque,PriorityQueue).Mapstores key-value pairs. It is part of the framework but does not extendCollection.
Declare variables, parameters and return types with the interface and choose the class only where you create the object: List<Order> orders = new ArrayList<>();. A method that accepts Collection<String> works with any list, set or deque, and you can swap HashMap for TreeMap later by changing one line.
Generics and boxing
Collections hold object references, never primitives. List<Integer> stores boxed Integer objects and the compiler boxes and unboxes for you. That costs memory for very large numeric data, and a null element unboxed into an int throws a NullPointerException. Never use raw types such as List without a type argument: the compiler can no longer catch a wrong element type.
Helper classes
Collections (plural) has static helpers such as max, frequency and unmodifiableList. Arrays.asList wraps an array as a fixed-size list: you can set elements but not add or remove them.
Mistakes that reach production
- Removing elements inside a for-each loop. The iterator usually notices the change and throws
ConcurrentModificationException, but the check is best-effort: removing the second-to-last element ends the loop early and silently skips the last one. UseremoveIfor an explicitIterator.remove(). - Returning an internal mutable list from a service. Any caller can then change your state. Return
List.copyOf(items)or an unmodifiable view. - Sharing a plain
ArrayListorHashMapbetween request threads. These classes are not thread-safe; use the concurrent collections or immutable ones.
Example
When to use it
- Declare repository methods as returning List<Order> so the implementation can change without touching callers.
- Write one validation helper that accepts Collection<String> and reuse it for request lists and tag sets.
- Replace a for-each loop that removes expired sessions with removeIf to stop ConcurrentModificationException in a cleanup job.
More examples
Removing while iterating
The for-each loop uses a hidden iterator, and removing through the list instead of the iterator makes its next step fail. removeIf does the same job in one pass and cannot get this wrong.
void main() {
List<String> sessions = new ArrayList<>(
List.of("s1:expired", "s2:active", "s3:expired", "s4:active"));
try {
for (String session : sessions) {
if (session.endsWith("expired")) {
sessions.remove(session); // changes the list under the iterator
}
}
} catch (ConcurrentModificationException e) {
IO.println("for-each remove failed: " + e.getClass().getSimpleName());
}
sessions.removeIf(session -> session.endsWith("expired"));
IO.println("after removeIf: " + sessions);
}
// Output:
// for-each remove failed: ConcurrentModificationException
// after removeIf: [s2:active, s4:active]Collections and Arrays helpers
Arrays.asList is a fixed-size view over the array, so set changes the array too and add throws. Copy it into new ArrayList<>(...) when you need a list that grows.
void main() {
List<Integer> latenciesMs = new ArrayList<>(List.of(120, 85, 240, 85, 99));
IO.println("max: " + Collections.max(latenciesMs));
IO.println("min: " + Collections.min(latenciesMs));
IO.println("85ms seen: " + Collections.frequency(latenciesMs, 85));
String[] array = {"a", "b", "c"};
List<String> fixed = Arrays.asList(array);
fixed.set(0, "z"); // writes through to the array
IO.println("list: " + fixed + ", array[0]: " + array[0]);
try {
fixed.add("d");
} catch (UnsupportedOperationException e) {
IO.println("Arrays.asList cannot grow");
}
}
// Output:
// max: 240
// min: 85
// 85ms seen: 2
// list: [z, b, c], array[0]: z
// Arrays.asList cannot growNot leaking internal state from a service
Returning the field itself would let any controller add or clear orders behind the store's back. List.copyOf gives each caller a stable, read-only snapshot.
record Order(String id, int totalCents) {}
class OrderStore {
private final List<Order> orders = new ArrayList<>();
void add(Order order) {
orders.add(order);
}
List<Order> all() {
return List.copyOf(orders); // a snapshot callers cannot modify
}
}
void main() {
var store = new OrderStore();
store.add(new Order("o-1", 2500));
store.add(new Order("o-2", 990));
List<Order> snapshot = store.all();
store.add(new Order("o-3", 4200));
IO.println("snapshot size: " + snapshot.size());
IO.println("store size: " + store.all().size());
try {
snapshot.add(new Order("o-x", 1));
} catch (UnsupportedOperationException e) {
IO.println("callers cannot modify the store");
}
}
// Output:
// snapshot size: 2
// store size: 3
// callers cannot modify the store
Discussion