Equals, hashCode and toString

Basic Java
≈ 4 min read

Three methods that describe an object

The equals(), hashCode(), and toString() methods are defined in Object, so every object has them. But the behavior inherited from Object is often too general for our own classes, so these methods are commonly overridden.

Each method has its own job: equals() answers the question “should these two objects be considered equal?”, hashCode() provides a numeric hash consistent with that equality, and toString() creates a textual representation of the object.

equals(): equality by meaning

If a class does not override equals(), the implementation from Object is used. It considers two objects equal only when both references point to the very same object — essentially the same rule as the == operator uses for references.

Point first = new Point(2, 3);
Point second = new Point(2, 3);

System.out.println(first == second);      // false
System.out.println(first.equals(second)); // false

But for a Point class, it may make sense to consider two points equal when their coordinates match. In that case, this rule must be expressed explicitly in equals().

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj == null || getClass() != obj.getClass()) {
        return false;
    }

    Point other = (Point) obj;
    return x == other.x && y == other.y;
}

First, the simplest case is handled — comparing an object with itself. Then null and an object of another class are rejected, and finally the fields that define equality for points are compared.

A correct equals() implementation must follow several rules:

  • an object is equal to itself;
  • if a.equals(b) is true, then b.equals(a) must also be true;
  • if a equals b, and b equals c, then a must equal c;
  • the result must not change on its own while the data used in the comparison remains unchanged;
  • any non-null object is not equal to null.

hashCode(): equal objects must stay consistent

hashCode() returns an integer — the object's hash. The main rule here is connected to equals(): if two objects are equal according to equals(), their hashCode() methods must return the same value.

The reverse is not true: the same hash does not mean that two objects are equal. Different objects can have the same hashCode().

@Override
public int hashCode() {
    return java.util.Objects.hash(x, y);
}

The hash is calculated from the same x and y data used by equals(). This guarantees that two equal objects receive the same hash.

If you override equals(), override hashCode() too. Otherwise, two objects may be considered equal while having inconsistent hashes. That breaks the contract of these methods and leads to incorrect behavior in code that relies on them being consistent.

toString(): an object in a readable form

toString() returns a string representation of an object. The implementation from Object usually produces a string such as Point@1a2b3c. It can serve as a technical representation, but it says almost nothing about the object's contents.

That is why toString() is often overridden in custom classes to include fields that are actually useful:

@Override
public String toString() {
    return "Point{x=" + x + ", y=" + y + "}";
}

Now printing the object immediately shows its state:

Point point = new Point(2, 3);
System.out.println(point); // Point{x=2, y=3}

There is no need to call point.toString() explicitly here: when an object is passed to System.out.println(), its string representation is used automatically.

A toString() format should be concise and easy for a person to read. The standard part after @ should not be treated as the object's memory address or as a stable identifier.