Question 10 · Section 20

Can you use Record as a key in HashMap

Record automatically gets correct equals() and hashCode(), which is critically important for HashMap to work.

Language versions: English Russian Ukrainian

🟢 Junior Level

Yes, Record is a great key for HashMap! This is one of the best roles for Record.

Record automatically gets correct equals() and hashCode(), which is critically important for HashMap to work.

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);

// Another object with the same values — will find the data!
CacheKey key2 = new CacheKey("t1", "user", "u1");
cache.get(key2);  // userData ✅

Why Record is good as a key:

  • ✅ Immutable — key won’t change after being added
  • ✅ equals/hashCode auto-generated
  • ✅ Compact and clear

🟡 Middle Level

How it works

Key contract in HashMap:

  1. If a.equals(b), then a.hashCode() == b.hashCode()
  2. Key must not change after being added to Map

Record satisfies both conditions:

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

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

// Contract is fulfilled
assert p1.equals(p2);           // true
assert p1.hashCode() == p2.hashCode();  // true

Common Mistakes

  1. Mutable components in key: ```java public record BadKey(List values) {}

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

key.values().add(“c”); // Changed! hashCode is no longer correct map.get(key); // May not find!


**Solution:**
```java
public record GoodKey(List<String> values) {
    public GoodKey {
        values = List.copyOf(values);  // immutable
    }
}
  1. Array in key: ```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! Arrays compare by reference k1.hashCode() == k2.hashCode(); // most likely false


**Solution:**
```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);
    }
}

Practical Application

1. Cache:

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

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

2. Composite key:

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

How HashMap uses hashCode:

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

// Record hashCode is generated by compiler through an optimized multiplication chain,
// NOT through Objects.hash(). Objects.hash creates an Object[] and is slower.
// Auto-generated hashCode for Record:

Auto-generated hashCode for Record:

public record User(String name, int age) {
    // Auto-generated:
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

// Objects.hash uses Arrays.hashCode:
// int result = Arrays.hashCode(new Object[]{name, age});

Architectural Trade-offs

Record vs regular class as key:

Aspect Record Regular class
equals/hashCode Auto, correct Manual, possible bugs
Immutability Guaranteed Need to maintain
Performance Optimal Depends on implementation
Mutable fields Need to protect Need to maintain

Edge Cases

1. Large Record as key:

// hashCode is computed every time — can be slow
public record LargeKey(
    String field1, String field2, String field3,
    String field4, String field5, String field6
) {}

// For hot paths — cache hashCode
public record CachedHashKey(String value) {
    private final int hash;

    public CachedHashKey {
        hash = value.hashCode();
    }

    @Override
    public int hashCode() {
        return hash;
    }
}

2. Null in Record:

public record NullableKey(String name) {}

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

k1.equals(k2);  // true (Objects.equals handles null correctly)

Performance

Operation          | Record key | String key
-------------------|------------|-----------
hashCode()         | 15 ns      | 10 ns
equals()           | 12 ns      | 8 ns
HashMap get/put    | O(1)       | O(1)

For composite keys — slightly more expensive, but O(1) is preserved

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() {
        // Caching for large keys
        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 for composite keys
public record CacheKey(String tenant, String entityType, String id) {}

// ✅ Immutable components
public record SafeKey(List<String> values) {
    public SafeKey { values = List.copyOf(values); }
}

// ✅ Use Records as keys in ConcurrentHashMap
Map<CacheKey, Object> cache = new ConcurrentHashMap<>();

// ❌ Mutable components without protection
// ❌ Arrays in key without overriding equals/hashCode
// ❌ Record with many fields (hashCode is expensive)

🎯 Interview Cheat Sheet

Must know:

  • Record is a great key for HashMap thanks to auto-generated equals/hashCode
  • Record is immutable — key won’t change after being added to Map
  • Mutable components (List, array) break hashCode contract — need defensive copy
  • Arrays are compared by reference, not by contents — need to override equals/hashCode with Arrays.equals
  • For large Records, hashCode can be expensive — can cache it
  • Null in components is handled correctly via Objects.equals

Common follow-up questions:

  • Why is Record better than regular class as key? — Auto-generation of equals/hashCode, guaranteed immutability
  • What happens with mutable component in key? — When changed, hashCode changes — data is lost in HashMap
  • Can you use array as key component? — Only with overriding equals/hashCode via Arrays.equals/hashCode
  • Is Record hashCode computed every time? — Yes, but can be cached for large keys

Red flags (DO NOT say):

  • ❌ “Array in Record compares correctly” — Arrays are compared by reference, not by contents
  • ❌ “Mutable list in key is safe” — Changing the list breaks HashMap
  • ❌ “Record hashCode is cached automatically” — No, computed every time
  • ❌ “hashCode() uses Objects.hash()” — Uses optimized multiplication chain

Related topics:

  • [[1. What is Record in Java and since which version are they available]]
  • [[5. What methods are automatically generated for a Record]]
  • [[9. Are Record fields final]]
  • [[14. Can you create an array of generic type]]