Питання 25 · Розділ 10

Чи можна зберігати null значення в HashMap?

get(key) повертає null у двох випадках:

Мовні версії: English Russian Ukrainian

🟢 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 у двох випадках:

  1. Ключа немає в мапі
  2. Ключ є, але його значення = 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

  1. Уникайте null-значень — використовуйте Optional або Null Object
  2. Завжди перевіряйте containsKey() перед обробкою null
  3. Документуйте — чи може ваш метод повернути null з Map

Типові помилки

  1. NPE при використанні значення:
    map.get("key").toString(); // NPE, якщо значення = null
    
  2. Плутанина з відсутністю ключа — логічна помилка

🔴 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]]