Java集合(8):Hashtable

一.Hashtable介绍

  和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射,它在很大程度上和HashMap的实现差不多。

  Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。

1.Hashtable的继承关系

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable

2.Hashtable的类图关系

  HashTable继承Dictionary类,实现Map接口。其中Dictionary类是任何可将键映射到相应值的类的抽象父类。每个键和每个值都是一个对象。在任何一个 Dictionary 对象中,每个键至多与一个值相关联。Map是”key-value键值对”接口。

二.Hashtable源码解析

1.Hashtable的私有属性

1 private transient Entry<?,?>[] table;//table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。
2 private transient int count;//count是Hashtable的大小,它是Hashtable保存的键值对的数量。 
3 private int threshold;//threshold是Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。
4 private float loadFactor;//loadFactor就是加载因子。 
5 private transient int modCount = 0;//modCount是用来实现fail-fast机制的

  所谓快速失败就是在并发集合中,其进行迭代操作时,若有其他线程对其进行结构性的修改,这时迭代器会立马感知到,并且立即抛出ConcurrentModificationException异常,而不是等到迭代完成之后才告诉你(你已经出错了)。

2.Hashtable的构造方法

 1 // 默认构造函数。
 2 public Hashtable() {
 3     this(11, 0.75f);
 4 }
 5 
 6 // 指定“容量大小”的构造函数
 7 public Hashtable(int initialCapacity) {
 8     this(initialCapacity, 0.75f);
 9 }
10 
11 // 指定“容量大小”和“加载因子”的构造函数
12 public Hashtable(int initialCapacity, float loadFactor) {
13     if (initialCapacity < 0)//验证初始容量
14         throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
15     if (loadFactor <= 0 || Float.isNaN(loadFactor))//验证加载因子
16         throw new IllegalArgumentException("Illegal Load: "+loadFactor);
17     if (initialCapacity==0)
18         initialCapacity = 1;
19     this.loadFactor = loadFactor;
20     table = new Entry[initialCapacity];//初始化table,获得大小为initialCapacity的table数组
21     threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//计算阀值
22     initHashSeedAsNeeded(initialCapacity);//初始化HashSeed值
23 }
24 
25 // 包含“子Map”的构造函数
26 public Hashtable(Map<? extends K, ? extends V> t) {
27     this(Math.max(2*t.size(), 11), 0.75f);//设置table容器大小,其值==t.size * 2 + 1
28     putAll(t);
29 }

3.存储数据put

将指定 key 映射到此哈希表中的指定 value。注意这里键key和值value都不可为空

 1 public synchronized V put(K key, V value) {
 2     if (value == null) {// 确保value不为null
 3         throw new NullPointerException();
 4     }
 5 
 6     /*
 7      * 确保key在table[]是不重复的
 8      * 处理过程:
 9      * 1、计算key的hash值,确认在table[]中的索引位置
10      * 2、迭代index索引位置,如果该位置处的链表中存在一个一样的key,则替换其value,返回旧值
11      */
12     Entry tab[] = table;
13     int hash = hash(key);    //计算key的hash值
14     int index = (hash & 0x7FFFFFFF) % tab.length;     //确认该key的索引位置
15     for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { //迭代,寻找该key,替换
16         if ((e.hash == hash) && e.key.equals(key)) {
17             V old = e.value;
18             e.value = value;
19             return old;
20         }
21     }
22 
23     modCount++;
24     if (count >= threshold) {  //如果容器中的元素数量已经达到阀值,则进行扩容操作
25         rehash();
26         tab = table;
27         hash = hash(key);
28         index = (hash & 0x7FFFFFFF) % tab.length;
29     }
30 
31     Entry<K,V> e = tab[index];// 在索引位置处插入一个新的节点
32     tab[index] = new Entry<>(hash, key, value, e);
33     count++;//容器中元素+1
34     return null;
35 }

  put方法的整个处理流程是:计算key的hash值,根据hash值获得key在table数组中的索引位置,然后迭代该key处的Entry链表(我们暂且理解为链表),若该链表中存在一个这个的key对象,那么就直接替换其value值即可,否则在将改key-value节点插入该index索引位置处。

