Hashtable
Hashtable 是个过时的集合类,不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。但这并不是我们不去了解它的理由。最起码 Hashtable 和 HashMap 的面试题在面试中经常被问到。
一、前言
Hashtable
和HashMap
,从存储结构和实现来讲基本上都是相同的。
它和HashMap
的最大的不同是它是线程安全的,另外它不允许key
和value
为null
。
为了能在哈希表中成功地保存和取出对象,用作key
的对象必须实现hashCode
方法和equals
方法。
二、fail-fast机制
iterator
方法返回的迭代器是fail-fast
的。如果在迭代器被创建后hashtable
被结构型地修改了,除了迭代器自己的remove
方法,迭代器会抛出一个ConcurrentModificationException
异常。
因此,面对在并发的修改,迭代器干脆利落的失败,而不是冒险的继续。
关于这个的理解,其实在上一章讲LinkedHashMap
中的第八点提到:
值得注意的是,afterNodeAccess()
函数中,会修改modCount
,因此当你正在accessOrder=true
的模式下,迭代LinkedHashMap
时,如果同时查询访问数据,也会导致fail-fast,因为迭代的顺序已经改变。
简单说,就是两个线程同时分别进行修改和遍历时,会抛出这个异常。
面试题:集合在遍历过程中是否可以删除元素,为什么迭代器就可以安全删除元素?
集合在使用 for
循环迭代的过程中不允许使用,集合本身的 remove
方法删除元素,如果进行错误操作将会导致 ConcurrentModificationException
异常的发生
Iterator
可以删除访问的当前元素(current
),一旦删除的元素是Iterator
对象中 next
所正在引用的,在 Iterator
删除元素通过 修改 modCount
与 expectedModCount
的值,可以使下次在调用 remove
的方法时候两者仍然相同因此不会有异常产生。
迭代器的fail-fast
机制并不能得到保证,它不能够保证一定出现该错误。一般来说,fail-fast
会尽最大努力抛出ConcurrentModificationException
异常。因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是:ConcurrentModificationException
应该仅用于检测 bug。
Hashtable
是线程安全的。如果不需要线程安全的实现是不需要的,推荐使用HashMap
代替Hashtable
。如果需要线程安全的实现,推荐使用java.util.concurrent.ConcurrentHashMap
代替Hashtable
。
二、继承关系
1 | public class Hashtable<K,V> |
extends Dictionary<K,V>
:Dictionary
类是一个抽象类,用来存储键/值对,作用和Map类相似。implements Map<K,V>
:实现了Map
,实现了Map
中声明的操作和default
方法。
hashMap
以及TreeMap
的源码,都没有继承于这个类。不过当我看到注释中的解释也就明白了,其 Dictionary
源码注释是这样的:NOTE: This class is obsolete. New implementations should implement the Map interface, rather than extending this class.
该话指出 Dictionary
这个类过时了,新的实现类应该实现Map
接口。
三、属性
1 | //哈希表 |
HashTable
并没有像HashMap
那样定义了很多的常量,而是直接写死在了方法里。
Hashtable
不要求底层数组的容量一定要为2的整数次幂,而HashMap
则要求一定为2的整数次幂。
四、构造函数
1 | /** |
我们可以获取到这些信息:HashTable
默认的初始化容量为11(与HashMap
不同),负载因子默认为0.75(与HashMap
相同)。而正因为默认初始化容量的不同,同时也没有对容量做调整的策略,所以可以先推断出,HashTable
使用的哈希函数跟HashMap
是不一样的(事实也确实如此)。
五、重要方法
5.1 get方法
1 | public synchronized V get(Object key) { |
这里可以看到,Hashtable
和HashMap
确认key
在数组中的索引的方法不同。
Hashtable
通过index = (hash & 0x7FFFFFFF) % tab.length;
来确认HashMap
通过i = (n - 1) & hash;
来确认
跟HashMap
相比,HashTable
的get
方法非常简单。我们首先可以看见get
方法使用了synchronized
来修饰,所以它能保证线程安全。并且它是通过链表的方式来处理冲突的。另外,我们还可以看见HashTable
并没有像HashMap
那样封装一个哈希函数,而是直接把哈希函数写在了方法中。而哈希函数也是比较简单的,它仅对哈希表的长度进行了取模。
5.2 put方法
1 | public synchronized V put(K key, V value) { |
从代码中可以总结出Hashtable
的put
方法的总体思路:
- 确认
value
不为null
。如果为null
,则抛出异常 - 找到
key
在table
中的索引,获取key
所在位置的entry
- 遍历
entry
,判断key
是否已经存在 - 如果
key
已经存在,替换value
,返回旧的value
- 如果
key
在hashtable
不是已经存在,就直接添加,否则直接将键值对添加到table
中,返回null
在方法中可以看到,在遍历桶中元素时,是按照链表的方式遍历的。可以印证,HashMap
的桶中可能为链表或者树。但Hashtable
的桶中只可能是链表。
5.3 remove方法
1 | public synchronized V remove(Object key) { |
从代码中可以总结出Hashtable
的remove
方法的总体思路:
- 找到
key
在table
中的索引,获取key
所在位置的entry
- 遍历
entry
,判断key
是否已经存在 - 如果
key
存在,删除key
映射的键值对,返回旧的value
- 如果
key
在hashtable
不存在,返回null
5.4 rehash方法
1 | /** |
看完代码,我们可以总结出rehash
的总体思路为:
- 新建变量新的容量,值为旧的容量的2倍+1
- 如果新的容量大于容量的最大值
MAX_ARRAY_SIZE
- 如果旧容量为
MAX_ARRAY_SIZE
,容量不变,中断方法的执行 - 如果旧容量不为
MAX_ARRAY_SIZE
,新容量变为MAX_ARRAY_SIZE
- 如果旧容量为
- 创建新的数组,容量为新容量
- 将旧的数组中的键值对转移到新数组中
这里可以看到,一般情况下,HashMap
扩容后容量变为原来的两倍,而Hashtable
扩容后容量变为原来的两倍加一。
HashTable
的rehash
方法相当于HashMap
的resize
方法。跟HashMap
那种巧妙的rehash
方式相比,HashTable
的rehash
过程需要对每个键值对都重新计算哈希值,而比起异或和与操作,取模是一个非常耗时的操作,所以这也是导致效率较低的原因之一。
六、遍历
可以使用与HashMap
一样的遍历方式,但是由于历史原因,多了Enumeration
的方式。
针对Enumeration,这里与iterator进行对比一下。
相同点
Iterator
和Enumeration
都可以对某些容器进行遍历。Iterator
和Enumeration
都是接口。
不同点
Iterator
有对容器进行修改的方法。而Enumeration
只能遍历。Iterator
支持fail-fast
,而Enumeration
不支持。Iterator
比Enumeration
覆盖范围广,基本所有容器中都有Iterator
迭代器,而只有Vector
、Hashtable
有Enumeration
。Enumeration
在JDK 1.0就已经存在了,而Iterator
是JDK2.0新加的接口。
七、Hashtable与HashMap对比
HashTable
的应用非常广泛,HashMap
是新框架中用来代替HashTable
的类,也就是说建议使用HashMap
。
下面着重比较一下二者的区别:
1.继承不同
Hashtable
是基于陈旧的Dictionary
类的,HashMap
是java1.2引进的Map接口的一个实现。
2.同步
Hashtable
中的方法是同步的,保证了Hashtable
中的对象是线程安全的。
HashMap
中的方法在缺省情况下是非同步的,HashMap
中的对象并不是线程安全的。在多线程并发的环境下,可以直接使用Hashtable
,但是要使用HashMap
的话就要自己增加同步处理了。
3.效率
单线程中, HashMap
的效率大于Hashtable
。因为同步的要求会影响执行的效率,所以如果你不需要线程安全的集合,HashMap
是Hashtable
的轻量级实现,这样可以避免由于同步带来的不必要的性能开销,从而提高效率。
4.null值
Hashtable
中,key
和value
都不允许出现null
值,否则出现NullPointerException
。
在HashMap
中,null
可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为nul
l。当get()
方法返回null
值时,即可以表示 HashMap
中没有该键,也可以表示该键所对应的值为null
。因此,在HashMap
中不能由get()
方法来判断HashMap
中是否存在某个键,而应该用containsKey()
方法来判断。
5.遍历方式
Hashtable
、HashMap
都使用了 Iterator
。而由于历史原因,Hashtable
可以使用Enumeration
的方式。
6.容量
Hashtable
和HashMap
它们两个内部实现方式的数组的初始大小和扩容的方式。
HashTable
中hash
数组默认大小是11,增加的方式是 old*2+1
。
HashMap
中hash
数组的默认大小是16,而且一定是2的指数。
八、总结
无论什么时候有多个线程访问相同实例的可能时,就应该使用Hashtable
,反之使用HashMap
。非线程安全的数据结构能带来更好的性能。
如果在将来有一种可能—你需要按顺序获得键值对的方案时,HashMap
是一个很好的选择,因为有HashMap
的一个子类 LinkedHashMap
。
所以如果你想可预测的按顺序迭代(默认按插入的顺序),你可以很方便用LinkedHashMap
替换HashMap
。反观要是使用的Hashtable
就没那么简单了。
如果有多个线程访问HashMap
,Collections.synchronizedMap()
可以代替,总的来说HashMap
更灵活,或者直接用并发容器ConcurrentHashMap
。
整理自: