Java Collections Internals & Performance
Look under the hood of Java's collections. After this lesson you'll know exactly how ArrayList, LinkedList, HashMap, TreeMap, and HashSet store data — and you'll pick the right one by its Big-O instead of by guessing.
Learn Java Collections Internals & Performance in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and…
Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
You should already know how to use Collections and Generics . This lesson explains the machinery inside them, so you understand why each one is fast or slow — not just how to call add() and get() .
Every collection is just a different strategy for storing items, with different trade-offs:
An ArrayList wraps a plain array (the backing array ). Because the items sit in one contiguous block, jumping to get(i) is a single calculation — that's O(1) random access.
Arrays can't grow, so when the backing array fills up, ArrayList allocates a bigger one (about 1.5x the size), copies everything across, and throws the old one away. Each individual copy is O(n), but because each resize buys a lot of headroom, the copies are rare. Averaged over many add() calls the cost is constant — this is amortized O(1) .
Inserting or removing in the middle is the slow case: every element after the gap must shift one slot, which is O(n) . Read the resizing happen in the output below.
A LinkedList stores no array at all. Each value lives in its own node , and every node holds a pointer to the previous and next node. Java's LinkedList is doubly-linked and keeps a reference to both the head and the tail.
That makes adding or removing at either end O(1) — you just re-wire a couple of pointers, nothing shifts. But there's no index math: to reach get(47) you must walk node by node from the nearest end, which is O(n) . The output prints how many nodes it walks.
A HashMap is an array of buckets . To store a key it calls the key's hashCode() , squeezes that number into the bucket range, and drops the entry there. Lookup repeats the same calculation and scans only that one bucket — so on average put() and get() are O(1) .
When two different keys land in the same bucket that's a collision , and the bucket holds a little list of entries. equals() then decides which entry is really your key. The load factor (size capacity, default 0.75) controls growth: once the map passes 0.75 full it doubles the bucket array and rehashes everything to keep collisions low.
A TreeMap keeps its keys in sorted order using a red-black tree — a binary search tree that rebalances itself on every insert so it never degrades into a slow lopsided shape. Every operation costs O(log n) : a touch slower than HashMap's O(1), but you get ordered iteration plus firstKey() , lastKey() , ceilingKey() , and range views for free.
A HashSet is simply a HashMap where you only care about the keys — the values are a hidden dummy object. That's why a set's elements are unique and why add() and contains() are average O(1) . (A TreeSet is the same trick over a TreeMap, giving sorted, O(log n) behaviour.)
Fill in the two blanks so a Point works as a HashMap key. Remember the contract: equal objects must return equal hash codes. The expected output is in the comments so you can self-check.
Each comment describes a job. Replace each ___ with the collection whose internals best fit that job. The hint on each line tells you the answer — match it to what you learned above.
Now with the scaffolding removed. You get only a comment outline and the expected output. Count each word, then print the counts in alphabetical order — the right Map gives you the sorting for free.
💡 Pre-size when you know the count: new ArrayList (10000) or new HashMap (expected * 4 / 3) skips repeated resize-and-copy work.
💡 Default to ArrayList: contiguous memory is CPU-cache friendly, so it beats LinkedList for almost everything. Reach for LinkedList (or ArrayDeque) only for true queue/deque workloads.
💡 Records are perfect keys: a record auto-generates correct, immutable equals() and hashCode() — ideal as HashMap keys with zero boilerplate.
* ArrayList add at the end is amortized O(1); "avg" means average case — a degenerate hashCode() can make hash collections O(n), or O(log n) once a bucket treeifies.
You can now explain how each core collection stores its data: ArrayList's resizing backing array, LinkedList's pointer-chained nodes, HashMap's buckets driven by hashCode() and equals() , TreeMap's self-balancing red-black tree, and HashSet riding on top of a HashMap. Best of all, you can read the right choice straight off the Big-O table.
Next up: Advanced HashMap, TreeMap & LinkedHashMap — ordering guarantees, navigable maps, and the tricks that make these data structures sing in real code.
Practice quiz
Why is ArrayList.add() at the end called amortized O(1) even though it sometimes copies the whole array?
- It never copies
- The copy is free
- Each resize roughly doubles capacity, so copies get rarer; averaged out the cost is constant
- It is actually O(n) per add
Answer: Each resize roughly doubles capacity, so copies get rarer; averaged out the cost is constant. Resizes grow capacity geometrically, so the expensive O(n) copies happen rarely — average cost per add stays constant.
What is the Big-O of ArrayList.get(i) (random access by index)?
- O(1)
- O(log n)
- O(n)
- O(n²)
Answer: O(1). Items sit in one contiguous block, so jumping to index i is a single calculation — O(1).
For a LinkedList, what is the cost of get(index) deep in the list?
- O(1)
- O(log n)
- O(n²)
- O(n) — it must walk the chain
Answer: O(n) — it must walk the chain. LinkedList has no index math; it walks node by node from the nearest end, which is O(n).
LinkedList's strength over ArrayList is that adding/removing at either END is:
- O(n)
- O(1) — just re-wire a couple of pointers
- O(log n)
- Impossible
Answer: O(1) — just re-wire a couple of pointers. Java's LinkedList tracks head and tail, so end insertions/removals only re-wire pointers — O(1).
When storing a key, a HashMap uses hashCode() to pick the bucket and then uses what to find the exact key?
- equals()
- toString()
- compareTo()
- clone()
Answer: equals(). hashCode() chooses the bucket; equals() identifies the exact key within that bucket's entries.
Why must you override both equals() and hashCode() together for a HashMap key?
- It is just a style rule
- hashCode() alone is enough
- Equal objects must return equal hash codes, or they land in different buckets and lookups fail
- equals() alone is enough
Answer: Equal objects must return equal hash codes, or they land in different buckets and lookups fail. If equal objects have different hash codes they go to different buckets, so the map never matches them — get returns null.
What does the default load factor 0.75 trigger when exceeded?
- The map is cleared
- The bucket array doubles and everything is rehashed
- The map throws an exception
- Nothing happens
Answer: The bucket array doubles and everything is rehashed. Load factor is size/capacity; passing 0.75 makes HashMap double its buckets and rehash to keep collisions low.
Since Java 8, a single HashMap bucket converts its list to a red-black tree (treeify) when it grows past:
- 2 entries
- 16 entries
- It never treeifies
- 8 entries (on a table of at least 64 buckets)
Answer: 8 entries (on a table of at least 64 buckets). Past 8 entries in one bucket (table ≥ 64), it treeifies so worst-case lookups in that bucket drop to O(log n).
TreeMap keeps keys sorted using a red-black tree, giving each operation a cost of:
- O(1)
- O(log n)
- O(n)
- O(n log n)
Answer: O(log n). A self-balancing binary search tree gives O(log n) per operation, plus sorted iteration and navigation methods.
A HashSet is essentially:
- A TreeMap with no values
- A LinkedList of unique items
- A HashMap where only the keys matter (values are a hidden dummy)
- An ArrayList that rejects duplicates
Answer: A HashMap where only the keys matter (values are a hidden dummy). HashSet rides on a HashMap using only keys, so add() and contains() are average O(1) and elements are unique.