Java collections
Collection choice is an API statement: it tells readers the access pattern, ordering, and mutability story. Choose for the dominant operation, and let immutability be the default the exceptions argue against.
Method
- Choose by dominant operation.
ArrayListfor indexed iteration (the default sequence);ArrayDequefor stack/queue ends (notLinkedList, whose pointer-chasing loses everywhere);HashMap/HashSetfor membership and lookup;LinkedHashMapwhen iteration order must match insertion (and its access-order mode for small LRU caches);TreeMapfor range queries and sorted iteration;EnumMap/EnumSetfor enum keys (array-backed, near-free). Concurrent access: thejava.util.concurrentversions, neverCollections.synchronizedXwrappers with compound operations (see jvm-memory-model). - Default to immutable snapshots at boundaries.
List.of,Map.of,List.copyOffor parameters, fields, and returns: callers cannot mutate your state, and safe publication comes free (see immutability-defaults). Return empty collections, never null (see null-handling). Mutable builders stay local to the method constructing them. - Model data carriers as records.
record Order(String id, Money total): equals/hashCode/toString correct by construction, shallow-immutable, pattern-matchable. Validate in the compact constructor; add derived accessors as methods. Records replace the Lombok-and-JavaBean boilerplate for value types (the python-dataclasses decision, JVM edition). - Use streams for pipelines, loops for everything else.
Streams pay off for filter-map-collect chains and grouping
(
Collectors.groupingBy,toMapwith a merge function: the two-argtoMapthrows on duplicate keys, a classic production surprise). Prefer loops when you need index math, early exit with side effects, checked exceptions, or debuggability of a complex pipeline. Parallel streams only for CPU-bound work on large data with no shared mutable state, measured before and after (see benchmark-design); they share one common pool and can starve your server's other work. - Size and box consciously on hot paths. Pre-size maps/lists when cardinality is known (rehashing churns; see jvm-gc-selection allocation pressure); primitive arrays or specialized primitive collections where millions of boxed Integers would otherwise dominate the heap (see memory-optimization). These are profile-driven moves, not defaults.
- Expose the least interface. Fields and signatures use
List/Map/Collection, notArrayList/HashMap;SequencedCollectionwhere first/last matter. The implementation is your choice to change later (see api-surface-minimalism).
Boundaries
Optionalis a return type for maybe-absence, not a field or parameter type; collections should be empty, notOptional<List<T>>.- Sorting and hashing depend on correct
equals/hashCode/compareTocontracts; mutable objects as map keys or set members whose hash changes after insertion are lost, not stored. - Kotlin's collection interfaces overlay these classes with read-only views, not true immutability; cross-language code still needs the copyOf discipline (see kotlin-idioms).