Avatar ·

В чем разница между HashMap и Hashtable в Java?

Всем привет! Я только начал изучать Java Collections Framework и запутался в двух похожих классах — HashMap и Hashtable. Они оба реализуют интерфейс Map и, кажется, делают одно и то же: хранят пары ключ-значение. Но я слышал, что между ними есть важные отличия, и Hashtable считается устаревшим. Я написал небольшой тестовый код, но не вижу разницы: ```java import java.util.*; public class TestMap { public static void main(String[] args) { // HashMap Map hashMap = new HashMap<>(); hashMap.put("key1", "value1"); hashMap.put(null, "nullValue"); // работает System.out.println("HashMap: " + hashMap); // Hashtable Map hashtable = new Hashtable<>(); hashtable.put("key1", "value1"); // hashtable.put(null, "nullValue"); // выдает NullPointerException? System.out.println("Hashtable: " + hashtable); } } ``` Почему Hashtable не позволяет использовать null в качестве ключа? В чем разница в потокобезопасности? И что значит "устаревший"? Может, стоит всегда использовать HashMap? Объясните, пожалуйста, подробно, с примерами. Заранее спасибо! <<>>

Основные различия между HashMap и Hashtable

Отличный вопрос! Действительно, на первый взгляд HashMap и Hashtable очень похожи, но у них есть ряд принципиальных отличий, которые важно понимать при выборе подходящей структуры данных.

1. Потокобезопасность (синхронизация)

Самое главное отличие: Hashtable — потокобезопасен, а HashMap — нет. Это значит, что методы Hashtable синхронизированы, и несколько потоков могут безопасно работать с одним экземпляром Hashtable без дополнительной синхронизации. HashMap же требует внешней синхронизации при использовании в многопоточной среде.

Пример, демонстрирующий проблему с HashMap:


import java.util.*;
import java.util.concurrent.*;

public class ThreadSafetyDemo {
    public static void main(String[] args) throws InterruptedException {
        // HashMap — не потокобезопасен
        Map<String, Integer> hashMap = new HashMap<>();
        
        // Hashtable — потокобезопасен
        Map<String, Integer> hashtable = new Hashtable<>();
        
        ExecutorService executor = Executors.newFixedThreadPool(10);
        
        // Запускаем 1000 задач, которые добавляют элементы в HashMap
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executor.submit(() -> hashMap.put("key" + finalI, finalI));
        }
        
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        
        // В многопоточной соде размер HashMap может быть меньше 1000 из-за race condition
        System.out.println("HashMap size: " + hashMap.size()); // может быть < 1000
    }
}

2. Поддержка null

Второе важное отличие: HashMap допускает null в качестве ключа (только один null-ключ) и любое количество null-значений. Hashtable не допускает null ни в качестве ключа, ни в качестве значения — при попытке вставить null будет выброшено исключение NullPointerException.


Map<String, String> hashMap = new HashMap<>();
hashMap.put(null, "nullKey");  // работает
hashMap.put("key1", null);     // работает

Map<String, String> hashtable = new Hashtable<>();
// hashtable.put(null, "value"); // NullPointerException!
// hashtable.put("key", null);   // NullPointerException!

3. Производительность и итерация

Поскольку Hashtable синхронизирует все свои методы, это создает накладные расходы. HashMap работает быстрее в однопоточных приложениях, так как не тратит время на синхронизацию.

  • HashMap использует более эффективный механизм итерации (fail-fast итератор)
  • Hashtable использует enumeration (устаревший механизм), хотя тоже поддерживает итераторы
  • В Java 8+ HashMap оптимизирован: при большом количестве коллизий использует деревья вместо связных списков

4. Устаревший статус и рекомендации

Hashtable считается устаревшим классом (legacy class). Он появился еще в Java 1.0, до Java Collections Framework. Начиная с Java 2 (1998 год), рекомендуется использовать:

  • HashMap — для однопоточных приложений
  • ConcurrentHashMap — для многопоточных приложений (более производительная альтернатива Hashtable)
  • Collections.synchronizedMap() — если нужно синхронизировать HashMap

Пример использования ConcurrentHashMap:


import java.util.concurrent.*;

Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("key", 100);
// Потокобезопасен, но работает быстрее Hashtable

5. Размер и емкость

Есть небольшие отличия в начальной емкости и механизме расширения:

  • HashMap: начальная емкость 16, коэффициент загрузки 0.75
  • Hashtable: начальная емкость 11, коэффициент загрузки 0.75
  • HashMap удваивает размер при расширении, Hashtable — удваивает и добавляет 1

Итоговая таблица сравнения

ХарактеристикаHashMapHashtable
ПотокобезопасностьНетДа (синхронизирован)
null ключи/значенияДа (1 null ключ)Нет
ПроизводительностьВысокаяНизкая (из-за синхронизации)
СтатусАктуальныйУстаревший (legacy)
РекомендацияДля однопоточных задачИспользуйте ConcurrentHashMap
<<>>

Дополнительные аспекты и практические советы

Коллега выше отлично описал основные различия. Я хочу добавить несколько практических моментов, которые помогут в реальной разработке.

Как безопасно использовать HashMap в многопоточном коде

Если вам нужно использовать HashMap в многопоточной среде, есть два подхода:


// Вариант 1: Оборачиваем в synchronizedMap
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
// Все методы теперь синхронизированы

// Вариант 2: Используем ConcurrentHashMap (рекомендуется)
Map<String, String> concurrentMap = new ConcurrentHashMap<>();
// Более производительный, использует сегментирование блокировок

Историческая справка

Hashtable существовал еще до появления Java Collections Framework. Когда в Java 2 появились коллекции (List, Set, Map), Hashtable был адаптирован для работы с ними (добавлена реализация интерфейса Map), но его старая синхронизированная природа осталась. HashMap был создан как современная, несинхронизированная альтернатива.

Когда может пригодиться Hashtable?

В современной разработке Hashtable практически не используется. Однако вы можете встретить его в старом коде (legacy code). Если вы поддерживаете такой код, помните:

  • Hashtable не позволяет null — это может вызвать неожиданные NPE
  • Итерация по Hashtable через Enumeration может быть медленнее, чем через Iterator
  • При рефакторинге старого кода заменяйте Hashtable на ConcurrentHashMap

Практический совет: В 99% случаев используйте HashMap для однопоточных приложений и ConcurrentHashMap для многопоточных. Hashtable оставьте только для чтения legacy кода.