skip to content
everything.effectiveJava.methodsCommonToAllObjects

methods common to all objects

items 10–14: equals, hashCode, toString, clone, compareTo.

chapter
chapter 16.02
reading time
8 min read

every class inherits equals, hashCode and toString from Object, and every collection trusts them. override equals and you've signed a contract (five rules); break it and sets and lists quietly lie to you. override hashCode with it, always. and since java 16, a record writes all three correctly for you.

items 10 to 14. amber marks what modern java changed, mostly thanks to records.

equals and hashCode

item 10 · equals has a contract, keep it

don't override equals at all if identity is what you mean (threads, streams, most services). override it for value-like classes, and then it must be:

reflexive
x.equals(x) is true.
symmetric
x.equals(y) exactly when y.equals(x).
transitive
x = y and y = z means x = z.
consistent
same answer every time, unless the objects change.
non-null
x.equals(null) is false, never an exception.

the classic way to break it: extend a class that has equals and add a field that should matter. there's no way to write the subclass's equals that keeps every rule. try them all:

RareCoin.equals =
row.equals(column)
abc
a truetruetrue
b falsetruefalse
c falsefalsetrue
  • ✓reflexive
  • ✗symmetric
  • ✓transitive
  • ✓keeps the year
RareCoin.java
  1. class RareCoin extends Coin {
  2. final int year;
  3. public boolean equals(Object o) {
  4. return o instanceof RareCoin r
  5. && r.cents == cents
  6. && r.year == year;
  7. }
  8. }

symmetry breaks. the plain coin says it equals the rare one, the rare one says no. a List or Set will disagree with itself depending on which side calls equals.

pick a strategy. the table is row.equals(column), computed live. red cells break a rule. only composition survives.

the "strict" version even breaks collections, depending on which side of the call your object is on:

Main.java
class Coin {
    final int cents;
    Coin(int cents) { this.cents = cents; }
    @Override public boolean equals(Object o) {
        return o instanceof Coin c && c.cents == cents;
    }
    @Override public int hashCode() { return Integer.hashCode(cents); }
}
 
class RareCoin extends Coin {
    final int year;
    RareCoin(int cents, int year) { super(cents); this.year = year; }
    @Override public boolean equals(Object o) {
        return o instanceof RareCoin r && r.cents == cents && r.year == year;
    }
}
 
public class Main {
    public static void main(String[] args) {
        Coin plain = new Coin(50);
        Coin rare = new RareCoin(50, 1933);
        System.out.println(java.util.List.of(rare).contains(plain));  // plain.equals(rare)
        System.out.println(java.util.List.of(plain).contains(rare));  // rare.equals(plain)
    }
}
output
true
false

the recipe for a hand-written equals: check the type with a pattern, compare the significant fields (== for primitives except float/double, which use Float.compare/Double.compare; Objects.equals for references that can be null), and make the class final if you can.

item 11 · equals and hashCode travel together

the rule: equal objects must have equal hash codes. forget to override hashCode and you keep Object's version, a per-object number that knows nothing about your fields. hash-based collections look in the bucket the hash points to, and only then call equals.

Key.hashCode =
k1 = Key("ada")add(k1)k2 = Key("ada")contains(k2)table[8]0·1·2·3·4·5·6·7·

an empty HashSet: 8 buckets. Key overrides equals (same name = equal) but not hashCode.

step 1/4
step through both versions. with Object's hashCode two equal keys land in different buckets, and equals never gets a chance to say they're equal.
Main.java
class Key {
    final String name;
    Key(String name) { this.name = name; }
    @Override public boolean equals(Object o) {
        return o instanceof Key k && k.name.equals(name);
    }
    // no hashCode: inherits Object's
}
 
void main() {
    var set = new HashSet<Key>();
    set.add(new Key("ada"));
    IO.println(new Key("ada").equals(new Key("ada")));
    IO.println(set.contains(new Key("ada")));
}
output
true
false

a good hashCode mixes every field equals uses, the usual way being 31 * result + field.hashCode(). Objects.hash(a, b, c) is a one-liner, but it boxes and allocates an array on every call, so keep it out of hot paths. and never leave a field out of the hash to "make it faster": you'll pile everything into a few buckets. why buckets work at all is in HashMap.

item 12 · give every class a useful toString

Object.toString() prints the class name, an @, and the hash in hex. nobody wants that in a log line or a test failure. print the fields that matter, and decide whether the format is a promise (if you document it, people will parse it).

don'tthe default
text
Coin@1b6d3586
dowhat you'd want in a log
text
Coin[cents=50]

copying and comparing

item 13 · stay away from clone

Cloneable is an odd interface: it has no methods, it changes how a protected method in Object behaves, clone() throws a checked exception you must swallow, and the copy is shallow. mutable fields like arrays end up shared between the "copies":

Main.java
class Playlist implements Cloneable {
    String[] songs = {"intro", "outro"};
 
    @Override public Playlist clone() {
        try {
            return (Playlist) super.clone(); // copies the fields, not the array
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }
}
 
void main() {
    Playlist a = new Playlist();
    Playlist b = a.clone();
    b.songs[0] = "remix";
    IO.println(a.songs[0]);         // a changed too
    IO.println(a.songs == b.songs); // one array, two playlists
}
output
remix
true
don'timplements Cloneable
java
Playlist b = a.clone();
doa copy constructor or factory
java
Playlist(Playlist other) {
    songs = other.songs.clone();
}
static Playlist copyOf(Playlist p) {
    return new Playlist(p);
}

a copy constructor can take an interface type (new ArrayList<>(anyCollection)), has no checked exception, and works with final fields. the one place clone() is fine: arrays. arr.clone() is the fastest way to copy one.

item 14 · natural orders: implement Comparable

if a class has an obvious order (numbers, dates, versions), implement Comparable<T> so it works with sort, TreeSet, TreeMap, Collections.max and friends. the contract mirrors equals: sign-symmetric, transitive, and ideally compareTo == 0 exactly when equals is true.

predict the output
java
List<Integer> xs = new ArrayList<>(List.of(3, -2_000_000_000, 2_000_000_000, 1));
xs.sort((a, b) -> a - b);
IO.println(xs);

the modern way to write the order is a static comparator built from key extractors, read top to bottom like a sentence:

Main.java
record Version(int major, int minor) implements Comparable<Version> {
    private static final Comparator<Version> ORDER =
        Comparator.comparingInt(Version::major).thenComparingInt(Version::minor);
 
    @Override public int compareTo(Version other) { return ORDER.compare(this, other); }
}
 
void main() {
    IO.println(new TreeSet<>(List.of(new Version(1, 10), new Version(1, 2), new Version(0, 9))));
}
output
[Version[major=0, minor=9], Version[major=1, minor=2], Version[major=1, minor=10]]

see? "1.10" sorts after "1.2", which string comparison would get wrong. more patterns (reversed(), nullsFirst, multi-key sorts) in sorting and comparators.

keylines
  • equals is a contract: reflexive, symmetric, transitive, consistent, false for null.
  • you can't extend a value class, add a field, and keep equals honest. compose instead.
  • override hashCode whenever you override equals, or hash collections can't find your keys.
  • clone is broken by design. copy constructors and factories, or just be immutable.
  • compare with Integer.compare or Comparator.comparing, never by subtracting.
  • records write equals, hashCode and toString for you. use them for data.