Every class extends java.lang.Object. Its equals, hashCode and toString defaults are almost never what a value class wants — and equals and hashCode must be overridden together.
What Object gives every class
| Method | Default behaviour | Override? |
|---|---|---|
boolean equals(Object o) | Identity: this == o | Yes, for value classes |
int hashCode() | Identity-based number | Always together with equals |
String toString() | ClassName@1b6d3586 (class + hex hash) | Yes — for logs and debugging |
Class<?> getClass() | Runtime class | Final — cannot |
Object clone() | Shallow field copy (needs Cloneable) | Rarely — prefer copy constructors |
wait / notify / notifyAll | Monitor-based thread coordination | Final — cannot |
finalize() | Deprecated for removal (Java 9+) | Never |
The equals / hashCode contract
Reflexive:
x.equals(x)istrue.Symmetric:
x.equals(y)⇔y.equals(x).Transitive:
x.equals(y)andy.equals(z)⇒x.equals(z).Consistent: repeated calls give the same answer while the objects don't change.
x.equals(null)isfalse.Equal objects must have equal hash codes. (Unequal objects may share a hash code — that's a collision, which is legal.)
A correct implementation
import java.util.*;
public class Main {
public static void main(String[] args) {
Point a = new Point(1, 2), b = new Point(1, 2);
System.out.println(a == b); // different objects
System.out.println(a.equals(b)); // same value
System.out.println(a.hashCode() == b.hashCode());
System.out.println(a); // our toString
Set<Point> set = new HashSet<>(List.of(a));
System.out.println(set.contains(b));
}
}
final class Point {
private final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true; // fast path
if (!(o instanceof Point p)) return false; // also handles null
return x == p.x && y == p.y;
}
@Override
public int hashCode() { return Objects.hash(x, y); }
@Override
public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}false
true
true
Point[x=1, y=2]
trueWhat breaks when hashCode is missing
import java.util.*;
public class Main {
public static void main(String[] args) {
Set<Key> set = new HashSet<>();
set.add(new Key("id-7"));
System.out.println(new Key("id-7").equals(new Key("id-7")));
System.out.println(set.contains(new Key("id-7"))); // looks in the wrong bucket
}
}
class Key {
final String id;
Key(String id) { this.id = id; }
@Override public boolean equals(Object o) {
return o instanceof Key k && id.equals(k.id);
}
// hashCode NOT overridden → identity hash, different for each object
}true
falseHashSet/HashMap first use hashCode() to pick a bucket and only then call equals on the entries in that bucket. Two "equal" keys with different hash codes land in different buckets and are never compared.
getClass() vs instanceof in equals
| Check | Subclass instances | Trade-off |
|---|---|---|
o instanceof Point p | Can be equal to a Point | Liskov-friendly, but symmetry breaks if a subclass adds fields and overrides equals |
getClass() != o.getClass() | Never equal to a Point | Strictly symmetric, but a harmless subclass (or proxy) is never equal |
Common guidance: make value classes final (or use a record) and use instanceof — then the question doesn't arise.
Helpers in java.util.Objects
import java.util.Objects;
public class Main {
public static void main(String[] args) {
String a = null, b = "x";
System.out.println(Objects.equals(a, b)); // null-safe, no NPE
System.out.println(Objects.equals(null, null));
System.out.println(Objects.hash(1, "a") == Objects.hash(1, "a"));
System.out.println(Objects.hashCode(null)); // 0 for null
System.out.println(Objects.toString(a, "n/a")); // default for null
System.out.println(Objects.requireNonNullElse(a, "fallback"));
try {
Objects.requireNonNull(a, "name must not be null");
} catch (NullPointerException e) {
System.out.println(e.getMessage());
}
}
}false
true
true
0
n/a
fallback
name must not be nullclone() — briefly
Object.clone() makes a shallow copy and only works if the class implements the marker interface Cloneable (otherwise it throws CloneNotSupportedException). It bypasses constructors and is awkward with final fields. Arrays are the one place it's pleasant: int[] copy = arr.clone();.
Why must you override hashCode when you override equals?
Can two unequal objects have the same hashCode?
What does the default toString print?
What is wrong with public boolean equals(Point other)?