Java Hashtable
Устаревший синхронизированный Hashtable в Java, почему его заменяют HashMap и ConcurrentHashMap, и когда он встречается.
Hashtable<K, V> — это оригинальная хеш-структура в Java, появившаяся ещё в JDK 1.0 в 1996 году — за два года до появления Collections Framework. Когда в JDK 1.2 были добавлены Map, HashMap и остальные коллекции, Hashtable был адаптирован для реализации Map, однако его особенности сохранились: каждый метод синхронизирован (synchronized), ни ключи, ни значения не могут быть null, а публичный API включает методы до появления Collections (elements(), keys()), предшествующие Iterator.
В новом коде вы почти никогда не захотите его использовать. Эта глава нужна для того, чтобы вы узнавали класс при встрече, понимали, почему он до сих пор существует, и знали, что использовать вместо него.
Почему он всё ещё существует
Три причины:
- Обратная совместимость. Несколько классов стандартной библиотеки возвращают
Hashtable—System.getProperties()возвращает экземплярProperties, который расширяетHashtable<Object, Object>. Некоторые старые JNDI API (InitialContext(Hashtable)) принимают его в качестве аргумента. - Существующий код. Любая кодовая база старше примерно 2005 года может всё ещё содержать
Hashtableв местах, куда никто не дошёл с рефакторингом. - Ошибочная привычка. Он встречается в интервью и учебниках, и начинающие разработчики иногда используют его с мыслью «хочу потокобезопасную Map» — не зная о
ConcurrentHashMap.
Чем он отличается от HashMap
Hashtable и HashMap — оба хеш-таблицы с цепочками, оба реализуют Map<K, V>, и оба имеют O(1) в среднем для get/put/remove. Различия:
| Характеристика | Hashtable | HashMap |
|---|---|---|
| Потокобезопасность | каждый метод synchronized на всей таблице | не потокобезопасен |
Ключ null | запрещён (NullPointerException) | допускается один |
Значение null | запрещено (NullPointerException) | допускаются несколько |
| Порядок итерации | не определён | не определён |
| Начальная ёмкость | 11 | 16 |
| Рост ёмкости | 2*old + 1 (нечётные размеры, медленное деление по модулю) | удваивается до следующей степени двойки |
| API до Collections | перечисления elements(), keys() | отсутствует |
| Древовидные корзины в Java 8 | нет | да — корзины становятся деревьями при более чем 8 записях |
| Итераторы с отказом при изменении | да, с версии 1.2 | да |
clone() | да (поверхностное копирование) | да (поверхностное копирование) |
Синхронизация — главное отличие и основная причина медлительности Hashtable: каждое чтение и каждая запись захватывает один и тот же замок на всю таблицу. Многопоточная программа, в которой два потока выполняют только get к Hashtable, сериализуется — они по очереди захватывают замок.
Почему synchronized на каждом методе — это не настоящая потокобезопасность
Удивительно распространённая ошибка: разработчики видят «каждый метод синхронизирован» и думают, что Hashtable делает их многопоточный код корректным. Это не так. Составные операции по-прежнему подвержены гонкам:
if (!table.containsKey(key)) { // synchronized
table.put(key, computeValue()); // synchronized — but separate lock acquisition
}Между двумя вызовами другой поток может выполнить put с тем же ключом. Оба потока видят, что containsKey возвращает false, оба вычисляют значение, оба делают put. Получается два вычисления, и побеждает неправильное значение.
Решение сегодня — не исправлять Hashtable, а использовать ConcurrentHashMap, в который встроены атомарные составные операции: putIfAbsent, computeIfAbsent, merge, replace(k, old, new). Они сами захватывают нужные замки и выполняют проверку-и-установку как единую операцию.
Что использовать вместо
Схема принятия решений, когда вам хочется написать new Hashtable<>():
- Однопоточный код, нужна
Map→HashMap. Всё. Накладные расходы наsynchronizedвHashtable— чистые потери без какой-либо выгоды. - Многопоточный код, нужна
Map→ConcurrentHashMap. Блокировка по сегментам (без блокировки при чтении в современных JDK), нет глобального замка, атомарные составные операции, значительно лучшая масштабируемость. - Многопоточный код, каждая операция должна быть атомарной с остальными →
Collections.synchronizedMap(new HashMap<>()). Та же поведение с одним замком, что уHashtable, но совместимость с современным Collections API. Всё равно хужеConcurrentHashMap, если последний подходит. - Вы работаете с API, требующим
Hashtable(Properties, JNDI) → используйтеHashtable, потому что этого требует API; не создавайте параллельную структуру.
Особенности до Collections
Hashtable появился до Iterator и предоставляет Enumeration<K> вместо него:
Enumeration<String> keys = table.keys();
while (keys.hasMoreElements()) {
System.out.println(keys.nextElement());
}У Enumeration есть только hasMoreElements() и nextElement() — без remove(). Адаптация в 1.2 добавила keySet(), entrySet() и values() из Map, и по ним можно итерироваться обычным Iterator. Но поскольку оба API существуют на одном объекте, в реальном коде встречаются оба стиля. Предпочитайте вид через Map — это язык, который вы уже знаете.
Практический пример: Hashtable, почему он не принимает null и от какой гонки он не защищает
Программа ниже демонстрирует видимые отличия от HashMap — синхронизированные методы, запрет null, устаревшее перечисление — и показывает гонку типа «проверь, затем действуй», от которой синхронизация Hashtable не защищает.
Что нужно вынести из запуска:
- Базовый API — это API интерфейса
Map, поэтомуHashtableвыглядит какHashMapпри простом использовании. Одинаковые результаты, но медленнее. - Null запрещён с обеих сторон — это единственная
Mapв семействе JDK, которая запрещает null значения наравне с null ключами. - Счётчик на
Hashtableвыдаёт неправильный результат. Каждый метод синхронизирован, ноgetи затемput— это две отдельные атомарные операции, а не одна. Потоки вступают в гонку между ними и теряют обновления. - Версия
ConcurrentHashMapсmergeработает корректно и быстро. Это правильный инструмент для «потокобезопасной Map» в 2026 году.
Что дальше
У Hashtable есть один потомок, который вам действительно пригодится: Properties — контейнер конфигурации, лежащий в основе System.getProperties() и формата .properties файлов. Он узкоспециализирован и удобен в использовании; это следующая глава и последняя глава про «структуры данных» в этой части книги, после которой мы перейдём к итерации, сортировке и статическим утилитным методам.