equals() and hashCode() in Java
Two objects that are equal must have the same hash code. Breaking that contract quietly breaks every hash based collection.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Two kinds of equality
== | equals | |
|---|---|---|
| Compares | References, or primitive values | Whatever the class defines |
| Overridable | No | Yes |
| Default behaviour | Identity | Identity, inherited from Object |
Identity asks "is this the same object". Equality asks "do these represent the same value". For a Money or a Point, the second question is the useful one.
The equals contract
For any non null references, equals must be:
- Reflexive -
a.equals(a)is true. - Symmetric - if
a.equals(b)thenb.equals(a). - Transitive - if
a.equals(b)andb.equals(c)thena.equals(c). - Consistent - repeated calls give the same answer while the objects are unchanged.
- Null safe -
a.equals(null)is false, never an exception.
The hashCode contract
- Equal objects must return the same hash code.
- Unequal objects may return the same hash code, which is a collision and is allowed.
- The value must not change while the object is in a hash based collection.
Only the first rule is enforced by consequences rather than by the compiler. Break it and a HashMap will store an entry it can never find again.Writing them correctly
import java.util.Objects;
public final class Money {
private final String currency;
private final long minorUnits;
public Money(String currency, long minorUnits) {
this.currency = Objects.requireNonNull(currency);
this.minorUnits = minorUnits;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true; // fast path
}
if (!(other instanceof Money money)) {
return false; // also handles null
}
return minorUnits == money.minorUnits
&& currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(currency, minorUnits);
}
@Override
public String toString() {
return currency + " " + minorUnits;
}
}Use exactly the same fields in both methods. That is the simplest way to keep the contract true.
What goes wrong without hashCode
// Money overrides equals but NOT hashCode
Set<Money> prices = new HashSet<>();
prices.add(new Money("INR", 500));
System.out.println(prices.contains(new Money("INR", 500))); // falseA HashSet first uses the hash code to find a bucket. The two objects produce different hashes, land in different buckets, and equals is never even consulted. The entry is present but unreachable.
bucket 12 -> [ Money INR 500 ] stored here from hash 12
lookup with a new object -> hash 87 -> bucket 87 is empty -> not foundMutable keys are a trap
class MutableKey {
String name; // used by equals and hashCode
}
MutableKey key = new MutableKey();
key.name = "first";
Map<MutableKey, String> map = new HashMap<>();
map.put(key, "value");
key.name = "second"; // the hash changed
System.out.println(map.get(key)); // null - it is in the old bucketThis is why String, Integer and LocalDate are good keys and mutable objects are not.
Records do it for you
public record Money(String currency, long minorUnits) { }A record generates equals, hashCode and toString from all its components, and they are consistent by construction. For a pure value type this is the best option available.
Inheritance and symmetry
class Point {
int x, y;
@Override public boolean equals(Object o) {
return o instanceof Point p && p.x == x && p.y == y;
}
}
class ColouredPoint extends Point {
String colour;
@Override public boolean equals(Object o) {
return o instanceof ColouredPoint c && super.equals(o) && c.colour.equals(colour);
}
}
Point p = new Point();
ColouredPoint c = new ColouredPoint();
// p.equals(c) can be true while c.equals(p) is false -> symmetry is brokenThere is no fully satisfactory way to extend a value class and keep the contract. The practical answers are to make value classes final, or to use composition instead of inheritance.
Common mistakes
- Overriding
equalswithouthashCode. - Declaring
equals(MyType other), which overloads and never overrides. - Using mutable fields in
equalsand then mutating them while the object is a key. - Comparing floating point fields directly rather than with
Double.compare. - Using
getClass() != o.getClass()orinstanceofwithout understanding the inheritance trade off. - Returning a constant from
hashCode. It is technically legal and turns a map into a linked list.
Best practices
- Use a record for value types and let the compiler generate all three methods.
- Otherwise use
Objects.equalsfor fields andObjects.hashfor the hash. - Use the same, immutable fields in both methods.
- Start
equalswith thethis == otherfast path. - Make value classes
final.
Practice
- Write a
Bookclass with ISBN based equality and confirm aHashSetrejects a duplicate. - Remove
hashCodefrom it and explain the exact step at which lookup fails. - Why is
a.equals(null)required to return false rather than throw? - Show how mutating a key field after insertion loses the entry.
- Convert the class to a record and list what the compiler now guarantees.
Conclusion
Override the pair together, build both from the same immutable fields, and prefer records when the class is a value. The contract is simple, and hash based collections depend on it completely.