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
This commit is contained in:
2026-10-01 04:27:27 +00:00
co-authored by Claude Sonnet 5
commit d4b3a63615
27 changed files with 1085 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
# 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 &mdash; nothing is retyped or paraphrased from memory.
One module per article. This repository currently holds:
| Module | Article | Status |
|---|---|---|
| [`collections`](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
```bash
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` &mdash; 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** &mdash; 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
&mdash; 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 &mdash; 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&rarr;32&rarr;64 purely from `treeifyBin()`'s own resize calls, then a real `Node`&rarr;`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 &mdash; 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 &mdash; see [LICENSE](LICENSE).