CodeGym /Courses /JAVA 25 SELF /equals, hashCode, toString: auto-generation

equals, hashCode, toString: auto-generation

JAVA 25 SELF
Level 22, Lesson 2
Available

1. Automatic generation of equals, hashCode, toString

Why are these methods needed?

When working with objects in Java, you quickly run into the same tasks. Sometimes you need to check whether two objects are equal. For example, to see if it already exists in a collection like a Set or a Map. In other cases, an object is used as a key in a HashMap, and here you can’t do without special comparison rules. And almost always you want to print an object to the log or screen so that the output isn’t just gibberish like MyClass@7b23ec81 but something meaningful.

For these cases, every class in Java has three special methods:

  • equals(Object o) is responsible for equality checks.
  • hashCode() gives the object a numeric “fingerprint” needed by collections like hash tables.
  • toString() returns a convenient string representation of the object, which greatly simplifies debugging and printing.

Why is this painful in regular classes?

In regular classes, you have to write these methods by hand. That’s where boredom and headaches begin. You get a lot of boilerplate code that only clutters the class. It’s very easy to make a mistake somewhere: forget to compare a field, compute hashCode incorrectly, and then chase mysterious bugs. And if you add a new field to the class — you’ll have to go back into all these methods and rewrite everything again.

Example of a regular class


public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return 31 * x + y;
    }

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

Look familiar? Yes — and that’s only for two fields! What if there are twenty?

How record does it

A record class does all of this for you. Just declare:


public record Point(int x, int y) { }

And Java will generate it automatically:

  • Constructor
  • Getters (x(), y())
  • equals, hashCode, toString

Automatically generated methods

  • equals compares all record components by value.
  • hashCode is computed from all components.
  • toString returns a string like Point[x=1, y=2].

Let’s see it in action!


public record Point(int x, int y) {}

public class Demo {
    public static void main(String[] args) {
        Point p1 = new Point(1, 2);
        Point p2 = new Point(1, 2);

        System.out.println(p1.equals(p2)); // true
        System.out.println(p1.hashCode() == p2.hashCode()); // true
        System.out.println(p1); // Point[x=1, y=2]
    }
}

Output:

true
true
Point[x=1, y=2]

Everything works as expected, without a single extra line of code!

2. Why this matters: collections, debugging, and safety

Correct behavior in collections

Imagine you use objects as keys in a HashMap or as elements in a HashSet. If equals and hashCode are implemented incorrectly, collections will behave oddly: they may fail to find an element you just added, or, conversely, treat two different objects as the same.

With record classes, you can be confident: equality and the hash always account for all record components (in the order in which they are declared).

Example: using a record as a key


import java.util.HashMap;
import java.util.Map;

public class Demo {
    public static void main(String[] args) {
        record Point(int x, int y) {}

        Map<Point, String> map = new HashMap<>();
        Point p1 = new Point(3, 4);
        map.put(p1, "Hello!");

        Point p2 = new Point(3, 4);
        System.out.println(map.get(p2)); // "Hello!" — works!
    }
}

Note: p1 and p2 are different objects (different references), but contain the same field values, so they are considered equal. And you will learn more about Map and HashMap at level 26 :P

Convenient debugging and logging

Instead of the dreary Point@1a2b3c4d (as is the default for regular classes), a record class prints nicely and informatively:

Point[x=3, y=4]

This saves a lot of time during debugging and logging.

3. How equals, hashCode, toString work inside a record

The equals method

A record class implements equals so that two objects are considered equal if:

  • They are of the same type (the same record class)
  • All their components are equal (== for primitives, equals() for objects)

Comparison example


Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = new Point(1, 3);

System.out.println(p1.equals(p2)); // true
System.out.println(p1.equals(p3)); // false

The hashCode method

The hash code is computed from all record components, usually using the standard method Objects.hash(...).


System.out.println(p1.hashCode()); // For example, 994
System.out.println(p2.hashCode()); // Also 994
System.out.println(p3.hashCode()); // A different number

The toString method

The string representation is always in the format:

ClassName[field1=value1, field2=value2, ...]

System.out.println(p1); // Point[x=1, y=2]

4. Overriding equals, hashCode, toString: when and how?

