Вопрос 10 · Раздел 20

Можно ли использовать Record как ключ в HashMap

Record автоматически получает корректные equals() и hashCode(), что критически важно для работы HashMap.

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Да, Record — отличный ключ для HashMap! Это одна из лучших ролей для Record.

Record автоматически получает корректные equals() и hashCode(), что критически важно для работы HashMap.

public record CacheKey(String tenantId, String entityType, String entityId) {}

Map<CacheKey, Object> cache = new HashMap<>();

CacheKey key1 = new CacheKey("t1", "user", "u1");
cache.put(key1, userData);

// Другой объект с теми же значениями — найдёт данные!
CacheKey key2 = new CacheKey("t1", "user", "u1");
cache.get(key2);  // userData ✅

Почему Record хорош как ключ:

  • ✅ Immutable — ключ не изменится после добавления
  • ✅ equals/hashCode автогенерированы
  • ✅ Компактный и понятный

🟡 Middle Level

Как это работает

Контракт ключа в HashMap:

  1. Если a.equals(b), то a.hashCode() == b.hashCode()
  2. Ключ не должен меняться после добавления в Map

Record удовлетворяет оба условия:

public record Point(int x, int y) {}

Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);

// Контракт выполняется
assert p1.equals(p2);           // true
assert p1.hashCode() == p2.hashCode();  // true

Типичные ошибки

  1. Mutable компоненты в ключе: ```java public record BadKey(List values) {}

BadKey key = new BadKey(new ArrayList<>(List.of(“a”, “b”))); map.put(key, “value”);

key.values().add(“c”); // Изменили! hashCode больше не корректен map.get(key); // Может не найти!


**Решение:**
```java
public record GoodKey(List<String> values) {
    public GoodKey {
        values = List.copyOf(values);  // immutable
    }
}
  1. Массив в ключе: ```java public record ArrayKey(int[] ids) {}

ArrayKey k1 = new ArrayKey(new int[]{1, 2, 3}); ArrayKey k2 = new ArrayKey(new int[]{1, 2, 3});

k1.equals(k2); // false! Массивы сравнивают по ссылке k1.hashCode() == k2.hashCode(); // скорее всего false


**Решение:**
```java
public record ArrayKey(int[] ids) {
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ArrayKey other)) return false;
        return Arrays.equals(ids, other.ids);
    }
    
    @Override
    public int hashCode() {
        return Arrays.hashCode(ids);
    }
}

Практическое применение

1. Кэш:

public record CacheKey(String tenantId, String query, List<String> params) {
    public CacheKey {
        params = List.copyOf(params);
    }
}

Map<CacheKey, List<User>> queryCache = new ConcurrentHashMap<>();

2. Композитный ключ:

public record OrderItemId(String orderId, String itemId) {}

Map<OrderItemId, OrderItem> items = new HashMap<>();
items.put(new OrderItemId("order-1", "item-1"), item);

🔴 Senior Level

Internal Implementation

Как HashMap использует hashCode:

// HashMap.internal hash
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// Record hashCode генерируется компилятором через оптимизированную цепочку умножений,
// НЕ через Objects.hash(). Objects.hash создаёт Object[] и медленнее.
// Автогенерированный hashCode для Record:

Автогенерированный hashCode для Record:

public record User(String name, int age) {
    // Автогенерированный:
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

// Objects.hash использует Arrays.hashCode:
// int result = Arrays.hashCode(new Object[]{name, age});

Архитектурные Trade-offs

Record vs обычный класс как ключ:

Аспект Record Обычный класс
equals/hashCode Авто, корректные Ручные, возможны ошибки
Immutability Гарантирована Нужно следить
Производительность Оптимальная Зависит от реализации
Мутабельные поля Нужно защищать Нужно следить

Edge Cases

1. Большие Record как ключ:

// hashCode вычисляется каждый раз — может быть медленно
public record LargeKey(
    String field1, String field2, String field3,
    String field4, String field5, String field6
) {}

// Для hot path — кэшируйте hashCode
public record CachedHashKey(String value) {
    private final int hash;
    
    public CachedHashKey {
        hash = value.hashCode();
    }
    
    @Override
    public int hashCode() {
        return hash;
    }
}

2. Null в Record:

public record NullableKey(String name) {}

NullableKey k1 = new NullableKey(null);
NullableKey k2 = new NullableKey(null);

k1.equals(k2);  // true (Objects.equals корректно обрабатывает null)

Производительность

Операция           | Record key | String key
-------------------|------------|-----------
hashCode()         | 15 ns      | 10 ns
equals()           | 12 ns      | 8 ns
HashMap get/put    | O(1)       | O(1)

Для составных ключей — чуть дороже, но O(1) сохраняется

Production Experience

Query cache:

public record QueryCacheKey(
    String query,
    List<Object> params,
    int limit,
    int offset
) {
    public QueryCacheKey {
        params = List.copyOf(params);
    }
    
    @Override
    public int hashCode() {
        // Кэширование для больших ключей
        return Objects.hash(query, params, limit, offset);
    }
}

@Service
public class QueryCache {
    private final Map<QueryCacheKey, List<User>> cache = new ConcurrentHashMap<>();
    
    public List<User> getOrLoad(String query, List<Object> params, int limit, int offset) {
        var key = new QueryCacheKey(query, params, limit, offset);
        return cache.computeIfAbsent(key, k -> loadFromDb(k));
    }
}

Best Practices

// ✅ Record для составных ключей
public record CacheKey(String tenant, String entityType, String id) {}

// ✅ Immutable компоненты
public record SafeKey(List<String> values) {
    public SafeKey { values = List.copyOf(values); }
}

// ✅ Используйте Records как ключи в ConcurrentHashMap
Map<CacheKey, Object> cache = new ConcurrentHashMap<>();

// ❌ Mutable компоненты без защиты
// ❌ Массивы в ключе без переопределения equals/hashCode
// ❌ Record с большим количеством полей (hashCode expensive)

🎯 Шпаргалка для интервью

Обязательно знать:

  • Record — отличный ключ для HashMap благодаря автогенерированным equals/hashCode
  • Record immutable — ключ не изменится после добавления в Map
  • Mutable компоненты (List, массив) ломают hashCode контракт — нужен defensive copy
  • Массивы сравниваются по ссылке, не по содержимому — нужно переопределить equals/hashCode с Arrays.equals
  • Для больших Record hashCode может быть дорогим — можно кэшировать
  • Null в компонентах корректно обрабатывается через Objects.equals

Частые уточняющие вопросы:

  • Почему Record лучше обычного класса как ключ? — Автогенерация equals/hashCode, гарантированная immutable
  • Что будет с mutable компонентом в ключе? — При изменении hashCode меняется — данные потеряны в HashMap
  • Можно ли использовать массив как компонент ключа? — Только с переопределением equals/hashCode через Arrays.equals/hashCode
  • Record hashCode вычисляется каждый раз? — Да, но можно кэшировать для больших ключей

Красные флаги (НЕ говорить):

  • ❌ “Массив в Record корректно сравнивается” — Массивы сравниваются по ссылке, не по содержимому
  • ❌ “Mutable список в ключе безопасен” — Изменение списка ломает HashMap
  • ❌ “Record hashCode кэшируется автоматически” — Нет, вычисляется каждый раз
  • ❌ “hashCode() использует Objects.hash()” — Используется оптимизированная цепочка умножений

Связанные темы:

  • [[1. Что такое Record в Java и с какой версии они доступны]]
  • [[5. Какие методы автоматически генерируются для Record]]
  • [[9. Являются ли поля Record финальными]]
  • [[14. Можно ли создать массив дженерик-типа]]