Java equals() Method

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

false

Java 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 == is false, and the default equals follows 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 default equals.
  • 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:

  • @Override tells the compiler β€œI am replacing a parent method.” A wrong name fails to compile.
  • The parameter type must be Object o, not IdCard 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; rejects null and a different type.
  • IdCard other = (IdCard) o; casts back to IdCard, safe after the type check.
  • Objects.equals(this.number, other.number) compares the data and handles null without 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

true

Same 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 be true. An object always equals itself.
  • Symmetric. If x.equals(y) is true, then y.equals(x) must be true too.
  • Transitive. If x.equals(y) and y.equals(z) are both true, then x.equals(z) must be true.
  • 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 return false, 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 == two checks identity. Two separate new Book(...) calls, so false.
  • one.equals(two) compares title and author. Both match, so true.
  • one.equals(three) has a different author, so false.

Output

false
true
false

== 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 HashSet uses hashCode to pick a bucket, then uses equals inside it.
  • Equal objects with different hash codes land in different buckets.
  • The set then never calls equals to 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

2
false
  • 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 of equals to compare objects. This compares memory addresses, not contents.
if (cardA == cardB) { ... } // ❌ true only for the same object
if (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 real equals from Object still runs.
public boolean equals(IdCard o) { ... } // ❌ overloads, does not override
@Override
public boolean equals(Object o) { ... } // βœ… correct signature, real override

The @Override annotation protects you here, because it refuses to compile if the signature is wrong.

  • Breaking symmetry. Using instanceof carelessly with subclasses, or comparing types in only one direction, can make x.equals(y) differ from y.equals(x).
if (!(o instanceof IdCard)) return false; // ⚠️ can break symmetry across subclasses
if (o == null || getClass() != o.getClass()) // βœ… symmetric: both sides must be the same class
return false;
  • Forgetting hashCode. As shown above, overriding equals alone quietly breaks HashSet and HashMap.
// ❌ 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 returning false.

βœ… Best Practices

Good habits that keep equals correct and easy to read.

  • Always use equals for objects, == for primitives. Make this automatic so the address-vs-contents bug never appears.
  • Keep the parameter type Object and add @Override. This guarantees you are overriding, not accidentally overloading.
  • Follow the standard pattern. Check this == o, then null and type with getClass(), then cast, then compare fields.
  • Use Objects.equals for object fields and == for primitives. Objects.equals handles null safely, while == is correct and fast for int, boolean, and the rest.
  • Always override hashCode alongside equals. 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 Object is 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, then null/type check, then cast, then compare fields.
  • βœ… The equals contract requires it to be reflexive, symmetric, transitive, consistent, and null-safe.
  • βœ… Use Objects.equals for object fields and == for primitive fields inside the method.
  • βœ… If you override equals, you must also override hashCode, or HashSet and HashMap break.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does the default equals from Object compare?

    Why: The inherited equals is just this == o, so it compares references, not contents.

  2. 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. 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. 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.

Java hashCode() Method

Share & Connect