一.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://blog.csdn.net/zheng0518/article/details/42199477
http://www.cnblogs.com/devinzhang/archive/2012/01/13/2321481.html