equals and hashCode are the two methods every Java developer overrides eventually and half of them override wrong. Get them right and your objects behave predictably as map keys and set members; get them wrong and objects vanish from HashSets, duplicate silently, or become impossible to look up. This is one of the most common real-world bugs and one of the most common interview questions.
This tutorial explains why the two methods are joined at the hip, walks through correct implementations, and shows exactly how a HashMap uses them. It builds on how HashMap works internally, so keep that open as a companion.
What each method means
Both methods live on java.lang.Object, so every class you write inherits them.
equals(Object o)answers "are these two objects logically the same?"hashCode()returns anintthat hash-based collections use to decide which bucket an object goes into.
The default equals inherited from Object compares identity — it returns true only if both references point to the exact same object. The default hashCode is derived from the object's memory address. For value-like classes (a Point, a Money, an Employee with an id) that default is almost never what you want.
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
public class Main {
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println(a.equals(b)); // false! — default identity comparison
}
}
a and b have identical coordinates but the default equals says they're different, because they're two separate objects in memory. To make them equal by value, we override equals.
Overriding equals correctly
A correct equals follows a standard shape: check identity, check type, then compare the fields that define equality.
class Point {
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; // same object
if (o == null || getClass() != o.getClass()) return false; // type check
Point other = (Point) o; // safe cast
return x == other.x && y == other.y; // field comparison
}
}
Now new Point(1, 2).equals(new Point(1, 2)) returns true. The equals contract requires four properties, and interviewers do ask you to name them:
- Reflexive:
x.equals(x)is alwaystrue. - Symmetric: if
x.equals(y)theny.equals(x). - Transitive: if
x.equals(y)andy.equals(z)thenx.equals(z). - Consistent: repeated calls return the same result if the objects don't change.
Plus, x.equals(null) must always be false — which our null check guarantees.
Why hashCode must come along
Here's the crux. If you override equals but not hashCode, you've created objects that are "equal" but return different hash codes — and that quietly breaks every hash-based collection.
import java.util.HashSet;
public class Broken {
public static void main(String[] args) {
HashSet<Point> set = new HashSet<>();
set.add(new Point(1, 2));
System.out.println(set.contains(new Point(1, 2)));
// prints FALSE even though equals says they're equal!
}
}
Why does contains fail? A HashSet first calls hashCode to find the right bucket, then uses equals only on objects in that bucket. Since the two Points have different default hash codes, they map to different buckets, and equals is never even called. The object is effectively lost.
Common mistake: Overriding
equalsand forgettinghashCodeis the number-one hash-collection bug. Your object goes into aHashMaporHashSetfine but can never be found again, or duplicates slip in. If you override one, override both — always.
The contract, stated precisely
The relationship between the two methods is a formal contract:
- If two objects are equal (
equalsreturnstrue), they MUST return the samehashCode. This is the rule that breaks lookups when violated. - If two objects have the same
hashCode, they are NOT required to be equal. Two different objects sharing a hash code is a collision — legal and expected.
Read rule 2 carefully: hashCode is allowed to be imperfect. A hash code just narrows the search to a bucket; equals does the final, exact comparison. Collisions only cost a little performance, never correctness — the full mechanics are in how HashMap works internally.
Implementing hashCode correctly
The modern, readable way uses Objects.hash, which combines your fields into one hash code. Use the same fields you compared in equals.
import java.util.Objects;
class Point {
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;
if (o == null || getClass() != o.getClass()) return false;
Point other = (Point) o;
return x == other.x && y == other.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y); // same fields as equals
}
}
Now the earlier HashSet.contains(new Point(1, 2)) returns true, because both Points hash to the same bucket and equals confirms they match. Objects.equals(a, b) is also handy inside equals for comparing object-typed fields safely, because it handles null for you.
Pro tip: The golden rule is "equals and hashCode must use the same fields." If
equalscomparesxandy,hashCodemust be built fromxandy. Using different fields for each is a subtle way to violate the contract even when both methods exist.
How HashMap uses them together
Putting it end to end, when you do map.put(key, value):
- Java calls
key.hashCode()to compute the bucket index. - If that bucket already holds entries, it walks them and calls
equalsto check for a matching key. - A match updates the existing value; no match adds a new entry.
map.get(key) does the same two-step: hash to the bucket, then equals within it. This is why a proper pair of methods is mandatory for any object you use as a HashMap key or a HashSet element. Mutable keys are dangerous for the same reason — if you change a field after insertion, the object's hash code changes and it's stranded in the wrong bucket.
Interview note: "What happens if you use a mutable object as a HashMap key and then mutate it?" The answer: its hashCode changes, so the map looks in the wrong bucket and can no longer find the entry — it's effectively lost. This is why immutable keys like
StringandIntegerare ideal. Expect this and the "override equals only" trap on the Java collections interview questions.
A quick checklist
Before you ship a class used in collections, confirm:
- Overrode
equals? Then overridehashCode. - Both use the same set of fields.
equalsis reflexive, symmetric, transitive, consistent, and false fornull.- Keys used in maps/sets are ideally immutable so their hash code never changes.
Modern IDEs generate a correct pair for you, and records (Java 16+) generate them automatically from the components — but you must understand the contract to trust the generated code and to explain it in an interview.
Where to go next
This contract is the bridge between core OOP and the collections framework, so review the four OOP concepts if identity vs equality still feels abstract, then dive into how HashMap works internally to see the buckets and collisions in action.
Implementing and debugging these methods on real domain objects — and explaining them under interview pressure — is exactly the kind of practice built into the collections module of the Java Full Stack course, with the full sequence mapped in the Java learning hub.
Frequently Asked Questions
Why must you override hashCode when you override equals?
What is the contract between equals and hashCode?
What happens if two objects have the same hashCode but are not equal?
Can I use Objects.equals and Objects.hash to implement them?
Does the default equals from Object compare field values?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

