Java equals() Method
Table of Contents + β
In the last lesson you learned about the Java toString() method. This lesson asks a different question: when are two objects βthe sameβ? The equals method is how Java decides, and its default surprises almost everyone. Letβs learn it.
π€ The problem: two βsameβ objects that are not equal
A small class holds a personβs ID card. Two cards with the same number should be the same person, right? Letβs test that.
class IdCard { String number;
IdCard(String number) { this.number = number; }}
public class Main { public static void main(String[] args) { IdCard a = new IdCard("A-100"); IdCard b = new IdCard("A-100");
System.out.println(a.equals(b)); // what do you expect? }}Both cards hold the same number, A-100. Most people expect true.
Output
falseJava says they are not equal, even though their contents match. To fix it, first see what equals does by default.
π§© The default equals compares references, not contents
Your class gets a free equals from Object. The default equals compares references. It is literally just ==:
public boolean equals(Object obj) { return (this == obj); // β for most classes, this is not what you want}==on objects asks βthe exact same object?β, not βthe same data?β- Two separate
new IdCard("A-100")calls build two objects in two memory spots. - So
==isfalse, and the defaultequalsfollows along.
Picture two printed copies of the same document. Same words, but still two sheets of paper. The default equals checks βthe same sheet?β, not βthe same words?β
π Reference equality vs logical equality
Naming the two ideas of βequalβ clears up most confusion.
- Reference equality asks βthe same object in memory?β That is
==and the defaultequals. - Logical equality asks βthe same meaning?β That is what we usually want, like two cards with the same number.
- For primitives,
==is perfect. It compares actual values. - For objects,
==compares memory addresses, which is rarely what we mean.
To get logical equality for our own classes, we write our own equals.
== on objects is a classic bug
For primitives like int, double, and char, == compares values and is correct. For objects, == compares memory addresses, which is almost never what you want. When comparing objects, reach for equals, not ==.
π οΈ Overriding equals to compare contents
To make two ID cards with the same number count as equal, we override equals to compare the fields we care about. Read the code first, then the walkthrough.
import java.util.Objects;
class IdCard { String number;
IdCard(String number) { this.number = number; }
@Override public boolean equals(Object o) { if (this == o) return true; // same object, quick yes if (o == null || getClass() != o.getClass()) return false; // null or different type IdCard other = (IdCard) o; // safe to cast now return Objects.equals(this.number, other.number); // compare the field }}Now the walkthrough, line by line:
@Overridetells the compiler βI am replacing a parent method.β A wrong name fails to compile.- The parameter type must be
Object o, notIdCard o. That is the exact shape we override. if (this == o) return true;is a quick shortcut for the same object.if (o == null || getClass() != o.getClass()) return false;rejectsnulland a different type.IdCard other = (IdCard) o;casts back toIdCard, safe after the type check.Objects.equals(this.number, other.number)compares the data and handlesnullwithout crashing.
Run the same test with this new equals in place.
public class Main { public static void main(String[] args) { IdCard a = new IdCard("A-100"); IdCard b = new IdCard("A-100");
System.out.println(a.equals(b)); // now compares the number field }}Output
trueSame number, so equals now says true. We only taught the class how to judge equality.
π The equals contract
When you override equals, Java trusts you to follow the equals contract. Collections like HashSet and HashMap rely on it, so breaking it makes objects go missing or show up twice. For non-null objects x, y, and z:
- Reflexive.
x.equals(x)must betrue. An object always equals itself. - Symmetric. If
x.equals(y)istrue, theny.equals(x)must betruetoo. - Transitive. If
x.equals(y)andy.equals(z)are bothtrue, thenx.equals(z)must betrue. - Consistent. Calling
x.equals(y)many times gives the same answer, as long as the objects do not change. - Null-safe.
x.equals(null)must returnfalse, never throw an error.
The standard pattern above satisfies all of these for free. The this == o check covers reflexive, the getClass() check keeps it symmetric and transitive, and the o == null check makes it null-safe.
π§ͺ A worked example: before and after
One runnable program with a Book (title and author). We compare two equal books, then one with a different author:
import java.util.Objects;
class Book { String title; String author;
Book(String title, String author) { this.title = title; this.author = author; }
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Book other = (Book) o; return Objects.equals(this.title, other.title) && Objects.equals(this.author, other.author); }}
public class Main { public static void main(String[] args) { Book one = new Book("Clean Code", "Alex"); Book two = new Book("Clean Code", "Alex"); Book three = new Book("Clean Code", "Riya");
System.out.println(one == two); // reference check System.out.println(one.equals(two)); // content check System.out.println(one.equals(three)); // different author }}Read the comparisons before the output.
one == twochecks identity. Two separatenew Book(...)calls, sofalse.one.equals(two)compares title and author. Both match, sotrue.one.equals(three)has a different author, sofalse.
Output
falsetruefalse== only cares about identity. The overridden equals cares about contents.
π Override equals, and you must override hashCode too
If you override equals, you must also override hashCode. These two are a pair, and HashSet and HashMap use both together.
- A
HashSetuseshashCodeto pick a bucket, then usesequalsinside it. - Equal objects with different hash codes land in different buckets.
- The set then never calls
equalsto notice they match.
This Point overrides equals but forgets hashCode:
import java.util.HashSet;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 this.x == other.x && this.y == other.y; } // β hashCode NOT overridden}
public class Main { public static void main(String[] args) { HashSet<Point> set = new HashSet<>(); set.add(new Point(1, 2)); set.add(new Point(1, 2)); // logically a duplicate
System.out.println(set.size()); // how many got stored? System.out.println(set.contains(new Point(1, 2))); // can we find it? }}A correct set should hold one point and find it. Without hashCode, here is the result.
Output
2false- The set stored both βequalβ points as if they were different.
- Then it could not even find a matching point.
- That is the classic symptom of forgetting
hashCode.
We fix this fully in the next lesson. Override one, override both.
They travel as a pair
Whenever you write an equals method, add a hashCode method right next to it. Most editors can generate both at once. Treat them as one task, never two.
β οΈ Common Mistakes
These slip-ups show up constantly.
- Using
==instead ofequalsto compare objects. This compares memory addresses, not contents.
if (cardA == cardB) { ... } // β true only for the same objectif (cardA.equals(cardB)) { ... } // β
compares the contents- Wrong parameter type. The method must take
Object, not your own class. With the wrong type you create a brand new overloaded method, and the realequalsfromObjectstill runs.
public boolean equals(IdCard o) { ... } // β overloads, does not override@Overridepublic boolean equals(Object o) { ... } // β
correct signature, real overrideThe @Override annotation protects you here, because it refuses to compile if the signature is wrong.
- Breaking symmetry. Using
instanceofcarelessly with subclasses, or comparing types in only one direction, can makex.equals(y)differ fromy.equals(x).
if (!(o instanceof IdCard)) return false; // β οΈ can break symmetry across subclassesif (o == null || getClass() != o.getClass()) // β
symmetric: both sides must be the same class return false;- Forgetting hashCode. As shown above, overriding
equalsalone quietly breaksHashSetandHashMap.
// β equals overridden, hashCode missing -> duplicates and lost lookups// β
override hashCode too (next lesson)- Forgetting the null check. Without it,
equals(null)can crash with a NullPointerException instead of returningfalse.
β Best Practices
Good habits that keep equals correct and easy to read.
- Always use
equalsfor objects,==for primitives. Make this automatic so the address-vs-contents bug never appears. - Keep the parameter type
Objectand add@Override. This guarantees you are overriding, not accidentally overloading. - Follow the standard pattern. Check
this == o, thennulland type withgetClass(), then cast, then compare fields. - Use
Objects.equalsfor object fields and==for primitives.Objects.equalshandlesnullsafely, while==is correct and fast forint,boolean, and the rest. - Always override
hashCodealongsideequals. They are a pair, and collections need both to work. - Compare only the fields that define identity. Skip fields that should not affect whether two objects are βthe same.β
π§© What Youβve Learned
Nicely done. Letβs recap the equals method.
- β
The default equals from
Objectis just==, so it compares references, not contents. - β
==on objects checks memory addresses; on primitives it correctly checks values. - β
Override equals to compare contents, using the pattern:
this == o, thennull/type check, then cast, then compare fields. - β The equals contract requires it to be reflexive, symmetric, transitive, consistent, and null-safe.
- β
Use
Objects.equalsfor object fields and==for primitive fields inside the method. - β
If you override
equals, you must also overridehashCode, orHashSetandHashMapbreak.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
What does the default equals from Object compare?
Why: The inherited equals is just this == o, so it compares references, not contents.
- 2
Why is == usually wrong for comparing objects?
Why: For objects, == checks whether both variables point to the same object, not whether their data matches.
- 3
What must the parameter type of a correct equals override be?
Why: The signature must be equals(Object o); using your own class creates an overload, not an override.
- 4
If you override equals, what else must you override?
Why: equals and hashCode are a pair; collections like HashSet and HashMap need both to behave correctly.
π Whatβs Next?
You now know how to make two objects equal by their contents. But we saw that equals alone is not enough. The moment your objects go into a HashSet or HashMap, you also need hashCode. Letβs learn what hashCode is and how to write it so it agrees with your equals.