equals, hashCode, and Comparable
Source: Effective Java, 3rd Edition — Chapter 3 by Joshua Bloch (Items 10–14)
Object’s nonfinal methods (equals, hashCode, toString, clone, finalize) have contracts. Break them and HashMap / HashSet misbehave. finalize was Item 8; here we add Comparable.compareTo, which has the same flavor.
Item 10: Obey the equals contract
Don’t override when identity is enough: unique entities (Thread), no logical equality needed (Pattern), a superclass already did it right (AbstractList), or the method will never run (optional throw new AssertionError()).
Do override for value classes (String, Integer) used as map keys / set elements — unless instance control already makes == the same as logical equality (enums).
Contract (equivalence relation)
| Rule | Meaning |
|---|---|
| Reflexive | x.equals(x) |
| Symmetric | x.equals(y) iff y.equals(x) |
| Transitive | chain of equals implies first equals last |
| Consistent | stable if state used by equals is unchanged |
| Non-null | x.equals(null) is false (no NPE) |
Symmetry fail: CaseInsensitiveString treating String as equal one way — cis.equals(s) true, s.equals(cis) false. Drop the one-way interop.
Transitivity / LSP fail: subclass adds a value field (ColorPoint on Point). “Full compare” breaks symmetry; “color-blind mixed compare” breaks transitivity (and can recurse across sibling subclasses). getClass() instead of instanceof breaks Liskov: CounterPoint is still a Point but fails unitCircle.contains. Workaround: composition — ColorPoint has a Point + asPoint() view. Abstract superclass + concrete leaves is OK. Don’t emulate Timestamp/Date.
Consistency: immutable equals stay forever equal; never hang equals on DNS (URL was a mistake).
Non-nullity: instanceof is false for null — no separate null check needed.
Recipe
if (o == this) return true;if (!(o instanceof MyType)) return false;- Cast.
- Compare significant fields (
==for most primitives;Float/Double.compare; recursiveequals;Objects.equalsfor nullable refs;Arrays.equals).
Order cheap/likely-to-differ fields first. Skip locks and pure derived fields (unless a cheap summary). Always pair with hashCode. Parameter must be Object — equals(MyClass) overloads, not overrides (@Override catches this). Prefer AutoValue / IDE generation over hand typos.
equals contract
1 / 2CaseInsensitiveString.equals(String) true but String.equals(cis) false
Pick an answer to see if you are right.
Item 11: Always override hashCode with equals
Equal objects → equal hash codes. Skip hashCode and map.put(new PhoneNumber(...), "Jenny") then get(new PhoneNumber(...)) returns null.
Never return 42 — legal but turns the table into one list. Recipe: result = hash(c1); then for each field result = 31 * result + c; using Type.hashCode, recursive hashCode, Arrays.hashCode, 0 for null. Objects.hash(...) is one-liner, slower (varargs/boxing). Cache lazily only if immutable and expensive. Do not drop significant fields for “speed.” Leave the exact formula unspecified when you can.
Item 12: Always override toString
Default PhoneNumber@163b91 is useless in logs. Prefer something like 707-867-5309. Collections print element toStrings. Document whether the format is a contract (then provide parse/from) or unspecified. Always expose accessors so clients are not forced to parse. Skip on static utilities and most enums; do override on abstract collection-style bases.
1 → 31*1+2=33 → 31*33+3=1026. Starter 0 fails.
Item 13: Override clone judiciously
Cloneable is a broken mixin: no clone method; it only changes Object.clone behavior. Prefer copy constructors / copy factories (new TreeSet<>(s)). If you must: public covariant clone(), super.clone(), then fix mutable deep structure (array .clone(), deep-copy lists iteratively). final mutable fields fight clone. Never call overridable methods on the half-built clone. Arrays are the one place clone shines.
Item 14: Consider Comparable
Natural order unlocks Arrays.sort, TreeSet, etc. Contract: antisymmetry of sign, transitivity, and compareTo==0 implies same comparisons to z. Consistent with equals is strongly recommended (BigDecimal is not — HashSet vs TreeSet surprise).
Use Type.compare / Comparator.comparingInt(...).thenComparingInt(...) — not hashCode() - hashCode() (overflow). Same “no value field on concrete superclass” issue as equals → compose.
clone and compareTo
1 / 2Copying a HashSet as a TreeSet
Pick an answer to see if you are right.
Chapter 3 map
| Item | Action |
|---|---|
| 10 | equals only for values; keep the five laws; compose don’t extend+field |
| 11 | hashCode whenever equals |
| 12 | human toString (+ accessors) |
| 13 | prefer copy ctor/factory over Cloneable |
| 14 | Comparable for natural order; consistent with equals if you can |