В чём разница между HashMap и Hashtable
HashMap и Hashtable — это две реализации ассоциативного массива (интерфейса Map), хранящие данные в виде пар «ключ-значение». Однако между ними существуют фундаментальные архите...
🟢 Junior Level
HashMap и Hashtable — это две реализации ассоциативного массива (интерфейса Map), хранящие данные в виде пар «ключ-значение». Однако между ними существуют фундаментальные архитектурные и исторические различия:
| Характеристика | HashMap |
Hashtable |
|---|---|---|
| Потокобезопасность | ❌ Не потокобезопасна (высокая скорость) | ✅ Потокобезопасна (методы synchronized) |
Поддержка null |
✅ Разрешает 1 null-ключ и любые null-значения |
❌ Запрещает null (выбрасывает NPE) |
| Статус в JDK | Современный стандарт (Java 1.2+) | Устаревший класс (Legacy с Java 1.0) |
| Производительность | Высокая (без блокировок, битовая индексация) | Низкая (блокировка всей таблицы на каждый вызов) |
| Трификация коллизий | Превращение бакетов в RB-Tree ($O(\log N)$) | Только связный список ($O(N)$) |
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class MapVsHashtableDemo {
public static void main(String[] args) {
// Современный выбор
Map<String, String> hashMap = new HashMap<>();
hashMap.put(null, "Allowed"); // OK
// Устаревший класс (Legacy)
Map<String, String> hashtable = new Hashtable<>();
// hashtable.put(null, "Error"); // Выбросит NullPointerException!
}
}
Аналогия:
Hashtable— это старый механический сейф, к которому пускают строго по одному человеку: пока один смотрит содержимое, все остальные ждут снаружи в очереди.HashMap— это открытый современный стеллаж без очередей для одного сотрудника (илиConcurrentHashMap— для одновременного параллельного доступа многих людей к разным полкам).
🟡 Middle Level
Детальная сравнительная таблица
| Критерий | HashMap |
Hashtable |
|---|---|---|
| Иерархия классов | Наследует AbstractMap |
Наследует устаревший класс Dictionary |
| Начальная ёмкость (Default) | $16$ ($2^4$, степень двойки) | $11$ (нечетное/простое число) |
| Формула ресайза | $\text{newCap} = \text{oldCap} \ll 1$ ($2 \times$) | $\text{newCap} = (\text{oldCap} \ll 1) + 1$ |
| Вычисление индекса | (n - 1) & hash (битовая маска, 1 такт) |
(hash & 0x7FFFFFFF) % n (деление по модулю) |
| Perturbation Function | (h = key.hashCode()) ^ (h >>> 16) |
Отсутствует (берется key.hashCode()) |
| Итераторы | Iterator (поведение Fail-Fast) |
Iterator (Fail-Fast) + Enumeration (Fail-Safe) |
| Структура бакета | Связный список $\to$ Красно-чёрное дерево | Исключительно односвязный список |
Почему Hashtable считается устаревшим антипаттерном?
- Грубая синхронизация (Coarse-Grained Locking):
Все публичные методы
Hashtableпомечены ключевым словомsynchronized:public synchronized V get(Object key) { ... } public synchronized V put(K key, V value) { ... }Это означает, что на всю таблицу существует один общий монитор (
this). Даже параллельные операции чтения (get) блокируют друг друга, выстраивая потоки в искусственную очередь. - Отсутствие трификации: При атаке Hash Flooding или неудачной хеш-функции
Hashtableдеградирует до $O(N)$ без шансов оптимизации в дерево. - Современная замена:
- Для однопоточного кода:
HashMap. - Для многопоточного кода:
ConcurrentHashMap(работает в десятки раз быстрее за счет CAS и побакетных блокировок). - Если строго требуется блокировка всей таблицы:
Collections.synchronizedMap(new HashMap<>()).
- Для однопоточного кода:
🔴 Senior Level
Индексация и математика ресайза: Modulo vs Bitwise AND
Разработчики Hashtable (1995 г.) и HashMap (1998 г.) выбрали принципиально разные подходы к вычислению индекса:
1. Подход Hashtable:
int index = (hash & 0x7FFFFFFF) % tab.length;
- Операция
& 0x7FFFFFFFобнуляет старший знаковый бит, переводя отрицательныйhashCode()в положительный диапазон[0, Integer.MAX_VALUE]. - Операция деления по модулю
%(idiv) является одной из самых медленных инструкций микропроцессора (15–40 тактов). - Формула ресайза
(oldCapacity << 1) + 1целенаправленно генерирует нечетные числа ($11 \to 23 \to 47 \to 95$). Нечетные числа уменьшают вероятность коллизий при использовании сырогоhashCode().
2. Подход HashMap:
int index = (table.length - 1) & hash;
- Побитовое «И» выполняется за 1 такт процессора.
- Обязывает емкость быть строго степенью двойки ($2^k$).
- Чтобы устранить проблему коллизий из-за степени двойки, применяется предварительное перемешивание битов:
(h = key.hashCode()) ^ (h >>> 16).
Итераторы: Enumeration против Iterator
В Hashtable сосуществуют два механизма обхода:
- Устаревший
Enumeration(hashtable.elements(),hashtable.keys()):- Не имеет метода
remove(). - Не является fail-fast: он не проверяет поле
modCount! Если другой поток модифицируетHashtableво время работыEnumeration, исключениеConcurrentModificationExceptionвыброшено не будет, но результат итерации может содержать пропущенные или повторно обойденные элементы.
- Не имеет метода
- Коллекционный
Iterator(hashtable.keySet().iterator()):- Был добавлен в Java 1.2 при адаптации
Hashtableпод интерфейсMap. Является полноценным fail-fast итератором.
- Был добавлен в Java 1.2 при адаптации
Наследие java.util.Properties
Почему Hashtable до сих пор не удалена из JDK?
Главная причина обратной совместимости — класс java.util.Properties (используемый для чтения конфигурационных файлов .properties), который исторически объявлен как:
public class Properties extends Hashtable<Object, Object>
Наследование Properties от Hashtable признано одной из самых известных ошибок проектирования стандартной библиотеки Java (нарушение принципа композиции в пользу наследования). Из-за этого в Properties можно случайно положить произвольные нестроковые объекты через put(key, value), что ломает методы getProperty(String key).
4 Tricky Questions
1. Если обернуть HashMap через Collections.synchronizedMap(map), станет ли она идентична Hashtable?
Ответ: Нет, между ними останется ряд ключевых отличий:
- Поддержка
null:Collections.synchronizedMap(new HashMap<>())по-прежнему разрешает одинnull-ключ и множествоnull-значений, тогда какHashtableвыброситNullPointerException. - Трификация: Обернутая
HashMapна Java 8+ при коллизиях продолжит превращать бакеты в красно-чёрные деревья ($O(\log N)$), аHashtableвсегда остается односвязным списком ($O(N)$). - Производительность индексации:
HashMapиспользует побитовую маску(n - 1) & hashвместо тяжелого деления%уHashtable. - Объект блокировки: В
Hashtableсинхронизация идет поthis(самому объекту таблицы). ВsynchronizedMapможно явно передать кастомный объект мьютекса вторым аргументом конструктора.
2. Почему в Hashtable вызов put(key, null) приводит к NullPointerException, хотя в методе put первая строчка — if (value == null) throw new NullPointerException();, а для ключа явной проверки нет?
Ответ: Для ключа явная проверка не требуется, потому что выброс NPE гарантирован первой же строчкой работы с ключом:
public synchronized V put(K key, V value) {
if (value == null) {
throw new NullPointerException(); // Явная проверка значения
}
// ...
int hash = key.hashCode(); // Неявный NPE, если key == null!
}
Если передать key == null, вызов key.hashCode() автоматически выбросит JVM-исключение NullPointerException. Для значения же проверка сделана явным if, так как у значения hashCode() внутри метода не вызывается.
3. Почему начальная ёмкость Hashtable равна 11, а у HashMap — 16?
Ответ:
- В
Hashtableиндекс бакета вычисляется делением по модулюhash % capacity. Чтобы минимизировать число коллизий при использовании простого деления (особенно если входные хеш-коды кратны какому-то числу), размер таблицы должен быть простым или нечетным числом. Число 11 — наименьшее удобное простое число близкое к 10. - В
HashMapдля вычисления индекса используется побитовое «И»(capacity - 1) & hash. Эта формула корректно распределяет элементы по всем бакетам только в том случае, если ёмкость является точной степенью двойки ($2^k$). Число 16 ($2^4$) было выбрано как оптимальная начальная степень двойки.
4. Будет ли итератор hashtable.elements() выбрасывать ConcurrentModificationException при параллельном добавлении элементов?
Ответ:
Нет, не будет.
Метод hashtable.elements() возвращает устаревший интерфейс Enumeration. Его внутренняя реализация Enumerator не отслеживает значение счетчика структурных изменений modCount и не имеет механизма fail-fast.
В результате обход через Enumeration выполнится без исключений, но поведение будет недетерминированным: итератор может вернуть элементы в неконсистентном состоянии, пропустить вновь добавленные записи или вернуть дубликаты.
🎯 Шпаргалка для интервью
- Главное отличие:
Hashtable— устаревший синхронизированный класс (Java 1.0);HashMap— быстрая несинхронизированная современная коллекция (Java 1.2+). - Синхронизация: В
Hashtableвсе методыsynchronized(блокировка всей таблицы). В современном коде для многопоточности используетсяConcurrentHashMap. - Поддержка
null:HashMapразрешаетnull(1 ключ, любые значения).Hashtableзапрещаетnullвезде (NPE). - Индексация:
HashMapиспользует побитовую маску(n - 1) & hash(1 такт);Hashtableиспользует делениеhash % capacity(медленно). - Иерархия:
HashMapнаследуетAbstractMap,Hashtableнаследует устаревшийDictionary. - Почему
Hashtableеще жива: Ради обратной совместимости, в частности из-за классаjava.util.Properties extends Hashtable.