Skip to main content

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)​

RuleMeaning
Reflexivex.equals(x)
Symmetricx.equals(y) iff y.equals(x)
Transitivechain of equals implies first equals last
Consistentstable if state used by equals is unchanged
Non-nullx.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​

  1. if (o == this) return true;
  2. if (!(o instanceof MyType)) return false;
  3. Cast.
  4. Compare significant fields (== for most primitives; Float/Double.compare; recursive equals; Objects.equals for 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.

Quiz

equals contract

1 / 2

CaseInsensitiveString.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.

Quiz

clone and compareTo

1 / 2

Copying a HashSet as a TreeSet

Pick an answer to see if you are right.

Chapter 3 map​

ItemAction
10equals only for values; keep the five laws; compose don’t extend+field
11hashCode whenever equals
12human toString (+ accessors)
13prefer copy ctor/factory over Cloneable
14Comparable for natural order; consistent with equals if you can