← Back to list

Why Do We Need to Override equals() in Java? (equals-hashCode part 1)

Photo by Volodymyr Dobrovolskyy on Unsplash

Firas Ahmed in Javarevisited · 2025-12-17 14:20 · 13 claps · 5.1 min read paywalled
#java #programming #java-programming #object-oriented #equals-method-java
Open on Medium ↗
Wiki topics: 💻 · Programming

Why Do We Need to Override equals() in Java? (equals-hashCode part 1)

Photo by Volodymyr Dobrovolskyy on Unsplash

Photo by Volodymyr Dobrovolskyy on Unsplash

In this article, we’ll discuss the implementation equals() method in Java. Key points we’ll dive into:

  • How the default equals() behaves and why it should be overridden.
  • Type-checking with equals() .
  • The equals() contract.
  • Tips when overriding equals() .

The equals() method

The equals() method is used compare two objects to determine if they’re equal. This method is provided by default in Java’s Object class:

public boolean equals(Object obj) {
  return (this == obj);
}

Why does the default equals() method of Object return false when two objects have the same value?

When comparing objects using the predefined equals() in the Object class, the method returns false.

If you look at the method’s implementation given above, it simply compares references of the objects. It does not take into consideration what’s inside the objects — it only checks if both references point to the same object in memory. Therefore, it behaves the same as comparison by == operator.

The result is the same for any object comparison since every class in Java implicitly extends the Object class:

public class Device {

    private String name;

    public Device(String name) {
        this.name = name;
    }
}

public class Main {

    public static void main(String[] args) {
        Device laptop = new Device("Apple");
        Device phone = new Device("Apple");

        System.out.println(laptop.equals(phone)); // false
    }
}

Even though laptop and phone have the same value of name, they’re pointing to two different objects.

When does it return true?

It can return true if both references point to the same object:

public class Main {
    public static void main(String[] args) {
        Device phone = new Device("Apple");
        Device smartPhone = phone;
        System.out.println(phone.equals(smartPhone)); // true
    }
}

But why does the default implementation of equals() only compare references?

This should be obvious, because at Object’s level — that is, the root Object — Java does not know what makes objects “equal”. It has no idea what an object contains. It only knows that each object has a memory address, so it checks if two references point to the same memory location. (this == obj). This is the only equality that the Objectclass can guarantee. Anything more specific must be defined by the class itself by overriding the equals() method.

Overriding equals()

To compare the contents of objects, the equals()method has to be overridden:

public class Device {

    private String name;

    public Device(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;  // compares references

        if (o == null || getClass() != o.getClass()) return false;

        Device device = (Device) o;
            return Objects.equals(name, device.name); // compares contents
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(name);
        }
}

In our implementation, we’re first comparing by references, then we’re performing type checkingif (o == null || getClass() != o.getClass() return false;— before finally comparing the contents of laptopand phone, which results in true.

Notice that we’ve also overridden hashCode() method. We’ll discuss why we need it in a future article.

What’s the purpose of type-checking in equals()?

Type checking in equals() simply refers to a way to ensure both compared objects are of the same type, because the input o can be any object, so we need to make sure we’re passing the object of the right type, or else a ClassCastException could occur.

This is achieved by checking what type o actually is using either:

  • getClass():
if (o == null || getClass() != o.getClass() return false;

This method returns true if both objects are of the exact same class. This is a strict check and more common. Subclasses are not allowed.

  • instanceof :
if (!(o instanceof Device)) return false;

It returns true if the object is of the current class or any of its subclasses. It’s more flexible but can cause problems if subclasses override equals(), as this can easily break the symmetry rule of the equals() contract.

The symmetry rule is one of the key rules of equals() contract defined by the Object class. It essentially states that equality must be mutual. — if A is equals to B, then B should be equal A. — This rule is often violated when comparing a superclass to a subclass that introduces additional fields. To solve this we can simply make equals() method final to ensure that no subclass overrides it and violates the symmetry rule.

String class is an example of a class that has its own implementation of the equals() method.

The equals() contract

Let’s dive deeper into important aspects of equals() contract. The contract specified in Java for this method essentially states that the equals() method must satisfy the following rules:

  • Reflexive: x.equals(x) must return true.
  • Symmetric: if x.equals(y) is true then y.equals(x) must also be true.
  • Transitive: for x, y and z, if x.equals(y) and y.equals(z) is true, then x.equals(z) must also be true.
  • Consistent: multiple invocations of x.equals(y) must return the same result, unless object data changes.
  • non-null: x.equals(null) must always return false.

Common Mistake: Missing Null Check in equals()

A common mistake when implementing equals() method is forgetting to handle null:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    Device device = (Device) o; // ❌ Throws NullPointerException if o is null
    return Objects.equals(name, device.name);
}

This causes a NullPointerException when equals() is called with a null argument:

public static void main(String[] args) {
    Device phone = new Device("Apple");
    Device anotherDevice = null;

    System.out.println(phone.equals(anotherDevice)); // throws NullPointerException
}

Fix: Add a null and type check before casting:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false; // this prevents NPE
    Device device = (Device) o;
    return Objects.equals(name, device.name);
}

By checking o == null, the method safely returns false instead of throwing an exception.

Tip:

When to use Objects.equals(a, b) instead of a.equals(b):

  • Use Objects.equals(a, b) when either side can be null, because it handles NullPointerException.
  • Use a.equals(b) only when you’re certain that a is not null.
Device a = null;
Device b = new Device();

System.out.println(Objects.equals(a, b)); // false
System.out.println(a.equals(b));   // NullPointerException

Tip:

When Objects.equals() Can Still Throw a NullPointerException:

Objects.equals() does not throw a NullPointerExceptionon its own, but it may propagate one thrown by the overridden equals() method of the first non-null argument does.

But if Objects.equals() belongs to the Objects class, why does our custom equals() in our class affects its null safety?

If you look at Objects.equals() implementation:

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

Notice that it’s internally invoking a.equals(b) when a is not null. So if a is an instance of our class Device then it’s actually calling our overridden equals() method. Therefore if our equals() implementation does not handle null safely, then Objects.equals(a, b)will throw an NPE.

In short, Objects.equals() is only as safe as our equals() implementation — it can still throw an exception if that implementation itself doesn’t handle null safely.

In summary, the default equals() only compares object references, not their internal state, which is why two distinct objects with identical values are considered unequal by default. To ensure logical equality, classes must override equals() and explicitly specify what makes two instances equal.

When overriding equals() , it’s important to:

  • perform proper reference, null and type checks.
  • follow the equals() contract to ensure predictability and correct behavior.
  • be careful with type-checking strategies (getClass()vs. instanceof) to prevent contract violations.

In a future article, we’ll look into the contract between equals() and hashCode() as it’s closely related to the topic of this article.

Thanks for reading!


메타데이터
post_id
cfb7fa180b5a
slug
why-do-we-need-to-override-equals-in-java-equals-hashcode-part-1-cfb7fa180b5a
url
https://medium.com/javarevisited/why-do-we-need-to-override-equals-in-java-equals-hashcode-part-1-cfb7fa180b5a
canonical_url
https://medium.com/javarevisited/why-do-we-need-to-override-equals-in-java-equals-hashcode-part-1-cfb7fa180b5a
author_url
https://medium.com/@ferasama
status
ok
fetched_at
2026-06-20 20:29:01