Чи можна використовувати Record як ключ в HashMap
Record автоматично отримує коректні equals() і hashCode(), що критично важно для роботи HashMap.
🟢 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:
- Якщо
a.equals(b), тоa.hashCode() == b.hashCode() - Ключ не повинен змінюватися після додавання в 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
Типові помилки
- 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
}
}
- Масив в ключі: ```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. Чи можна створити масив дженерик-типу]]