equals-hashcode: equals/hashCode contract demos, missing hashCode and constant-hash costs, mutable keys, records, JPA entity id trap, 13 contract tests

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b66uwsWr9B1ouyrzUh
This commit is contained in:
Claude
2026-11-12 20:08:35 +05:30
parent 319697c345
commit 682b7b067c
22 changed files with 692 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
== Reflexive / null ==
p.equals(p) = true
p.equals(null) = false
== Symmetry broken by instanceof + extra field ==
plain.equals(red) = true
red.equals(plain) = false
List.of(plain).contains(red) = false
List.of(red).contains(plain) = true
== Transitivity broken by the lenient fix ==
r.equals(plain) = true
plain.equals(b) = true
r.equals(b) = false
== Consistency broken by a changing equals() ==
f.equals(f2) x4 = true false true false
@@ -0,0 +1,15 @@
== equals() without hashCode() ==
a.equals(b) = true
a.hashCode() == b.hashCode() = false
HashSet size after adding a then b = 2
HashSet.contains(new equal key) = false
HashMap.get(equal key) = null
== with hashCode() ==
HashSet size after adding c then d = 1
HashSet.contains(new equal key) = true
== a legal but useless hashCode(): return 42 ==
keys = 2000
equals() calls for 2000 gets, constant hash = 2002999
equals() calls for 2000 gets, good hash = 2000
+14
View File
@@ -0,0 +1,14 @@
before: get = 100, contains = true
after acc.owner = "ankur":
map.get(acc) = null
set.contains(acc) = false
map.containsKey(acc) = false
map.size() = 1
iteration still sees it = [Account(ankur)]
map.remove(acc) = null
map.size() after remove = 1
set.add(acc) again -> size = 2 [Account(ankur), Account(ankur)] (one object, two entries)
immutable key: map.get("asha") = 1
@@ -0,0 +1,3 @@
date.equals(ts) = true
ts.equals(date) = false
date.getTime() == ts.getTime() = true
+23
View File
@@ -0,0 +1,23 @@
== generated equals/hashCode ==
a.equals(b) = true
a.hashCode() == b.hashCode() = true
new HashSet of both, size = 1
a == b = false
== array component: reference equality ==
w1.equals(w2) = false
same array shared: true
== mutable List component: equals works, hashCode can change ==
hashCode before = 4714, after list.add(3) = 34537
record in HashSet, list mutated -> contains = false
== doubles compare with Double.compare semantics ==
NaN record equals NaN record = true
Double.NaN == Double.NaN = false
0.0 record equals -0.0 record = false
0.0 == -0.0 = true
== overriding equals in a record ==
t1.equals(t2) = true
toString still shows both fields: Tagged[isbn=1, title=Effective Java 3e]
+10
View File
@@ -0,0 +1,10 @@
== id-based equals/hashCode, id assigned on save ==
two unsaved entities equal? true (both ids null)
set size after adding two different unsaved entities = 1
add, then save(): set.contains(c) = false
== constant hashCode ==
add, then save(): set.contains(d) = true
== natural key, immutable ==
add, then save(): set.contains(e) = true
@@ -0,0 +1,22 @@
== what the JDK uses ==
"hello".hashCode() = 99162322
31*h + c over "hello" = 99162322
List.of(1,2,3).hashCode() = 30817
Arrays.hashCode({1,2,3}) = 30817
Objects.hash(1,2,3) = 30817
Set.of(1,2,3).hashCode() = 6 (sum of elements)
Map.of(1,2).hashCode() = 3 (key^value per entry, summed)
== Objects.hash(...) traps ==
Objects.hash("a") vs "a".hashCode() = 128 vs 97
Objects.hash(1, 2) == Objects.hash(2, 1) ? false
Objects.hash() (no args) = 1
Objects.hash((Object) null) = 31
== Arrays as fields ==
x.equals(y) = false, Arrays.equals = true
x.hashCode() == y.hashCode() ? false
Arrays.hashCode equal ? true
== equal hashCodes do not mean equal objects ==
"Aa".hashCode() = 2112, "BB".hashCode() = 2112, equal strings? false
+3
View File
@@ -0,0 +1,3 @@
[INFO] Tests run: 13, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.205 s -- in com.ankurm.equalshashcode.EqualsHashCodeTest
[INFO] Tests run: 13, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS