@EqualsAndHashCode & @ToString
@EqualsAndHashCode and @ToString generate the value-comparison and string-representation methods that Java classes so often need. They are powerful, but their default “include every field” behavior is exactly what causes the most notorious Lombok bugs in JPA entities. This page covers correct usage and the pitfalls to avoid.
Generating the methods
By default both annotations consider all non-static, non-transient fields.
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode
public class Money {
private String currency;
private long amount;
}
This generates a toString() like Money(currency=USD, amount=500) and an equals/hashCode pair that compares both fields. For simple value objects this is precisely what you want.
Choosing fields with exclude / of
You can narrow which fields participate. There are two complementary styles:
exclude— include everything except the named fields.of— include only the named fields (blocklist vs allowlist).
@ToString(exclude = "password")
@EqualsAndHashCode(of = {"username"})
public class Account {
private String username;
private String password; // never logged, never compared
private Instant lastLogin;
}
The modern, refactor-safe alternative is the member-level marker annotations with onlyExplicitlyIncluded:
@ToString(onlyExplicitlyIncluded = true)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Account {
@EqualsAndHashCode.Include
@ToString.Include
private String username;
private String password;
}
Tip: Prefer
@EqualsAndHashCode.Include/.Excludeover the string-basedof/exclude. They survive field renames and let your IDE find usages.
callSuper for inheritance
When a class extends another that also has meaningful equality or string state, set callSuper = true so the parent’s implementation is incorporated. Forgetting this is a subtle bug in class hierarchies.
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Employee extends Person {
private String department;
}
Without callSuper = true, two Employee objects with different Person fields but the same department would compare equal.
JPA pitfalls — the important part
Putting a default @EqualsAndHashCode or @ToString (or @Data, which bundles both) on an entity with relationships leads to two serious failures.
Infinite recursion / lazy-loading blow-ups
With a bidirectional relationship, the generated toString() of each side calls the other’s toString(), recursing forever. The same traversal also triggers lazy loading, throwing LazyInitializationException outside a transaction or firing surprise queries.
// BUGGY — toString/equals traverse the relationship
@Entity
@ToString
@EqualsAndHashCode
public class Author {
@Id @GeneratedValue private Long id;
private String name;
@OneToMany(mappedBy = "author")
private List<Book> books; // <-- recursion + lazy loading
}
Fix: exclude the relationship from both toString() and equals():
@Entity
@Getter @Setter
@ToString(exclude = "books")
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Author {
@Id @GeneratedValue
@EqualsAndHashCode.Include
private Long id;
private String name;
@OneToMany(mappedBy = "author")
private List<Book> books;
}
Identity based on a generated ID
A database-generated id is null before persistence and assigned afterward. If equals/hashCode use it, an entity’s hash code changes after a save, breaking any HashSet or HashMap it was added to while transient. Comparing on all mutable business fields is equally fragile.
| Strategy | Verdict |
|---|---|
Default @EqualsAndHashCode (all fields) | Avoid — mutable fields break hash contracts |
@EqualsAndHashCode on generated id only | Risky while the entity is transient (id == null) |
| A stable, assigned business key (e.g. UUID/natural key) | Recommended for entities |
Warning: For JPA entities, the safest pattern is to assign a stable identifier (such as a
UUIDset in the constructor) and baseequals/hashCodeon that. Never rely on@Data’s all-field defaults for managed entities — see @Data and Lombok Best Practices.