转自:HashMap 与 HashTable 的区别并加以修改
作者
Hashtable:
HashMap:
HashMap 比 Hashtable 多了一个并发大神:Doug Lea。
产生时间
Hashtable 是 java 一开始发布时就提供的键值映射的数据结构,而 HashMap 产生于 JDK1.2。虽然 Hashtable 比 HashMap 出现的早一些,但是现在 Hashtable 基本上已经被弃用了。而 HashMap 已经成为应用最为广泛的一种数据类型了。
继承父类
HashMap 是继承自AbstractMap
类,而 Hashtable 是继承自Dictionary
类。不过它们都实现了同时实现了map
、Cloneable
(可复制)、Serializable
(可序列化)这三个接口
1 | public class Hashtable<K,V> |
在 Dictionary 源码中你能看到这么一行注释:
翻译如下:此类已过时。新的实现应实现 Map 接口,而不是扩展此类。
父类都过时了,Hashtable 怎么样就不用说了。
对外提供的接口
Hashtable 比 HashMap 多提供了 elments() 和 contains() 两个方法。
1 | public synchronized Enumeration<V> elements() { |
- elments() 方法继承自 Hashtable 的父类 Dictionnary。elements() 方法用于返回此 Hashtable 中的 value 的枚举。
- contains()方法判断该 Hashtable 是否包含传入的 value。它的作用与 containsValue()一致。实际上,Hashtable 的 containsValue 方法就是调用了 contains 方法
hashMap 去掉了 contains 方法,使用 containsValue 和 containsKey 方法。
对 Null key 和 Null value 的支持
- Hashtable 既不支持 Null key 也不支持 Null value。
当 key 为 Null 时,调用 put() 方法,运行到下面这一步就会抛出空指针异常。因为拿一个 Null 值去调用方法了。
而且方法开头就是对 value 的限制 - HashMap 中,null 可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为 null。当 get()方法返回 null 值时,可能是 HashMap 中没有该键,也可能使该键所对应的值为 null。因此,在 HashMap 中不能由 get()方法来判断 HashMap 中是否存在某个键, 而应该用 containsKey()方法来判断。
线程安全性
- Hashtable 是线程安全的,它的每个方法中都加入了 Synchronize 方法。在多线程并发的环境下,可以直接使用 Hashtable,不需要自己为它的方法实现同步
- HashMap 不是线程安全的,在多线程并发的环境下,可能会产生死锁等问题。使用 HashMap 时就必须要自己增加同步处理,
虽然 HashMap 不是线程安全的,但是它的效率会比 Hashtable 要好很多。这样设计是合理的。在我们的日常使用当中,大部分时间是单线程操作的。HashMap 把这部分操作解放出来了。
遍历方式的内部实现
Hashtable、HashMap 都使用了 Iterator。而由于历史原因,Hashtable 还使用了 Enumeration 的方式 。
HashMap 的 Iterator 是 fail-fast 迭代器。当有其它线程改变了 HashMap 的结构(增加,删除,修改元素),将会抛出 ConcurrentModificationException。不过,通过 Iterator 的 remove()方法移除元素则不会抛出 ConcurrentModificationException 异常。但这并不是一个一定发生的行为,要看 JVM。
JDK8 之前的版本中,Hashtable 是没有 fast-fail 机制的。在 JDK8 及以后的版本中 ,Hashtable 也是使用 fast-fail 的, 源码如下:
1 | public T next() { |
modCount 的使用类似于并发编程中的 CAS(Compare and Swap)技术。在 Hashtable 中,每次在发生增删改的时候都会出现 modCount++的动作。而 modcount 可以理解为是当前 Hashtable 的状态。每发生一次操作,状态就向前走一步。设置这个状态,主要是由于 Hashtable 等容器类在迭代时,判断数据是否过时时使用的。
尽管 Hashtable 采用了原生的同步锁来保护数据安全。但是在出现迭代数据的时候,则无法保证边迭代,边正确操作。于是使用这个值来标记状态。一旦在迭代的过程中状态发生了改变,则会快速抛出一个异常,终止迭代行为。
初始容量大小和每次扩充容量大小
Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。
创建时,如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小。也就是说 Hashtable 会尽量使用素数、奇数。而 HashMap 则总是使用 2 的幂作为哈希表的大小。
之所以会有这样的不同,是因为 Hashtable 和 HashMap 设计时的侧重点不同。Hashtable 的侧重点是哈希的结果更加均匀,使得哈希冲突减少。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。而 HashMap 则更加关注 hash 的计算效率问题。在取模计算时,如果模数是 2 的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。HashMap 为了加快 hash 的速度,将哈希表的大小固定为了 2 的幂。当然这引入了哈希分布不均匀的问题,所以 HashMap 为解决这问题,又对 hash 算法做了一些改动。这从而导致了 Hashtable 和 HashMap 的计算 hash 值的方法不同
计算 hash 值的方法
为了得到元素的位置,首先需要根据元素的 KEY 计算出一个 hash 值,然后再用这个 hash 值来计算得到最终的位置。
Hashtable 直接使用对象的 hashCode。hashCode 是 JDK 根据对象的地址或者字符串或者数字算出来的 int 类型的数值。然后再使用除留余数发来获得最终的位置。
1 | // Hashtable |
Hashtable 在计算元素的位置时需要进行一次除法运算,而除法运算是比较耗时的。
HashMap 为了提高计算效率,将哈希表的大小固定为了 2 的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。
HashMap 的效率虽然提高了,但是 hash 冲突却也增加了。因为它得出的 hash 值的低位相同的概率比较高,而计算位运算
为了解决这个问题,HashMap 重新根据 hashcode 计算 hash 值后,又对 hash 值做了一些运算来打散数据。使得取得的位置更加分散,从而减少了 hash 冲突。当然了,为了高效,HashMap 只做了一些简单的位处理。从而不至于把使用 2 的幂次方带来的效率提升给抵消掉。
1 | // HashMap |