skip to content
everything.collections.choosingACollection

choosing a collection

a decision tree and a big-O cheat sheet.

chapter
chapter 06.12
reading time
6 min read

start with the defaults: ArrayList, HashMap, HashSet, ArrayDeque. switch only when a question demands it: need order? LinkedHashMap / TreeMap. need the minimum? PriorityQueue. enum keys? EnumMap / EnumSet. shared between threads? the concurrent versions.

and almost never LinkedList, Vector, Stack or Hashtable.

the picture

you know how there's always a coworker who reaches for a TreeMap "because it's sorted" when nothing ever reads it in order? the collection you pick is a claim about how the data is used. pick by the questions you'll ask of it, not by the name that sounds most capable.

    what are you storing?
    answer a few questions. click an earlier answer to change it.
    answer the questions the way your code actually uses the data. the recommendation links to the chapter that explains it. click an earlier answer to go back.

    the questions, in the order that matters:

    1. values or key → value pairs? that's Collection vs Map.
    2. will threads modify it concurrently? if yes, everything below has a different answer (see concurrent collections). wrapping in Collections.synchronizedX works for simple cases, but compound actions still need your own locking.
    3. duplicates? no → a Set.
    4. what order do you need back? none → hash. insertion → linked hash. sorted or ranges → tree. by priority → heap.
    5. how do you take things out? by index → ArrayList. from the ends → ArrayDeque.
    6. does it change after you build it? no → List.copyOf / Set.copyOf / Map.copyOf, and stop worrying about who modifies it.

    the cheat sheet

    collectionget / lookupaddremoveordered byinside
    ArrayListget(i) O(1)end: O(1)*, middle: O(n)end: O(1), middle: O(n)indexObject[]
    LinkedListget(i) O(n)ends: O(1)ends: O(1)indexnodes
    ArrayDequeends only, O(1)ends: O(1)*ends: O(1)insertioncircular Object[]
    PriorityQueuepeek O(1)O(log n)poll O(log n)priority (only the head)heap in Object[]
    HashMap / HashSetO(1) avgO(1) avg*O(1) avgnothingbuckets
    LinkedHashMap / LinkedHashSetO(1) avgO(1) avg*O(1) avginsertion (or access)buckets + list
    TreeMap / TreeSetO(log n)O(log n)O(log n)sortedred-black tree
    EnumMap / EnumSetO(1)O(1)O(1)declarationarray / bits
    * amortized: the occasional resize copies everything. contains(x) on any list is O(n).

    big-O hides constants, and the constants are mostly about memory: a flat array of references beats a web of node objects, because the CPU can prefetch an array and can't predict pointers. that's why ArrayList and ArrayDeque win so often even where the big-O ties.

    what the big-O looks like in nanoseconds

    rough numbers, same setup as the deque chapter: jdk 25.0.4 (temurin), apple M4 Pro, default G1 and heap, 100,000 Integers, System.nanoTime around whole loops after warmup rounds, median of 7, three runs agreeing within about 20%. not a JMH benchmark: treat these as orders of magnitude, not as the speed of your code.

    contains(x), 100,000 elements
    ArrayList15,300 ns/op
    TreeSet98 ns/op
    HashSet37 ns/op
    log scale
    get(key), 100,000 entries
    TreeMap164 ns/op
    HashMap33 ns/op
    log scale
    log scale. the first group is "is x in here?" over 100,000 elements: a list scans, a hash set jumps, a tree set walks ~17 levels. the second is get(key) on 100,000 entries.

    the gap that matters is the 400x between scanning and hashing, not the 3 to 5x between hashing and a tree. if you call list.contains inside a loop, that's the bug. if you're choosing between HashMap and TreeMap, choose by whether you need the order.

    quick check
    you need to look users up by id, and also list them in the order they signed up. which one?
    quick check
    a job scheduler keeps adding tasks and always runs the most urgent one next. which one?

    the ones to skip

    skipwhyuse
    LinkedList24 bytes per element, O(n) indexing, cache-hostileArrayList or ArrayDeque
    Vector, Stackevery method synchronized, legacy since java 1.2ArrayList, ArrayDeque
    Hashtablesame story, and no nullsHashMap or ConcurrentHashMap
    Collections.synchronizedList(...) for heavy concurrencyone lock for everythingthe java.util.concurrent classes
    keylines
    • defaults: ArrayList, HashMap, HashSet, ArrayDeque.
    • need order? linked for insertion, tree for sorted, heap for "next best".
    • enum keys or elements: EnumMap, EnumSet.
    • threads modifying it: the java.util.concurrent versions.
    • an O(n) call inside a loop beats any class choice for slowness. find it first.
    • built once, never changed: List.copyOf and friends.