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:
@@ -0,0 +1,8 @@
|
||||
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field transient java.util.HashMap$Node[] java.util.HashMap.table accessible: module java.base does not "opens java.util" to unnamed module @1dbd16a6
|
||||
at java.base/java.lang.reflect.AccessibleObject.throwInaccessibleObjectException(AccessibleObject.java:353)
|
||||
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:329)
|
||||
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:277)
|
||||
at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:179)
|
||||
at java.base/java.lang.reflect.Field.setAccessible(Field.java:173)
|
||||
at com.ankurm.interviewlab.collections.BucketInspector.dump(BucketInspector.java:41)
|
||||
at com.ankurm.interviewlab.collections.ReflectionAddOpensDemo.main(ReflectionAddOpensDemo.java:23)
|
||||
@@ -0,0 +1,5 @@
|
||||
reflection succeeded: 2 non-empty bucket(s)
|
||||
table.length = 16 | non-empty buckets = 2
|
||||
bucket[ 7] chain=1 kind=Node
|
||||
bucket[ 14] chain=1 kind=Node
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
Real bucket layout of a 6-entry HashMap<String,Integer> (default capacity)
|
||||
==========================================================================
|
||||
|
||||
map.size() = 6
|
||||
table.length = 16 | non-empty buckets = 4
|
||||
bucket[ 0] chain=2 kind=Node
|
||||
bucket[ 1] chain=2 kind=Node
|
||||
bucket[ 4] chain=1 kind=Node
|
||||
bucket[ 5] chain=1 kind=Node
|
||||
|
||||
Index for each key, computed the same way HashMap.hash() does it:
|
||||
apple hashCode=93029210 spread=93030097 index=(16-1)&spread=1
|
||||
banana hashCode=-1396355227 spread=-1396317280 index=(16-1)&spread=0
|
||||
cherry hashCode=-1361513063 spread=-1361552575 index=(16-1)&spread=1
|
||||
date hashCode=3076014 spread=3075968 index=(16-1)&spread=0
|
||||
egg hashCode=100357 spread=100356 index=(16-1)&spread=4
|
||||
fig hashCode=101380 spread=101381 index=(16-1)&spread=5
|
||||
@@ -0,0 +1,11 @@
|
||||
Forcing real treeification: 20 keys with hashCode()==1, loadFactor=1000f (size-resize disabled)
|
||||
===============================================================================================
|
||||
|
||||
All keys hash to the same bucket (see CollidingKey javadoc for why a resize can't separate them).
|
||||
|
||||
after inserting key #1 : table.length=16 bucket[1] chain=1 kind=Node
|
||||
after inserting key #9 : table.length=32 bucket[1] chain=9 kind=Node
|
||||
after inserting key #10 : table.length=64 bucket[1] chain=10 kind=Node
|
||||
after inserting key #11 : table.length=64 bucket[1] chain=11 kind=TreeNode
|
||||
|
||||
Final state: table.length=64, bucket[1] kind=TreeNode, first became a tree at insert #11
|
||||
@@ -0,0 +1,14 @@
|
||||
Classic ConcurrentModificationException: for-each + Collection.remove() mid-loop
|
||||
================================================================================
|
||||
|
||||
list before: [1, 2, 3, 4, 5]
|
||||
threw: java.util.ConcurrentModificationException
|
||||
|
||||
stack trace (trimmed to the ArrayList$Itr frames that matter):
|
||||
java.util.ConcurrentModificationException
|
||||
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
|
||||
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050)
|
||||
at com.ankurm.interviewlab.collections.ConcurrentModificationTest.lambda$classicForEachPlusCollectionRemoveThrows$0(ConcurrentModificationTest.java:28)
|
||||
at com.ankurm.interviewlab.collections.ConcurrentModificationTest.classicForEachPlusCollectionRemoveThrows(ConcurrentModificationTest.java:27)
|
||||
at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
|
||||
at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
|
||||
@@ -0,0 +1,13 @@
|
||||
The fail-fast contract is 'best-effort', not guaranteed: a case that does NOT throw
|
||||
===================================================================================
|
||||
|
||||
list before: [10, 20] (size=2)
|
||||
elements the loop actually saw before ending: [10]
|
||||
list after the loop: [20]
|
||||
ConcurrentModificationException thrown: false
|
||||
|
||||
Why: after removing 10, size becomes 1 and the iterator's cursor is already 1
|
||||
(it advanced to 1 when next() returned 10). ArrayList.Itr.hasNext() is just
|
||||
'return cursor != size;' - no modCount check - so hasNext() sees 1 != 1, returns
|
||||
false, and the loop ends normally. next() is the only method that checks
|
||||
modCount, and it is never called again. The element 20 is silently never visited.
|
||||
@@ -0,0 +1,8 @@
|
||||
ConcurrentHashMap's iterator is weakly consistent: it never throws CME
|
||||
======================================================================
|
||||
|
||||
map before: {0=v0, 1=v1, 2=v2, 3=v3, 4=v4}
|
||||
entries visited during iteration (original 5, plus possibly some newly-added ones): 19
|
||||
map after: 24 entries total
|
||||
ConcurrentModificationException thrown: false
|
||||
(weakly consistent: may or may not reflect the mutation, but is guaranteed never to throw)
|
||||
@@ -0,0 +1,5 @@
|
||||
The fix: Iterator.remove() instead of Collection.remove() during iteration
|
||||
==========================================================================
|
||||
|
||||
list before: [1, 2, 3, 4, 5]
|
||||
list after removing all even numbers via Iterator.remove(): [1, 3, 5]
|
||||
@@ -0,0 +1,10 @@
|
||||
equals() overridden without hashCode(): a == b is true, but hashCode() differs
|
||||
==============================================================================
|
||||
|
||||
a.equals(b) = true
|
||||
a.hashCode() = 1390869998
|
||||
b.hashCode() = 1820383114
|
||||
(these should be IDENTICAL per the contract - they are not, because hashCode() was never overridden)
|
||||
|
||||
HashSet<EqualsOnlyPoint> after adding two equal-but-differently-hashed points: size=2
|
||||
HashSet<CorrectPoint> after adding two equal, correctly-hashed points: size=1
|
||||
@@ -0,0 +1,9 @@
|
||||
Mutating a key's hash-relevant field after insertion: the entry is still THERE, just unfindable
|
||||
===============================================================================================
|
||||
|
||||
map.put(key(tag=42), ...) then key.setTag(99)
|
||||
containsKey(new MutableKey(42)) before mutation : true
|
||||
containsKey(new MutableKey(42)) after mutation : false (looks for old hash's bucket - key no longer hashes there)
|
||||
containsKey(new MutableKey(99)) after mutation : false (looks in the NEW hash's bucket - key was never filed there either)
|
||||
map.size() is still : 1 (the entry was never removed!)
|
||||
the mutated key object IS still found by direct iteration over keySet(): true
|
||||
Reference in New Issue
Block a user