Питання 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. Чи можна створити масив дженерик-типу]]