Skip to content
Spring Boot sb lombok 3 min read

@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 / .Exclude over the string-based of/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.

StrategyVerdict
Default @EqualsAndHashCode (all fields)Avoid — mutable fields break hash contracts
@EqualsAndHashCode on generated id onlyRisky 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 UUID set in the constructor) and base equals/hashCode on that. Never rely on @Data’s all-field defaults for managed entities — see @Data and Lombok Best Practices.

Last updated June 13, 2026
Was this helpful?