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

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

2.2 Middle🔥 201 комментариев
#Другое

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

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

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

Правила переопределения hashCode() с использованием Lombok

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

Основное правило: соблюдение контракта

Забывай о том, что Lombok это генерирует автоматически. Контракт между equals() и hashCode() ОСТАЕТСЯ в силе:

  • Если equals() вернул truehashCode() ОБЯЗАН вернуть одинаковое значение
  • Нарушение этого правила = непредсказуемое поведение HashMap/HashSet

1. Использовать параметр of для выбора полей

Указывай ТОЛЬКО те поля, которые участвуют вEquals:

// ✅ Правильно: явно указываем поля
@EqualsAndHashCode(of = {"id", "email"})
public class User {
    private Long id;           // Участвует
    private String email;      // Участвует
    private String name;       // НЕ участвует
    private LocalDateTime createdAt;  // НЕ участвует
}

Зачем это?

  • Если включить изменяемые поля (name), то после изменения объект не найдется в HashMap
  • Если включить логически неважные поля, то разные пользователи с одинаковым id/email будут считаться разными
// ❌ ОШИБКА: включил все поля
@EqualsAndHashCode
public class User {
    private Long id;
    private String email;
    private String name;  // Изменяемое поле!
    private LocalDateTime createdAt;
}

Map<User, String> map = new HashMap<>();
User user = new User(1L, "alice@example.com", "Alice", now());
map.put(user, "ADMIN");

user.name = "Алиса";  // Меняем имя
System.out.println(map.get(user));  // null! (ПРОБЛЕМА)

2. Никогда не включай изменяемые поля

Immutable поля — это поля, которые никогда не меняются после создания объекта:

@EqualsAndHashCode(of = {"id", "uuid"})  // Неизменяемые поля
public class Product {
    private final Long id;              // final - неизменяемо
    private final UUID uuid;            // final - неизменяемо
    
    private String name;                // Изменяемо (setter)
    private BigDecimal price;           // Изменяемо (setter)
    private LocalDateTime updatedAt;    // Изменяемо (setter)
    
    public void setName(String name) { this.name = name; }
    public void setPrice(BigDecimal price) { this.price = price; }
}

3. Обработка отношений (relationships)

Если класс имеет связь с другим объектом, не включай весь объект, используй его id:

// ❌ ОШИБКА: включил весь объект
@EqualsAndHashCode(of = {"id", "owner"})
public class Document {
    private Long id;
    private User owner;  // Весь объект - может быть null или изменчив
}

// ✅ Правильно: только id
@EqualsAndHashCode(of = {"id", "ownerId"})
public class Document {
    private Long id;
    private Long ownerId;  // Только id, стабильное
    private User owner;    // Транзиент, не участвует
}

4. Использование doNotUseGetters для производительности

По умолчанию Lombok вызывает getters, что может быть дорого:

// ❌ Может быть медленно (вызывает getter)
@EqualsAndHashCode(of = {"id"})
public class HeavyObject {
    private Long id;
    
    public Long getId() {
        // Может содержать логику!
        return id;
    }
}

// ✅ Быстрее (обращается напрямую к полю)
@EqualsAndHashCode(of = {"id"}, doNotUseGetters = true)
public class HeavyObject {
    private Long id;
}

5. Исключение транзиентных полей

Удаляй поля, которые не должны участвовать (например, кеши или вычисляемые значения):

@EqualsAndHashCode(exclude = {"computedValue", "cache"})
public class DataProcessor {
    private Long id;
    private String data;
    
    @Transient
    private String computedValue;  // Вычисляется
    
    @Transient
    private Map<String, Object> cache;  // Кеш
}

6. Иерархия классов (subclass)

Если класс наследуется, используй callSuper = true:

public abstract class Entity {
    @EqualsAndHashCode.Include
    private Long id;
}

