Чи можна зберігати null значення в HashMap?
get(key) повертає null у двох випадках:
🟢 Junior Level
Так, HashMap дозволяє зберігати будь-яку кількість null в якості значень.
Приклад:
HashMap<String, String> map = new HashMap<>();
map.put("name", null);
map.put("email", null);
map.put("age", "25");
System.out.println(map.get("name")); // null
System.out.println(map.size()); // 3 (всі три елементи збережені)
Проблема: Коли get(key) повертає null, незрозуміло — ключа немає чи значення дорівнює null?
Рішення: Використовуйте containsKey() для перевірки:
if (map.containsKey("name")) {
// Ключ є, і його значення може бути null
}
🟡 Middle Level
Проблема неоднозначності (Ambiguity)
get(key) повертає null у двох випадках:
- Ключа немає в мапі
- Ключ є, але його значення =
null
Map<String, String> map = new HashMap<>();
map.put("key", null);
System.out.println(map.get("key")); // null (значення)
System.out.println(map.get("missing")); // null (немає ключа)
// Як відрізнити?
System.out.println(map.containsKey("key")); // true
System.out.println(map.containsKey("missing")); // false
Порівняння колекцій
| Колекція | Null-значення | Особливості |
|---|---|---|
| HashMap | Дозволено | Стандартна поведінка |
| Hashtable | Заборонено (NPE) | Застаріла |
| ConcurrentHashMap | Заборонено (NPE) | Багатопотокова безпека |
| TreeMap | Дозволено | Сортування за ключами |
Чому ConcurrentHashMap забороняє null?
В багатопотоковому середовищі не можна атомарно перевірити get() + containsKey():
// Потік A:
String val = map.get(key); // null
// Потік B видаляє ключ між викликами:
// Потік A:
boolean exists = map.containsKey(key); // false — але val вже був null!
Best Practices
- Уникайте null-значень — використовуйте Optional або Null Object
- Завжди перевіряйте
containsKey()перед обробкою null - Документуйте — чи може ваш метод повернути null з Map
Типові помилки
- NPE при використанні значення:
map.get("key").toString(); // NPE, якщо значення = null - Плутанина з відсутністю ключа — логічна помилка
🔴 Senior Level
Liveness Problem
В ConcurrentHashMap заборона null — це усунення ambiguity в check-then-act:
// Гіпотетичний сценарій (CHM насправді забороняє null)
// Показує НАВІЩО CHM забороняє null:
if (map.get(key) == null) { // Крок 1: null
if (!map.containsKey(key)) { // Крок 2: false (ключ доданий іншим потоком!)
map.put(key, value); // Крок 3: перезаписуємо чуже значення
}
}
Заборона null робить get() == null однозначним індикатором відсутності ключа.
Internal Implementation
В HashMap null-значення зберігається як звичайний запис:
// putVal:
Node<K,V> e;
// ...
e.value = value; // value може бути null
Значення не бере участі в хешуванні — жодних обмежень.
Optional як альтернатива
Map<String, Optional<Value>> map = new HashMap<>();
// Явний вираз намірів:
map.put("key", Optional.empty()); // Навмисно "нічого"
map.put("key", Optional.of(val)); // Є значення
// Отримання:
Optional<Value> opt = map.getOrDefault("key", Optional.empty());
Overhead: додатковий об’єкт Optional на кожен запис.
Null Object Pattern
class User {
static final User EMPTY = new User("", 0);
// ...
}
Map<String, User> map = new HashMap<>();
map.put("missing", User.EMPTY); // Замість null
Zero overhead, явна семантика, NPE-safe.
Memory Implications
null-значення не займає додаткової пам’яті:
- Посилання на null = 0 байт (просто відсутність об’єкта)
- Node все одно створюється (заголовок + hash + key + value ref + next)
Production Diagnostics
Ознаки проблем з null-значеннями:
- NPE в chain-методах:
map.get(key).method() - Логічна помилка: null трактується як “немає даних”
- В БД: null-значення в Map → null в колонці (може порушити constraint)
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- HashMap допускає будь-яку кількість null-значень
- Проблема: get(key) = null неоднозначно — ключа немає АБО значення = null?
- Рішення: containsKey() для однозначної перевірки
- ConcurrentHashMap забороняє null-значення — щоб усунути ambiguity в check-then-act
- Null Object Pattern: створити константу EMPTY замість null
- Optional як альтернатива: Map<String, Optional
> — явний вираз намірів
Часті уточнюючі запитання:
- Чому CHM забороняє null-значення? — Liveness Problem: get=null має однозначно означати «немає ключа» для атомарних операцій
- Як відрізнити null від відсутності ключа? — containsKey()
- Чим null-значення відрізняється від null-ключа? — null-значень багато, null-ключ лише один
- Optional vs Null Object? — Optional = додатковий об’єкт (overhead), Null Object = zero overhead
Червоні прапорці (НЕ говорити):
- «HashMap забороняє null-значення» — допускає
- «ConcurrentHashMap допускає null-значення» — ні, NPE
- «null-значення = 0 байт» — Node все одно створюється (~32-48 байт)
Пов’язані теми:
- [[24. Чи можна зберігати null ключ в HashMap]]
- [[23. Що таке ConcurrentHashMap і чим він відрізняється від HashMap]]