← Назад к вопросам
Что нужно соблюсти при переопределении 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:
- Исключить циклические ссылки (exclude)
- Исключить lazy поля
- Исключить вспомогательные/изменяемые поля
- Использовать onlyExplicitlyIncluded = true для явного контроля
- Соблюдать контракт equals() и hashCode()
- Учитывать наследование (callSuper = true)
- Тестировать контракт equals()