W3docs

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.

В новом коде вы почти никогда не захотите его использовать. Эта глава нужна для того, чтобы вы узнавали класс при встрече, понимали, почему он до сих пор существует, и знали, что использовать вместо него.

Почему он всё ещё существует

Три причины:

  1. Обратная совместимость. Несколько классов стандартной библиотеки возвращают HashtableSystem.getProperties() возвращает экземпляр Properties, который расширяет Hashtable<Object, Object>. Некоторые старые JNDI API (InitialContext(Hashtable)) принимают его в качестве аргумента.
  2. Существующий код. Любая кодовая база старше примерно 2005 года может всё ещё содержать Hashtable в местах, куда никто не дошёл с рефакторингом.
  3. Ошибочная привычка. Он встречается в интервью и учебниках, и начинающие разработчики иногда используют его с мыслью «хочу потокобезопасную Map» — не зная о ConcurrentHashMap.

Чем он отличается от HashMap

Hashtable и HashMap — оба хеш-таблицы с цепочками, оба реализуют Map<K, V>, и оба имеют O(1) в среднем для get/put/remove. Различия:

ХарактеристикаHashtableHashMap
Потокобезопасностькаждый метод synchronized на всей таблицене потокобезопасен
Ключ nullзапрещён (NullPointerException)допускается один
Значение nullзапрещено (NullPointerException)допускаются несколько
Порядок итерациине определённе определён
Начальная ёмкость1116
Рост ёмкости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<>():

  • Однопоточный код, нужна MapHashMap. Всё. Накладные расходы на synchronized в Hashtable — чистые потери без какой-либо выгоды.
  • Многопоточный код, нужна MapConcurrentHashMap. Блокировка по сегментам (без блокировки при чтении в современных 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 не защищает.

java— editable, runs on the server

Что нужно вынести из запуска:

  • Базовый API — это API интерфейса Map, поэтому Hashtable выглядит как HashMap при простом использовании. Одинаковые результаты, но медленнее.
  • Null запрещён с обеих сторон — это единственная Map в семействе JDK, которая запрещает null значения наравне с null ключами.
  • Счётчик на Hashtable выдаёт неправильный результат. Каждый метод синхронизирован, но get и затем put — это две отдельные атомарные операции, а не одна. Потоки вступают в гонку между ними и теряют обновления.
  • Версия ConcurrentHashMap с merge работает корректно и быстро. Это правильный инструмент для «потокобезопасной Map» в 2026 году.

Что дальше

У Hashtable есть один потомок, который вам действительно пригодится: Properties — контейнер конфигурации, лежащий в основе System.getProperties() и формата .properties файлов. Он узкоспециализирован и удобен в использовании; это следующая глава и последняя глава про «структуры данных» в этой части книги, после которой мы перейдём к итерации, сортировке и статическим утилитным методам.

Практика

Практика
Старший разработчик просит вас убрать `Hashtable<String, Integer>` из многопоточного счётчика, который делает `int n = t.get(k); t.put(k, n + 1);`. Какая замена правильная?
Старший разработчик просит вас убрать `Hashtable<String, Integer>` из многопоточного счётчика, который делает `int n = t.get(k); t.put(k, n + 1);`. Какая замена правильная?
Was this page helpful?