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.
GO TO FULL VERSION