4.9 KiB
list-benchmarks
Companion code for the ankurm.com post "ArrayList vs LinkedList in 2026: JMH Benchmarks and Why
LinkedList Rarely Wins." Module in java-core-examples, the Java-core series.
Versions this was built and tested against
| Component | Version | Notes |
|---|---|---|
| JDK | 25.0.4.1+1 (Temurin, LTS) | |
| JMH | 1.37 | |
| JUnit Jupiter | 5.11.0 | Correctness tests only - do not read these as throughput proof. |
| Maven | 3.9.11 | |
| Hardware | shared multi-tenant container | See the honesty note below - this is not an isolated benchmarking box. |
Quickstart
export JAVA_HOME=/path/to/jdk-25
mvn package
java -jar target/benchmarks.jar ListOpsBenchmark
java -jar target/benchmarks.jar DequeBenchmark
java -cp target/classes com.ankurm.listbenchmarks.IteratorRemovalDemo
scripts/run-all.sh regenerates every file in output/. scripts/run.sh <Class> [jmh-args] runs
one benchmark ad hoc.
What's in here
| File | What it shows |
|---|---|
src/main/java/.../ListOpsBenchmark.java |
ArrayList vs LinkedList: append, front-insert, middle-insert, random get, full iteration. Each mutating benchmark inserts then immediately removes, so list size stays constant across the whole run. |
src/main/java/.../DequeBenchmark.java |
ArrayDeque vs LinkedList vs ArrayList used as a stack/queue: push/pop at the head, offer/poll at the tail. |
src/main/java/.../IteratorRemovalDemo.java |
The one case LinkedList can win: removing through an Iterator cursor instead of by index. Plain nanoTime(), not JMH - read as illustrative. |
src/test/java/.../CorrectnessTest.java |
Sanity checks: both lists agree on contents and ordering; ArrayDeque genuinely does not implement List. |
output/01-list-ops-sweep.txt |
Full JMH report for ListOpsBenchmark, sizes 1,000 and 100,000. |
output/02-deque-ops-sweep.txt |
Full JMH report for DequeBenchmark, sizes 1,000 and 100,000. |
output/03-iterator-removal-demo.txt |
Captured run of the iterator-removal illustration. |
output/04-correctness-test.txt |
JUnit run (4/4 passing). |
Reading the numbers honestly (shared container, not an isolated box)
This run shares CPU with five other concurrent build/benchmark workloads on the same host - JMH
was still run one class at a time, back to back, never overlapping with itself, but it cannot see
or control what else is scheduled on the machine. Several rows carry an error bar wider than the
mean (DequeBenchmark.offerPollTail ArrayDeque @1000: 203,926 ± 985,600 ops/ms), which is
the signature of scheduler noise, not of the JVM doing something strange. Treat every number here
as directional, and prefer the ratios between rows measured in the same run over the
absolute ops/ms.
With that caveat:
addFrontis the one operation whereLinkedListclearly and consistently wins - flat ~43,000 ops/ms at both 1,000 and 100,000 elements, because it is an O(1) pointer write regardless of size.ArrayList.addFrontfalls from ~6,600 ops/ms at 1,000 elements to ~53 ops/ms at 100,000 - two orders of magnitude - because every call shifts the whole array by one.addMiddlefavorsArrayListby roughly 20-25x at both sizes, even though both implementations are doing O(n) work to get there.ArrayList's half is a singleSystem.arraycopy;LinkedList's is a pointer-chasing walk node by node to find the midpoint, and that walk is the slower half of "O(n) to reach the middle, O(1) to unlink."getRandomanditerateSumboth favorArrayList, the first by ~2,000x at 100,000 elements (random access onLinkedListis a full traversal per call), the second by ~4-5x even though iteration is O(n) either way - this is the cache-locality gap the post discusses: one contiguousint[]-backed array versus following a chain of individually-allocatedNodeobjects scattered across the heap.- For stack/queue-shaped work,
ArrayDequeis the one to reach for, notLinkedList-pushPopHeadfavorsArrayDequeoverLinkedListby ~3.6x at 1,000 elements, andArrayDequenever degrades toArrayList's O(n) head-shift problem (ArrayList.pushPopHeadcollapses to 56 ops/ms at 100,000 elements, a 1,800x gap fromArrayDeque's 103,406). - The iterator-removal illustration (
output/03) is the sharpest number in this repository: removing every third element of a 200,000-element list through anIteratorcursor takes 1,314 ms withArrayListand 5.0 ms withLinkedList- a 262x gap - becauseArrayList.remove(index)still shifts the tail of the array on every call even when the index came from an iterator already sitting on it, whileLinkedList.Iterator.remove()unlinks the node the cursor already holds. This is also, honestly, close to the only realistic case where reaching forLinkedListoverArrayListis the right call in 2026 code.
License
MIT - see the repo-wide LICENSE.