A checkout service keeps open orders in an ArrayList and asks open.contains(order) on every payment. A warehouse worker queue is a LinkedList because “it is a queue.” A session cache is a Hashtable because it is “thread-safe.” All three compile. All three are the wrong type for the hot operation.
Name the hot operation, then pick the JDK type whose contract makes that operation cheap — and whose nulls, order, and thread story you can live with. This page is the series decision guide, not a recap of every class. Glossary (Collection vs Map, optional operations, fail-fast, views vs copies) lives on the Collections Roadmap. Layout theory lives on the Data Structures Roadmap. Jump to the row that matches the pain you have today.
The four questions before new
Ask these in order. The first one you cannot answer is the one to stop on.
- What is the hot operation? Index
i, uniqueness, key lookup, either end, next-best, sorted range, hand-off to another thread. - Do you need encounter order? Insertion, access, sort, or none.
- Do you need nulls? Most queues and every concurrent type in this series say no.
- Does another thread look? If yes, you want a
java.util.concurrenttype or you do not share. A synchronized wrapper is not the default.
If you cannot name the hot operation, you are not ready to name a class. Start there.
Indexed list
get(i) is cheap only when the slots sit in one array. A linked list makes you walk.
| Job | Reach for | Avoid as a default | Post |
|---|---|---|---|
| Growable indexed list | ArrayList | LinkedList, Vector | ArrayList: The Default List and When Growth Bites |
List contract, subList, RandomAccess | List + ArrayList | Treating every List as O(1) get | List: Index, SubList, and Random Access as a Type |
| List and deque on the same nodes | LinkedList (rarely) | LinkedList as the default List | LinkedList: Nodes When ArrayList and ArrayDeque Already Lost |
| Tiny unmodifiable list | List.of | Arrays.asList when you meant immutable | Collection Factories |
ArrayList is the default list. Reach for LinkedList only when you already hold a ListIterator and splice, or you truly need List plus Deque in one object. For stack and queue work, skip both and use ArrayDeque.
Uniqueness
Uniqueness is not List.contains. That method walks.
| Job | Reach for | Avoid as a default | Post |
|---|---|---|---|
| Unique elements, no order | HashSet | ArrayList.contains as a set | HashSet and LinkedHashSet |
| Unique and insertion order | LinkedHashSet | HashSet plus a side list | HashSet and LinkedHashSet |
| Unique and sorted | TreeSet | Sorting a HashSet on every read | Navigable Collections |
| Unique enums | EnumSet | HashSet<OrderStatus> | EnumSet and EnumMap |
The Set contract | Set | Assuming iteration order | Set: Uniqueness Without Scanning the List |
The Set post is the contract. HashSet is the usual delivery. LinkedHashSet is the sequenced hash set. TreeSet is sorted, O(log n), and rejects null under natural order.
Key → value
A map is mappings, not a Collection. Lookup by key is not a scan of pairs.
| Job | Reach for | Avoid as a default | Post |
|---|---|---|---|
| Key → value, no order | HashMap | Hashtable; a list of pairs | HashMap: The Default Map and What the Table Actually Does |
| Insertion or access order, LRU | LinkedHashMap | A list plus a map “to be sure” | LinkedHashMap |
| Sorted keys, ceiling/floor | TreeMap | Sorting HashMap keys on every read | Navigable Collections |
| Enum keys | EnumMap | HashMap<OrderStatus, …> | EnumSet and EnumMap |
== identity, not equals | IdentityHashMap | HashMap with records you meant as values | IdentityHashMap and WeakHashMap |
| GC-able keys | WeakHashMap | Using it as an LRU cache | IdentityHashMap and WeakHashMap |
| Shared map | ConcurrentHashMap | Hashtable, synchronizedMap | ConcurrentHashMap |
| Shared and sorted | ConcurrentSkipListMap | A locked TreeMap | Concurrent Queues and Skip Lists |
HashMap is the default map. computeIfAbsent and merge belong on the Map contract. Do not mutate a key after insert. WeakHashMap is not a cache with a max size — LinkedHashMap removeEldestEntry is the bounded LRU.
Ends, stacks, queues, next-best
Indexes are the wrong primitive when the job is the ends.
| Job | Reach for | Avoid as a default | Post |
|---|---|---|---|
| Stack / queue / deque, one thread | ArrayDeque | Stack, LinkedList as a queue | ArrayDeque |
offer / poll vs add / remove | Queue / Deque | Treating a queue as a List | Queue and Deque |
| Next-best, not FIFO | PriorityQueue | Sorting the whole list on every insert | PriorityQueue |
| Hand-off between threads | BlockingQueue | Busy-wait on a ConcurrentLinkedQueue | BlockingQueue |
| Non-blocking unbounded ends | ConcurrentLinkedQueue | Calling .take() on it (there is none) | Concurrent Queues and Skip Lists |
ArrayDeque is the default for stack and queue in one thread. PriorityQueue is a heap: iterator order is not sorted. Bounded producer-consumer is ArrayBlockingQueue or a capped LinkedBlockingQueue — the uncapped default capacity on LinkedBlockingQueue is a trap the blocking post names.
First, last, reversed
Java 21 gave ordered containers a shared vocabulary. HashSet and HashMap are not in it.
| Job | Reach for | Avoid as a default | Post |
|---|---|---|---|
| First / last / reversed on an ordered type | SequencedCollection | Index arithmetic on every ordered type | Sequenced Collections |
If you needed a sequence on uniqueness, that is LinkedHashSet, not HashSet iteration order.
Unmodifiable, views, and the methods around the types
| Job | Reach for | Avoid as a default | Post |
|---|---|---|---|
| Unmodifiable copy | List.copyOf / Map.copyOf / List.of | unmodifiableList wrapping a live ArrayList you still mutate | Collection Factories |
Sort, shuffle, asList, empty/singleton | Collections / Arrays | Arrays.asList when you meant List.of | Collections and Arrays |
| Iterate a collection, land in another | stream() + Collectors / Stream.toList() | parallelStream() plus add to an outer ArrayList | Spliterator and Collectors |
| Shared data you did not mean to pick | — | Vector, Stack, Hashtable | Legacy Collections |
Collections.synchronizedList still needs the iterator locked by you. Prefer CopyOnWriteArrayList when reads dominate and writes are rare, or ConcurrentHashMap for a shared map.
Shared with another thread
| Job | Reach for | Avoid as a default |
|---|---|---|
| Shared map | ConcurrentHashMap | Hashtable, Collections.synchronizedMap |
| Snapshot iteration, rare writes | CopyOnWriteArrayList | COW as a general ArrayList |
| Blocking FIFO buffer | ArrayBlockingQueue or a capped LinkedBlockingQueue | Uncapped LinkedBlockingQueue (capacity Integer.MAX_VALUE) |
| Next-best and another thread waits | PriorityBlockingQueue | A locked PriorityQueue |
| Available after a delay | DelayQueue | Treating it as a scheduler |
| Rendezvous (no buffer) | SynchronousQueue | A queue of size 1 “to be sure” |
| Transfer now if a consumer waits | LinkedTransferQueue | Busy-wait on ConcurrentLinkedQueue |
| Blocking both ends | LinkedBlockingDeque | Sharing an ArrayDeque |
| Non-blocking unbounded ends | ConcurrentLinkedQueue / ConcurrentLinkedDeque | Calling .take() on them (there is none) |
| Concurrent sorted map / set | ConcurrentSkipListMap / ConcurrentSkipListSet | A synchronized TreeMap |
Whole-table synchronized is not how you share a collection in 2026. Confine to one thread, or name a concurrent type.
Interview lens
This page is the “which type?” round. Interviewers want the default, then the exception.
| Question | Honest answer |
|---|---|
Default List / Set / Map / deque? | ArrayList, HashSet, HashMap, ArrayDeque. |
When LinkedList? | Almost never as a List. As a deque, ArrayDeque wins. |
When TreeMap instead of HashMap? | You need sorted keys or ceiling/floor, and you can pay O(log n). |
When ConcurrentHashMap instead of HashMap? | Another thread looks. Not because “we might need it later.” |
Vector vs ArrayList? | Vector is legacy whole-table lock. It is not the thread-safe ArrayList. |
| Queue for workers? | One thread: ArrayDeque. Two threads: BlockingQueue. |
| Unique + order? | LinkedHashSet, not HashSet iteration. |
What to draw. Four defaults. Then one extra for order (LinkedHash*), one for sort (Tree*), one for share (ConcurrentHashMap), one for hand-off (BlockingQueue).
Wrong answer: “Always use Vector and Hashtable in a web app because they are synchronized.”
Cheat sheet
Hot op first index | unique | key | ends | next-best | sorted | hand-off
Then order? nulls? another thread?
Defaults ArrayList HashSet HashMap ArrayDeque
Order LinkedHashSet / LinkedHashMap
Sorted TreeSet / TreeMap O(log n)
Enum universe EnumSet / EnumMap
Share a map ConcurrentHashMap no nulls
Hand-off BlockingQueue put/take
Snapshot reads CopyOnWriteArrayList writes copy the array
Skip Vector, Stack, Hashtable, LinkedList-as-default
Not a cache WeakHashMap
Not a sequence HashSet / HashMap iteration
Do:
- Pick the type for the operation that runs in the hot path.
- Program to
List/Set/Map/Deque.newthe class that matches the four questions. - Prefer factories (
List.of,copyOf) when the collection should not grow.
Don’t:
- Scan a list to fake a set or a map.
- Share an
ArrayListorHashMapacross threads because tests passed with one worker. - Treat
HashSetorder,PriorityQueueiteration, orWeakHashMapas an LRU.
Wrap-up
The Collections Framework is a small set of contracts and a larger set of deliveries. ArrayList, HashSet, HashMap, and ArrayDeque cover most application code. Order, sorting, an enum universe, another thread, or a blocking hand-off are the reasons to leave those defaults — not a longer class name for the same job.
The Collections Roadmap is the glossary and the living catalog. This page is the “which new?” test. If the hot operation is a layout question more than a JDK type question, start at Pick the Right Structure.