过程演示如下:

  首先我们假设一个容量为5的table,存在如下的键值对:

  然后我们插入一个数:put(16,22),key=16在table的索引位置为1,同时在1索引位置有两个数,程序对该“链表”进行迭代,发现存在一个key=16,这时要做的工作就是用newValue=22替换oldValue16,并将oldValue=16返回。

  在put(33,33),key=33所在的索引位置为3,并且在该链表中也没有存在某个key=33的节点,所以就将该节点插入该链表的第一个位置。

  扩容操作:在put方法中,如果需要向table[]中添加Entry元素,会首先进行容量校验,如果容量已经达到了阀值,HashTable就会进行rehash()扩容处理

 1 protected void rehash() {
 2     int oldCapacity = table.length;
 3     Entry<K,V>[] oldMap = table;
 4 
 5     int newCapacity = (oldCapacity << 1) + 1;//新容量=旧容量 * 2 + 1
 6     if (newCapacity - MAX_ARRAY_SIZE > 0) {
 7         if (oldCapacity == MAX_ARRAY_SIZE)
 8             return;
 9         newCapacity = MAX_ARRAY_SIZE;
10     }
11     Entry<K,V>[] newMap = new Entry[];//新建一个size = newCapacity 的HashTable
12     modCount++;
13     threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//重新计算阀值
14     boolean rehash = initHashSeedAsNeeded(newCapacity);//重新计算hashSeed
15     table = newMap;
16     for (int i = oldCapacity ; i-- > 0 ;) {//将原来的元素拷贝到新的HashTable中
17         for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
18             Entry<K,V> e = old;
19             old = old.next;
20             if (rehash) {
21                 e.hash = hash(e.key);
22             }
23             int index = (e.hash & 0x7FFFFFFF) % newCapacity;
24             e.next = newMap[index];
25             newMap[index] = e;
26         }
27     }
28 }

  通过上面rehash代码我们可以看到容量扩大两倍+1,同时需要将原来HashTable中的元素一一复制到新的HashTable中,这个过程是比较消耗时间的,同时还需要重新计算hashSeed的,毕竟容量已经变了。

  关于阀值:比如初始值11、加载因子默认0.75,那么这个时候阀值threshold=8,当容器中的元素达到8时,HashTable进行一次扩容操作,容量 = 8 * 2 + 1 =17,而阀值threshold=17*0.75 = 13,当容器元素再一次达到阀值时,HashTable还会进行扩容操作,一次类推。

4.数据读取get()

  相对于put方法,get方法就会比较简单,处理过程就是计算key的hash值,判断在table数组中的索引位置,然后迭代链表,匹配直到找到相对应key的value,若没有找到返回null。

 1 public synchronized V get(Object key) {
 2     Entry tab[] = table;
 3     int hash = hash(key);
 4     int index = (hash & 0x7FFFFFFF) % tab.length;
 5     for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
 6         if ((e.hash == hash) && e.key.equals(key)) {
 7             return e.value;
 8         }
 9     }
10     return null;
11 }

5.其他方法

三.Hashtable的遍历

1.遍历Hashtable的键值对(效率较高)

第一步:根据entrySet()获取Hashtable的“键值对”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。

 1 // 假设table是Hashtable对象
 2 // table中的key是String类型,value是Integer类型
 3 Integer integ = null;
 4 Iterator iter = table.entrySet().iterator();
 5 while(iter.hasNext()) {
 6     Map.Entry entry = (Map.Entry)iter.next();
 7     // 获取key
 8     key = (String)entry.getKey();
 9         // 获取value
10     integ = (Integer)entry.getValue();
11 }

2.通过Iterator遍历Hashtable的键(效率较低)

第一步:根据keySet()获取Hashtable的“键”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。

 1 // 假设table是Hashtable对象
 2 // table中的key是String类型,value是Integer类型
 3 String key = null;
 4 Integer integ = null;
 5 Iterator iter = table.keySet().iterator();
 6 while (iter.hasNext()) {
 7         // 获取key
 8     key = (String)iter.next();
 9         // 根据key,获取value
10     integ = (Integer)table.get(key);
11 }

3.通过Iterator遍历Hashtable的值

第一步:根据value()获取Hashtable的“值”的集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。

1 // 假设table是Hashtable对象
2 // table中的key是String类型,value是Integer类型
3 Integer value = null;
4 Collection c = table.values();
5 Iterator iter= c.iterator();
6 while (iter.hasNext()) {
7     value = (Integer)iter.next();
8 }

4.通过Enumeration遍历Hashtable的键(效率较高)

第一步:根据keys()获取Hashtable的集合。
第二步:通过Enumeration遍历“第一步”得到的集合。

Enumeration enu = table.keys();
while(enu.hasMoreElements()) {
    System.out.println(enu.nextElement());
}  

5.通过Enumeration遍历Hashtable的值(效率较高)

第一步:根据elements()获取Hashtable的集合。
第二步:通过Enumeration遍历“第一步”得到的集合。

Enumeration enu = table.elements();
while(enu.hasMoreElements()) {
    System.out.println(enu.nextElement());
}

四.HashTable和HashMap的比较

  HashTable的应用非常广泛,HashMap是新框架中用来代替HashTable的类,也就是说建议使用HashMap

下面着重比较一下二者的区别:

1.继承不同

Hashtable是基于陈旧的Dictionary类的,HashMap是java 1.2引进的Map接口的一个实现。

2.同步

Hashtable 中的方法是同步的,保证了Hashtable中的对象是线程安全的。

HashMap中的方法在缺省情况下是非同步的,HashMap中的对象并不是线程安全的。在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就要自己增加同步处理了。

3.效率

单线程中, HashMap的效率大于Hashtable。因为同步的要求会影响执行的效率,所以如果你不需要线程安全的集合,HashMap是Hashtable的轻量级实现,这样可以避免由于同步带来的不必要的性能开销,从而提高效率。

4.null值

Hashtable中,key和value都不允许出现null值,否则出现NullPointerException。

在HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当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更灵活

参考:http://cmsblogs.com/?p=618

http://blog.csdn.net/zheng0518/article/details/42199477

http://www.cnblogs.com/devinzhang/archive/2012/01/13/2321481.html

原文地址:https://www.cnblogs.com/Eason-S/p/5692943.html