Files
asmhatreandClaude Sonnet 5 d4b3a63615 Add collections module: HashMap bucket layout, treeification, and a real ConcurrentModificationException reproduction
Companion code for the ankurm.com post "Top 40 Java Collections Interview Questions
(HashMap Internals to Fail-Fast Iterators)". Verified against JDK 25's own HashMap
source (TREEIFY_THRESHOLD=8, UNTREEIFY_THRESHOLD=6, MIN_TREEIFY_CAPACITY=64), with
every claim backed by a committed docs/output/*.txt transcript from a real run.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YXCrLgRKFgCh9RHKW8xaqJ
2026-10-01 04:27:27 +00:00

5.8 KiB

java-interview-lab

Runnable companion code for ankurm.com's Java interview-prep article series. Every number, stack trace, and console line quoted in those articles comes from a file committed somewhere under this repository — nothing is retyped or paraphrased from memory.

One module per article. This repository currently holds:

Module Article Status
collections Top 40 Java Collections Interview Questions (HashMap Internals to Fail-Fast Iterators) this commit

collections module

HashMap/HashSet/ArrayList internals, verified against the real JDK 25 java.util.HashMap source (not against its javadoc prose) and reproduced with real, runnable code.

Versions

Component Version Verified against
JDK 25.0.4.1+1 (Temurin, LTS) java -version; java.util.HashMap source extracted from this JDK's own lib/src.zip
JUnit 5.11.0 org.junit.jupiter:junit-jupiter on Maven Central
Maven 3.9.11 mvn -version

The three constants this article leans on hardest (TREEIFY_THRESHOLD = 8, UNTREEIFY_THRESHOLD = 6, MIN_TREEIFY_CAPACITY = 64) were read directly out of java.util.HashMap's source for this exact JDK build, not copied from a blog post or an older javadoc. See collections/src/main/java/.../BucketInspector.java for the reflective dump that proves them at runtime.

Quickstart

cd collections
export JAVA_HOME=/path/to/jdk-25   # must be JDK 25 or newer
export PATH="$JAVA_HOME/bin:$PATH"

mvn test                           # runs everything, including the HashMap-internals demos

The first mvn invocation in a fresh environment must run online (not -o) once, so the compiler/surefire plugins populate the local repository cache; -o works for every run after that. This is a Maven quirk, not specific to this module.

Run one scenario at a time with ./scripts/run.sh <name> (add-opens-fail, add-opens-ok, treeify, cme, eqhash, bucket — see the script for what each prints). Regenerate every committed transcript in docs/output/ with ./scripts/run-all.sh.

No HTTP endpoints in this module — every demo is a plain Java main() method or a JUnit test; there is nothing to start as a server. The diagnostic tool is BucketInspector.dump(HashMap), a reflective helper, not an endpoint.

The one flag that matters: --add-opens

BucketInspector reflects into java.util.HashMap's private table field to show the real bucket layout. Since Java 9, that throws InaccessibleObjectException unless the JVM is started with:

--add-opens java.base/java.util=ALL-UNNAMED

pom.xml's surefire configuration sets this for mvn test automatically. Running ReflectionAddOpensDemo directly with plain java -cp ... reproduces the crash on purpose — see docs/output/01-reflection-without-add-opens.txt.

Source layout

collections/
├── pom.xml
├── scripts/
│   ├── run.sh          one named scenario at a time
│   └── run-all.sh       regenerates every file in docs/output/
├── src/main/java/com/ankurm/interviewlab/collections/
│   ├── BucketInspector.java          reflective bucket/bin dump (needs --add-opens)
│   ├── CollidingKey.java             hashCode()==1 for every instance - forces one bucket
│   ├── ReflectionAddOpensDemo.java   standalone entry point for the --add-opens demo
│   └── eqhash/
│       ├── EqualsOnlyPoint.java      equals() overridden, hashCode() is NOT - the broken case
│       ├── CorrectPoint.java         both overridden consistently - the fixed case
│       └── MutableKey.java           a key whose hash-relevant field can change post-insertion
├── src/test/java/com/ankurm/interviewlab/collections/
│   ├── support/Transcript.java       writes docs/output/NN-*.txt while the test asserts the same numbers
│   ├── BucketLayoutTest.java
│   ├── TreeifyTriggerTest.java
│   ├── ConcurrentModificationTest.java
│   └── EqualsHashCodeContractTest.java
└── docs/output/*.txt   captured real output - see index below

Index of captured output (docs/output/)

File What it proves
01-reflection-without-add-opens.txt InaccessibleObjectException, uncaught, crashing the JVM — real stack trace
02-reflection-with-add-opens.txt Same code, with the flag: a real bucket dump of a 2-entry HashMap
03-bucket-layout-small-map.txt Real bucket indices for 6 string keys at default capacity 16
04-treeify-trigger.txt Capacity doubling 16→32→64 purely from treeifyBin()'s own resize calls, then a real Node→TreeNode conversion at insert #11
05-cme-classic-reproduction.txt A real ConcurrentModificationException, full stack trace, from for-each + Collection.remove()
06-cme-silent-non-reproduction.txt The documented case where the SAME kind of mid-loop removal does not throw
07-cme-concurrenthashmap-no-throw.txt ConcurrentHashMap's weakly-consistent iterator mutated during iteration, never throws
08-iterator-remove-safe.txt Iterator.remove() used correctly — no exception, correct result
09-equals-hashcode-broken-contract.txt equals() without hashCode(): a real HashSet failing to deduplicate two equal objects
10-mutable-key-lost-entry.txt A key mutated after insertion: the entry is unreachable by lookup but still physically present

All ten are regenerated by one command (./scripts/run-all.sh); eight of the ten (03 through 10) are generated by a JUnit test that also asserts the same numbers it prints, so a figure in the article that stops being true fails the build, not just the next blog audit.

License

MIT — see LICENSE.