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.
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 wheny.equals(x).- transitive
x = yandy = zmeansx = 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:
- ✓reflexive
- ✗symmetric
- ✓transitive
- ✓keeps the year
- class RareCoin extends Coin {
- final int year;
- public boolean equals(Object o) {
- return o instanceof RareCoin r
- && r.cents == cents
- && r.year == year;
- }
- }
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.
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:
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)
}
}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.
an empty HashSet: 8 buckets. Key overrides equals (same name = equal) but not hashCode.
equals never gets a chance to say they're equal.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")));
}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).
Coin@1b6d3586Coin[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":
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
}remix true
Playlist b = a.clone();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.
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:
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))));
}[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.
- 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.compareorComparator.comparing, never by subtracting. - records write equals, hashCode and toString for you. use them for data.