Sometimes (rarely, but it happens) you need to change the default behavior of these methods. For example, you want toString to return a string in a different format, or you want comparison to happen based only on part of the fields.

Caution: if you override equals/hashCode, do it very deliberately! Violating their “contract” can lead to bugs that are hard to catch.

How to override a method

Just declare your method inside the body of the record class:


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

Point p = new Point(3, 5);
System.out.println(p); // (3; 5)

Can you override equals/hashCode?

Yes, but it is strongly discouraged unless you know exactly what you’re doing. For example, if you want comparison to be based only on the x field (which is already odd):


public record Point(int x, int y) {
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point other)) return false;
        return x == other.x;
    }

    @Override
    public int hashCode() {
        return Integer.hashCode(x);
    }
}

Point p1 = new Point(1, 2);
Point p2 = new Point(1, 999);

System.out.println(p1.equals(p2)); // true (!)

But be careful: if you override equals, always override hashCode as well — otherwise collections will behave incorrectly.

Best practice

  • If you don’t know exactly why you should override — don’t override!
  • For toString — feel free to use your own format if you want.
  • For equals/hashCode — only if there is a strong reason and you understand the consequences.

5. Practice: comparing objects and using records in collections

Example: comparing two record objects


public record User(String name, int age) {}

public class Demo {
    public static void main(String[] args) {
        User u1 = new User("Alice", 20);
        User u2 = new User("Alice", 20);
        User u3 = new User("Bob", 25);

        System.out.println(u1.equals(u2)); // true
        System.out.println(u1.equals(u3)); // false

        System.out.println(u1.hashCode() == u2.hashCode()); // true
        System.out.println(u1); // User[name=Alice, age=20]
    }
}

Example: using a record as a HashMap key

Let’s imagine we have an application where we store the number of user visits by their name and age (you never know — there might be two “Ivan, 20 years old” in the club).


import java.util.HashMap;
import java.util.Map;

public class Demo {
    public static void main(String[] args) {
        record User(String name, int age) {}

        Map<User, Integer> visits = new HashMap<>();
        User ivan20 = new User("Ivan", 20);
        User ivan22 = new User("Ivan", 22);

        visits.put(ivan20, 5);
        visits.put(ivan22, 2);

        // Let's verify that lookup by value works correctly
        System.out.println(visits.get(new User("Ivan", 20))); // 5
        System.out.println(visits.get(new User("Ivan", 22))); // 2
    }
}

If equals and hashCode were not implemented correctly, the lookup wouldn’t work. And you will learn more about Map and HashMap in the level 26 lectures :P

6. Common mistakes when working with equals, hashCode, toString in record classes

Mistake #1: Expecting that fields can be changed after creation.
Record fields are always final, and comparison is based on their values set in the constructor. If you somehow “change” the internal state (for example, via a mutable object inside a field), equality and the hash may become incorrect.

Mistake #2: Overriding equals but forgetting about hashCode.
If you override one of these methods — always override the other! Otherwise, collections (HashSet, HashMap) will behave unpredictably.

Mistake #3: Expecting toString to be in some other format.
If you need a special string format — just override toString. By default, the format is always ClassName[field1=value1, field2=value2].

Mistake #4: Using a record for complex classes with mutable fields.
Record fields should be immutable. If you use, for example, an ArrayList as a field and someone changes its contents, equality and the hash code may “break.” It’s better to use only immutable types inside records.

Mistake #5: Using a record for classes whose behavior is not that of a value object.
A record is not “a small class with short syntax.” It is a value object designed to store a set of values. If you have complex logic, mutable state, or a need for inheritance — use a regular class.

1
Task
JAVA 25 SELF, level 22, lesson 2
Locked
Book details in the library report 📚
Book details in the library report 📚
1
Task
JAVA 25 SELF, level 22, lesson 2
Locked
Map location identification 📍
Map location identification 📍
1
Task
JAVA 25 SELF, level 22, lesson 2
Locked
Nice user profile presentation 🧑‍💻
Nice user profile presentation 🧑‍💻
1
Task
JAVA 25 SELF, level 22, lesson 2
Locked
Product identification by name in the store 🏷️
Product identification by name in the store 🏷️
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION