Java中HashMap与HashTable的区别

Java中 HashMap与HashTable的区别

HashMapHashtable 是 Java 中两种用于存储键值对的数据结构,它们之间有几个关键的区别:

  1. 线程安全性:
    • HashMap:是非线程安全的。多个线程可以同时修改 HashMap,这可能导致不同步的访问。
    • Hashtable:是线程安全的。Hashtable 中的方法都是同步的,适用于多线程环境。

我们可以看到 Hashtable 源码中,很多就被 synchronized(同步的) 修饰符修饰,所以可以在多线程环境中安全运行。

public synchronized boolean contains(Object value) { ... }
public synchronized boolean containsKey(Object key) { ... }
public synchronized V get(Object key) { ... }
public synchronized V put(K key, V value) { ... }
public synchronized V remove(Object key) { ... }
... ...

HashMap 源码如下

public boolean containsKey(Object key) { ... }
public V put(K key, V value) { ... }
public V get(Object key) { ... }
public void putAll(Map<? extends K, ? extends V> m) { ... }
public V remove(Object key) { ... }
... ... 

我们可以看到,在 HashMap 中,没有 synchronized 修饰,所以在多线程的情况下可能会产生问题,不建议在多线程使用。如果我们不需要同步且只是在单线程的情况下,那么 HashMap 的性能是要比 Hashtable 的性能要好的。从源码中我们可以看出,HashMap 大量使用的是位运算,而不是简单的乘除。

  1. 性能:

    • HashMap:由于不是线程安全的,因此在单线程环境下通常比 Hashtable 性能更好。
    • Hashtable:由于需要维护同步,性能通常较差。在多线程环境中,可能需要使用 Hashtable
  2. null 键值:

    • HashMap:允许键和值都为 null,可以存储一个键为 null 的条目。
    • Hashtable:不允许键和值为 null,如果尝试插入 null 键或值,将抛出 NullPointerException
  3. 继承关系:

    • HashMap:继承自 AbstractMap 类,实现了 Map 接口。
    • Hashtable:继承自 Dictionary 类,实现了 Map 接口,并被认为是遗留类。
    • HashMap是继承自AbstractMap类,而Hashtable是继承自Dictionary类。不过它们都实现了同时实现了mapCloneable(可复制)、Serializable(可序列化)这三个接口。
public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
    ... ...
}
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    ... ...
}
  1. 迭代器:

    • HashMap:通过迭代器或者 Java 8 中的 forEach 进行遍历。Iterator
    • Hashtable:通过迭代器进行遍历。Enumeration
  2. 初始容量和加载因子:

    • HashMap:可以设置初始容量和加载因子,以调整性能。默认初始容量为 16,加载因子为 0.75。
    • Hashtable:默认初始容量为 11,加载因子为 0.75。加载因子是调整表的大小的阈值。

当达到临界值时,扩容机制不同,假设两者的当前容量都为 n,那么扩容后,Hashtable 扩容为 2n + 1,而 HashMap 扩容为 2n

临界值(threshold) = 容量(capacity) * 负载因子(loadFactor)

这里我们简单介绍一下什么是负载因子?

当我们不断向 HashMap 或者 Hashtable 中 put 元素时,可能会产生大量的哈希冲突,所以需要自动扩容。

以下是 Hashtable 的扩容源码。同样,当我们利用 HashMap 进行 put 元素到临界值时,HashMap
同样也会扩容。不论是在 Hashtable 还是在 HashMap,当对其进行扩容后,都必须将原先的元素分配到新的地方存储。

loadFactor 负载因子,表示 Hashtable 或者 HashMap 容量的饱满程度,默认的负载因子是 0.75,也就是说在 Hashtable 或者 HashMap 中元素容量到达 3/4 的时候开始扩容。

这里我们就会有疑惑,底层的数据结构不是 数组 + 链表 吗,根据 hash 算法可以一直链下去,也就是所谓的拉链法,那为什么还需要扩容呢?

这里就跟我们上面所说到的 哈希冲突 有关~

底层的数据结构 数组 + 链表 可以很好的化解一些哈希冲突问题,但是如果我们不进行扩容,我们 put 的元素越来越多,那么发生哈希冲突的概率就会越来越大,会从一开始的数组链表最后退化为链表,这时就导致查询速度降低。

一开始,元素不多的情况下,如图所示:

  1. Fail-Fast 机制:
    • HashMap:支持 fail-fast 机制,当在迭代过程中修改了 HashMap 结构(除非通过迭代器自身的 remove 方法),将抛出 ConcurrentModificationException 异常。
    • Hashtable:也支持 fail-fast 机制。

Java中HashMap与HashTable的区别_第1张图片
后来,元素慢慢增多,但是不进行扩容,如图所示

Java中HashMap与HashTable的区别_第2张图片
那么,我们应该如何改变这种情况呢?

  • 优化 hash 算法(已优化)
  • 扩大容量,降低碰撞概率

所以我们需要扩大容量,并在一个特定的临界值下。这样 loadFactor 负载因子就产生了~

那么负载因子的初始值为什么不是其他数值,而是 0.75 呢?

我们可以想到,负载因子不能太大,也不能太小。如果太大,会增加哈希冲突,如果太小,会浪费内存空间,所以经过官方的数学计算和统计,得到了一个在空间和成本上都比较权衡的数据,也就是
0.75,一般情况下,我们不建议去修改这个负载因子的默认值~

在计算 key 的 hash 值时,Hashtable 直接使用的是 hascode( ),HashMap 使用的是自定义的 hash( ) 算法

    public synchronized V put(K key, V value) { 
        ... ...
        int hash = key.hashCode();
        ... ...
    }

HashMap 源码如下

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
 
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

综上所述,一般情况下,推荐使用 HashMap,因为它的性能更好且在大多数应用场景中足够。如果需要线程安全性,也可以考虑使用 ConcurrentHashMap 或者在特殊情况下使用 Hashtable

你可能感兴趣的:(#,Java的零星知识,java,python,开发语言)