// ✅ Включаем equals/hashCode родителя
@EqualsAndHashCode(callSuper = true, of = {"name"})
public class User extends Entity {
    private String name;
    private String email;
}

// ❌ Игнорируем id родителя - ОШИБКА
@EqualsAndHashCode(of = {"name"})
public class User extends Entity {
    private String name;
}

7. Работа с Collections

Не включай изменяемые коллекции в hashCode:

// ❌ ОШИБКА: список изменяемый
@EqualsAndHashCode
public class Order {
    private Long id;
    private List<Item> items;  // Изменяемо (добавляем товары)
}

// ✅ Правильно: только id
@EqualsAndHashCode(of = {"id"})
public class Order {
    private Long id;
    private List<Item> items;  // Не участвует в equals/hashCode
}

Полный пример правильного использования

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

@EqualsAndHashCode(of = {"id", "email"}, doNotUseGetters = true)
@Getter
@Setter
public class User {
    // Neизменяемые поля (участвуют в equals/hashCode)
    private final Long id;
    private final String email;
    
    // Изменяемые поля (НЕ участвуют)
    private String name;
    private String phone;
    private LocalDateTime updatedAt;
    
    public User(Long id, String email) {
        this.id = id;
        this.email = email;
    }
}

// Использование
Map<User, String> userRoles = new HashMap<>();

User user1 = new User(1L, "alice@example.com");
user1.setName("Alice");
userRoles.put(user1, "ADMIN");

User user2 = new User(1L, "alice@example.com");
user2.setName("Alice Updated");  // Другое имя

// Работает! Хотя name разные
System.out.println(userRoles.get(user2));  // ADMIN
System.out.println(userRoles.size());      // 1 (не 2)

Ошибки, которые часто допускают

❌ Ошибка 1: По умолчанию все поля

@EqualsAndHashCode  // Включит ВСЕ поля!
public class User {
    private Long id;
    private String email;
    private String password;  // Может меняться
    private LocalDateTime lastLogin;  // Меняется часто
}

❌ Ошибка 2: Забыл о nullable полях

@EqualsAndHashCode(of = {"id", "email"})
public class User {
    private Long id;
    private String email;  // Может быть null
}

// Проблема: user с email=null и user с email="test" в разных bucket'ах

❌ Ошибка 3: Не использовал в паре с equals

// ❌ Переопределил только hashCode
@EqualsAndHashCode(of = {"id"})
public class User {
    private Long id;
    private String email;
}

// Будет сгенерирован и equals с тем же полем
// Но если кто-то переопределит equals в подклассе...

Проверка правильности

Тест для проверки контракта:

@Test
void testEqualsHashCodeContract() {
    User user1 = new User(1L, "alice@example.com");
    user1.setName("Alice");
    
    User user2 = new User(1L, "alice@example.com");
    user2.setName("Alice Updated");  // Разное имя
    
    // Если equals вернул true
    if (user1.equals(user2)) {
        // hashCode ОБЯЗАН быть одинаковым
        assertEquals(user1.hashCode(), user2.hashCode(),
            "Нарушен контракт между equals и hashCode");
    }
}

@Test
void testHashMapBehavior() {
    Map<User, String> map = new HashMap<>();
    User user = new User(1L, "alice@example.com");
    map.put(user, "ADMIN");
    
    user.setName("Updated Name");  // Меняем изменяемое поле
    
    // Должна найти по прежнему ключу
    assertNotNull(map.get(user), "Не найден в HashMap после изменения изменяемого поля");
}

Итоговый чеклист для Lombok @EqualsAndHashCode

  • Используй параметр of — явно указывай поля
  • Только immutable поля — не включай final и изменяемые
  • Исключай транзиентные — используй exclude для кешей
  • Не используй в листах/картах — только id отношений
  • Тестируй контракт — проверяй, что equals/hashCode согласованы
  • Документируй решение — почему именно эти поля

Правильное использование @EqualsAndHashCode сэкономит часы отладки проблем с HashMap и HashSet.