← Назад к вопросам

Что нужно соблюсти при переопределении equals в Lombok?

1.0 Junior🔥 111 комментариев
#Soft Skills и карьера

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Переопределение equals в Lombok — @EqualsAndHashCode

Lombok предоставляет аннотацию @EqualsAndHashCode для автоматической генерации методов equals() и hashCode(). Однако при использовании нужно соблюдать несколько важных правил.

Основное использование

@Data
@EqualsAndHashCode
public class User {
    private Long id;
    private String name;
    private String email;
}

// Эквивалентно:
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    
    User user = (User) o;
    return Objects.equals(id, user.id) &&
           Objects.equals(name, user.name) &&
           Objects.equals(email, user.email);
}

public int hashCode() {
    return Objects.hash(id, name, email);
}

1. ИСКЛЮЧИТЬ поля с циклическими ссылками

Проблема: если использовать поля с циклическими ссылками, получится StackOverflowError.

// НЕПРАВИЛЬНО: циклическая ссылка
@Data
@EqualsAndHashCode
public class User {
    private Long id;
    @OneToMany
    private List<Post> posts;  // Post имеет ссылку на User!
}

@Data
@EqualsAndHashCode
public class Post {
    private Long id;
    @ManyToOne
    private User author;  // Циклическая ссылка User -> Post -> User
}

// При вызове equals() -> StackOverflowError

// ПРАВИЛЬНО: исключить циклические поля
@Data
@EqualsAndHashCode(exclude = {"posts"})
public class User {
    private Long id;
    @OneToMany
    private List<Post> posts;
}

@Data
@EqualsAndHashCode(exclude = {"author"})
public class Post {
    private Long id;
    @ManyToOne
    private User author;
}

2. ИСКЛЮЧИТЬ ленивые поля (Lazy)

Проблема: обращение к lazy полям вне сессии вызывает LazyInitializationException.

// НЕПРАВИЛЬНО
@Data
@EqualsAndHashCode
public class User {
    private Long id;
    
    @OneToMany(fetch = FetchType.LAZY)
    private List<Order> orders;  // Может быть не загружено!
}

// ПРАВИЛЬНО
@Data
@EqualsAndHashCode(exclude = {"orders"})
public class User {
    private Long id;
    private String name;
    
    @OneToMany(fetch = FetchType.LAZY)
    private List<Order> orders;  // Исключаем из equals
}

3. ИСКЛЮЧИТЬ изменяемые/вспомогательные поля

// НЕПРАВИЛЬНО: используем временные метки
@Data
@EqualsAndHashCode
public class User {
    private Long id;
    private String name;
    private LocalDateTime createdAt;  // Может быть разным!
    private LocalDateTime updatedAt;  // Может меняться!
}

// Два User с одинаковым id и name, но разными временем создания != equal
// Это нарушает контракт equals()

// ПРАВИЛЬНО: исключить служебные поля
@Data
@EqualsAndHashCode(of = {"id", "name"})  // Используем только эти поля
public class User {
    private Long id;
    private String name;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

4. Использовать onlyExplicitlyIncluded = true

Проблема: если добавить новое поле, оно автоматически попадет в equals().

// НЕБЕЗОПАСНО
@Data
@EqualsAndHashCode
public class User {
    private Long id;
    private String name;
    private String email;
}

// Если позже добавить поле:
private String phoneNumber;  // Автоматически в equals()!

// БЕЗОПАСНЕЕ: явно указать поля
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class User {
    @EqualsAndHashCode.Include
    private Long id;
    
    @EqualsAndHashCode.Include
    private String name;
    
    // email НЕ в equals()
    private String email;
}

5. Соблюдать контракт equals() и hashCode()

Правило: если a.equals(b), то a.hashCode() == b.hashCode().

// ПРАВИЛЬНО
@Data
@EqualsAndHashCode(of = {"id"})
public class User {
    @EqualsAndHashCode.Include
    private Long id;
    
    private String name;  // Может быть разным, но equals на id
    
    // hashCode() = hash(id) - соответствует equals()
}

User u1 = new User(1L, "John");
User u2 = new User(1L, "Jane");

System.out.println(u1.equals(u2));           // true (одинаковый id)
System.out.println(u1.hashCode() == u2.hashCode());  // true (одинаковый hash)

6. Проблема с наследованием

// НЕПРАВИЛЬНО: использовать equals в наследовании
@Data
@EqualsAndHashCode
public class User {
    private Long id;
    private String name;
}

@Data
@EqualsAndHashCode
public class AdminUser extends User {
    private String adminRole;
}

User user = new User(1L, "John");
AdminUser admin = new AdminUser(1L, "John", null);

admin.setAdminRole("ADMIN");
user.equals(admin);  // true или false? Проблема!

// ПРАВИЛЬНО
@Data
@EqualsAndHashCode(callSuper = true)  // Учитывать parent
public class AdminUser extends User {
    private String adminRole;
}

7. Работа с Collections

// ПРОБЛЕМА: List и Set чувствительны к порядку
@Data
@EqualsAndHashCode
public class Team {
    private Long id;
    private Set<User> members;  // Set - ok, порядок неважен
}

// ПРАВИЛЬНО для List
@Data
@EqualsAndHashCode(exclude = {"members"})
public class Team {
    private Long id;
    private List<User> members;  // List исключаем, так как порядок важен
}

Рекомендуемый паттерн

// ЛУЧШИЙ СПОСОБ
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Entity
public class User {
    // Идентификатор в equals
    @Id
    @EqualsAndHashCode.Include
    private Long id;
    
    // Важные поля (уникальные)
    @EqualsAndHashCode.Include
    private String email;  // email должен быть уникален
    
    // Служебные и связанные поля - исключаем
    private String name;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Post> posts;
}

// Результат:
// equals() сравнивает только id и email
// hashCode() зависит только от id и email
// Нет проблем с lazy загрузкой
// Нет проблем с циклическими ссылками

Альтернатива: ручное переопределение

@Data
public class User {
    private Long id;
    private String email;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return Objects.equals(id, user.id) &&
               Objects.equals(email, user.email);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id, email);
    }
}

Проверка в тестах

@Test
public void testEqualsContract() {
    User u1 = new User(1L, "john@example.com");
    User u2 = new User(1L, "john@example.com");
    User u3 = new User(2L, "jane@example.com");
    
    // Рефлексивность
    assertTrue(u1.equals(u1));
    
    // Симметричность
    assertTrue(u1.equals(u2));
    assertTrue(u2.equals(u1));
    
    // Транзитивность
    assertEquals(u1.hashCode(), u2.hashCode());
    assertNotEquals(u1.hashCode(), u3.hashCode());
}

Вывод

При использовании @EqualsAndHashCode в Lombok:

  1. Исключить циклические ссылки (exclude)
  2. Исключить lazy поля
  3. Исключить вспомогательные/изменяемые поля
  4. Использовать onlyExplicitlyIncluded = true для явного контроля
  5. Соблюдать контракт equals() и hashCode()
  6. Учитывать наследование (callSuper = true)
  7. Тестировать контракт equals()
Что нужно соблюсти при переопределении equals в Lombok? | PrepBro