Java中的HashMap与Hashtable:理解它们的差异

36 阅读4分钟

HashMap 和 Hashtable 是Java中用于存储键值对的集合类,但它们之间有一些关键区别。

  1. 线程安全性

    • HashMap:不是线程安全的。如果多个线程同时访问一个HashMap而没有正确的同步,会导致数据不一致。在源码中,HashMap的主要方法(如 put, get)都没有使用 synchronized 关键字。
    • Hashtable:是线程安全的,所有的关键方法都使用了 synchronized 关键字来确保线程安全。这可以在源码中看到每个方法都是同步的。多个线程可以安全地共享一个Hashtable实例,但由于使用了同步机制,其性能比HashMap差。
  2. 性能

    • HashMap:由于不需要同步,其性能通常比Hashtable好。
    • Hashtable:因为每个方法都是同步的,所以开销较大,性能相对较低。
  3. 允许null值

    • HashMap:允许一个null键和多个null值。在源码中,put 方法会单独处理 keynull 的情况。
    • Hashtable:不允许null键或null值。在源码中会直接通过 Objects.requireNonNull 方法进行校验,抛出 NullPointerException
  4. 继承关系

    • HashMap:继承自AbstractMap
    • Hashtable:继承自Dictionary,历史上较久。
  5. 初始容量和增长因子

    • HashMap:可以指定初始容量和加载因子。HashMap 的默认初始容量是16,加载因子是0.75。扩容时,容量会翻倍。
    • Hashtable:默认初始容量是11,加载因子是0.75。扩容是直接扩大一倍加一。
  6. 数据结构

    • HashMapHashtable 都是基于哈希表的数据结构实现的。两者内部使用数组加链表的方式来处理哈希冲突。

源码分析

HashMap 中的 put 方法(简化版):

public V put(K key, V value) {
    // 处理key为null的情况
    if (key == null)
        return putForNullKey(value);
    // 计算key的hash值,确定存储位置
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    // 遍历链表,检查是否存在相同的key
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            return oldValue;
        }
    }
    // 如果不存在相同的key,则新增一个节点
    addEntry(hash, key, value, i);
    return null;
}

HashMap 中,putForNullKey 方法专门用于处理键为 null 的情况。由于 HashMap 允许使用 null 作为键,它需要特别处理这种情况,以避免在哈希计算和索引时出现问题。

以下是 putForNullKey 方法的基本实现逻辑(源码简化):

private V putForNullKey(V value) {
    // 遍历链表,寻找key为null的节点
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            // 如果找到,则更新值
            V oldValue = e.value;
            e.value = value;
            return oldValue;
        }
    }
    // 如果没有找到,则创建新节点,并将其放在链表的头部
    addEntry(0, null, value, 0);
    return null;
}

说明:

  1. 存储位置

    • 当键为 null 时,HashMap 将其总是存储在数组的第一个位置,即 table[0]。这是因为 null 键的哈希值固定为0。
  2. 遍历链表

    • putForNullKey 会遍历位于 table[0] 的链表,检查是否已有节点的键为 null
  3. 更新或添加

    • 如果找到已有的键为 null 的节点,它将更新该节点的值。
    • 如果没有找到,则调用 addEntry 方法在 table[0] 位置添加一个新的节点。

小结:

这个方法说明了 HashMapnull 键的特殊处理机制。通过将 null 键的所有条目存储在数组的第一个位置,HashMap 简化了对 null 键的索引和管理。


Hashtable 中的 put 方法(简化版):

public synchronized V put(K key, V value) {
    // 不允许null键
    if (key == null || value == null) {
        throw new NullPointerException();
    }
    // 计算key的hash值,确定存储位置
    int hash = hash(key);
    int index = (hash & 0x7FFFFFFF) % table.length;
    // 遍历链表,检查是否存在相同的key
    for (Entry<K,V> e = table[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            V old = e.value;
            e.value = value;
            return old;
        }
    }
    // 如果不存在相同的key,则新增一个节点
    addEntry(hash, key, value, index);
    return null;
}

结论

  • 在源码中可以看到,HashMap 的关键方法没有使用同步机制,而 Hashtable 中几乎每个方法都有 synchronized 修饰。这是两者性能差异的根本原因。
  • 由于 Hashtable 使用了更粗粒度的同步机制,推荐在需要并发操作时使用 ConcurrentHashMap 代替。

代码说明

HashMap应用场景: 适用于单线程环境或不要求线程安全的场景。例如,缓存数据在单线程应用中的使用。

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>();
        map.put("key1", "value1");
        map.put("key2", "value2");
        map.put("key3", "value3");

        System.out.println("HashMap: " + map);

        // 允许null键和值
        map.put(null, "nullValue");
        System.out.println("HashMap with null key: " + map);
    }
}

Hashtable应用场景: 适用于简单的多线程环境,或者不介意使用较老旧类的遗留系统。

import java.util.Hashtable;

public class HashtableExample {
    public static void main(String[] args) {
        Hashtable<String, String> table = new Hashtable<>();
        table.put("keyA", "valueA");
        table.put("keyB", "valueB");
        table.put("keyC", "valueC");

        System.out.println("Hashtable: " + table);

        // 不允许null键和值,下面的代码会抛出NullPointerException
        // table.put(null, "nullValue");
    }
}

如果你在开发一个现代的Java应用,通常会选择使用HashMap,因为它性能更好并且更灵活。如果需要线程安全的Map,建议使用ConcurrentHashMap而不是Hashtable,因为ConcurrentHashMap提供更好的并发性能。