【源码】Hashtable源码剖析

微信公众号:javafirst

注:以下源码基于jdk1.7.0_11

上一篇分析了HashMap的源码,相信大家对HashMap都有了更深入的理解。本文将介绍Map集合的另一个常用类,Hashtable。
Hashtable出来的比HashMap早,HashMap 1.2才有,而Hashtable在1.0就已经出现了。HashMap和Hashtable实现原理基本一样,都是通过哈希表实现。而且两者处理冲突的方式也一样,都是通过链表法。下面我们就详细介绍下这个类。
首先看类声明:
[java]  view plain  copy
  1. public class Hashtable<K,V>  
  2.     extends Dictionary<K,V>  
  3.     implements Map<K,V>, Cloneable, java.io.Serializable  

Hashtable并没有去继承AbstractMap,而是选择继承了Dictionary类,Dictionary是个被废弃的抽象类,文档已经说得很清楚了:
[java]  view plain  copy
  1. NOTE: This class is obsolete.  New implementations should  
  2.  * implement the Map interface, rather than extending this class.  

这个类的方法如下(全是抽象方法):
[java]  view plain  copy
  1. public abstract  
  2. class Dictionary<K,V> {  
  3.     
  4.     public Dictionary() {  
  5.     }  
  6.     abstract public int size();  
  7.     abstract public boolean isEmpty();  
  8.     abstract public Enumeration<K> keys();  
  9.     abstract public Enumeration<V> elements();  
  10.     abstract public V get(Object key);  
  11.     abstract public V put(K key, V value);  
  12.     abstract public V remove(Object key);  
  13. }  

没啥好说的,下面直接看Hashtable源码,首先依然是成员变量:

[java]  view plain  copy
  1. private transient Entry<K,V>[] table;//存储键值对对象的桶数组  
  2.    /** 
  3.     * The total number of entries in the hash table. 
  4.     *键值对总数 
  5.     */  
  6.    private transient int count;  
  7.    /** 
  8.     * The table is rehashed when its size exceeds this threshold.  (The 
  9.     * value of this field is (int)(capacity * loadFactor).) 
  10.     *容量的阈值,超过此容量将会导致扩容。值为容量*负载因子 
  11.     */  
  12.    private int threshold;  
  13.    /** 
  14.     * The load factor for the hashtable. 
  15.     *负载因子 
  16.     */  
  17.    private float loadFactor;  
  18.    /** 
  19.     * hashtable被改变的次数,用于快速失败机制 
  20.     */  
  21.    private transient int modCount = 0;  
成员变量跟HashMap基本类似,但是HashMap更加规范,HashMap内部还定义了一些常量,比如默认的负载因子,默认的容量,最大容量等等。
接下来是构造器:
[java]  view plain  copy
  1. public Hashtable(int initialCapacity, float loadFactor) {//可指定初始容量和加载因子  
  2.         if (initialCapacity < 0)  
  3.             throw new IllegalArgumentException("Illegal Capacity: "+  
  4.                                                initialCapacity);  
  5.         if (loadFactor <= 0 || Float.isNaN(loadFactor))  
  6.             throw new IllegalArgumentException("Illegal Load: "+loadFactor);  
  7.         if (initialCapacity==0)  
  8.             initialCapacity = 1;//初始容量最小值为1  
  9.         this.loadFactor = loadFactor;  
  10.         table = new Entry[initialCapacity];//创建桶数组  
  11.         threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//初始化容量阈值  
  12.         useAltHashing = sun.misc.VM.isBooted() &&  
  13.                 (initialCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);  
  14.     }  
  15.     /** 
  16.      * Constructs a new, empty hashtable with the specified initial capacity 
  17.      * and default load factor (0.75). 
  18.      */  
  19.     public Hashtable(int initialCapacity) {  
  20.         this(initialCapacity, 0.75f);//默认负载因子为0.75  
  21.     }  
  22.     public Hashtable() {  
  23.         this(110.75f);//默认容量为11,负载因子为0.75  
  24.     }  
  25.     /** 
  26.      * Constructs a new hashtable with the same mappings as the given 
  27.      * Map.  The hashtable is created with an initial capacity sufficient to 
  28.      * hold the mappings in the given Map and a default load factor (0.75). 
  29.      */  
  30.     public Hashtable(Map<? extends K, ? extends V> t) {  
  31.         this(Math.max(2*t.size(), 11), 0.75f);  
  32.         putAll(t);  
  33.     }  
需注意的点:
1.Hashtable的默认容量为11,默认负载因子为0.75.(HashMap默认容量为16,默认负载因子也是0.75)
2.Hashtable的容量可以为任意整数,最小值为1,而HashMap的容量始终为2的n次方。
3.为避免扩容带来的性能问题,建议指定合理容量。
另外我们看到,Hashtable的编码相比较HashMap不是很规范,构造器中出现了 硬编码,而HashMap中定义了常量。
跟HashMap一样,Hashtable内部也有一个静态类叫Entry,其实是个键值对对象,保存了键和值的引用。也可以理解为一个单链表的结点,因为其持有下一个Entry对象的引用:
[java]  view plain  copy
  1. private static class Entry<K,V> implements Map.Entry<K,V> {//键值对对象  
  2.         int hash;//哈希值  
  3.         final K key;//键  
  4.         V value;//值  
  5.         Entry<K,V> next;//指向下一个  
  6.         protected Entry(int hash, K key, V value, Entry<K,V> next) {  
  7.             this.hash = hash;  
  8.             this.key =  key;  
  9.             this.value = value;  
  10.             this.next = next;  
  11.         }  
  12.         protected Object clone() {//直接通过new的方式克隆  
  13.             return new Entry<>(hash, key, value,  
  14.                                   (next==null ? null : (Entry<K,V>) next.clone()));  
  15.         }  
  16.         // Map.Entry Ops  
  17.         public K getKey() {  
  18.             return key;  
  19.         }  
  20.         public V getValue() {  
  21.             return value;  
  22.         }  
  23.         public V setValue(V value) {//可设置值  
  24.             if (value == null)  
  25.                 throw new NullPointerException();  
  26.             V oldValue = this.value;  
  27.             this.value = value;  
  28.             return oldValue;  
  29.         }  
  30.         public boolean equals(Object o) {  
  31.             if (!(o instanceof Map.Entry))  
  32.                 return false;  
  33.             Map.Entry<?,?> e = (Map.Entry)o;  
  34.             return key.equals(e.getKey()) && value.equals(e.getValue());  
  35.         }  
  36.         public int hashCode() {  
  37.             return hash ^ value.hashCode();  
  38.         }  
  39.         public String toString() {  
  40.             return key.toString()+"="+value.toString();  
  41.         }  
  42.     }  
再次强调:HashMap和Hashtable存储的是键值对对象,而不是单独的键或值。
明确了存储方式后,再看put和get方法:
[java]  view plain  copy
  1. public synchronized V put(K key, V value) {//向哈希表中添加键值对  
  2.         // Make sure the value is not null  
  3.         if (value == null) {//确保值不能为空  
  4.             throw new NullPointerException();  
  5.         }  
  6.         // Makes sure the key is not already in the hashtable.  
  7.         Entry tab[] = table;  
  8.         int hash = hash(key);//根据键生成hash值---->若key为null,此方法会抛异常  
  9.         int index = (hash & 0x7FFFFFFF) % tab.length;//通过hash值找到其存储位置  
  10.         for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {/遍历链表  
  11.             if ((e.hash == hash) && e.key.equals(key)) {//若键相同,则新值覆盖旧值  
  12.                 V old = e.value;  
  13.                 e.value = value;  
  14.                 return old;  
  15.             }  
  16.         }  
  17.         modCount++;  
  18.         if (count >= threshold) {//当前容量超过阈值。需要扩容  
  19.             // Rehash the table if the threshold is exceeded  
  20.             rehash();//重新构建桶数组,并对数组中所有键值对重哈希,耗时!  
  21.             tab = table;  
  22.             hash = hash(key);  
  23.             index = (hash & 0x7FFFFFFF) % tab.length;//这里是取摸运算  
  24.         }  
  25.         // Creates the new entry.  
  26.         Entry<K,V> e = tab[index];  
  27.         //将新结点插到链表首部  
  28.         tab[index] = new Entry<>(hash, key, value, e);//生成一个新结点  
  29.         count++;  
  30.         return null;  
  31.     }  
需注意的点:
1.Hasbtable并不允许值和键为空(null),若为空,会抛空指针.大家可能奇怪,put方法在开始处仅对value进行判断,并未对key判断,这里我认为是设计者的疏忽。当然,这并不影响使用,因为当调用hash方法时,若key为空,依然会抛出空指针异常:
[java]  view plain  copy
  1. private int hash(Object k) {  
  2.         if (useAltHashing) {  
  3.             if (k.getClass() == String.class) {  
  4.                 return sun.misc.Hashing.stringHash32((String) k);  
  5.             } else {  
  6.                 int h = hashSeed ^ k.hashCode();  
  7.                 h ^= (h >>> 20) ^ (h >>> 12);  
  8.                 return h ^ (h >>> 7) ^ (h >>> 4);  
  9.              }  
  10.         } else  {  
  11.             return k.hashCode();//此处可能抛空指针异常  
  12.         }  
  13.     }  
2.HashMap计算索引的方式是h&(length-1),而Hashtable用的是模运算,效率上是低于HashMap的。
3.另外Hashtable计算索引时将hash值先与上0x7FFFFFFF,这是为了保证hash值始终为正数。
4.特别需要注意的是这个方法包括下面要讲的若干方法都加了synchronized关键字,也就意味着这个Hashtable是个线程安全的类,这也是它和HashMap最大的不同点.
下面我们看下扩容方法rehash:
[java]  view plain  copy
  1. protected void rehash() {  
  2.         int oldCapacity = table.length;//记录旧容量  
  3.         Entry<K,V>[] oldMap = table;//记录旧的桶数组  
  4.         // overflow-conscious code  
  5.         int newCapacity = (oldCapacity << 1) + 1;//新容量为老容量的2倍加1  
  6.         if (newCapacity - MAX_ARRAY_SIZE > 0) {  
  7.             if (oldCapacity == MAX_ARRAY_SIZE)//容量不得超过约定的最大值  
  8.                 // Keep running with MAX_ARRAY_SIZE buckets  
  9.                 return;  
  10.             newCapacity = MAX_ARRAY_SIZE;  
  11.         }  
  12.         Entry<K,V>[] newMap = new Entry[newCapacity];//创建新的数组  
  13.         modCount++;  
  14.         threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);  
  15.         boolean currentAltHashing = useAltHashing;  
  16.         useAltHashing = sun.misc.VM.isBooted() &&  
  17.                 (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);  
  18.         boolean rehash = currentAltHashing ^ useAltHashing;  
  19.         table = newMap;  
  20.         for (int i = oldCapacity ; i-- > 0 ;) {//转移键值对到新数组  
  21.             for (Entry<K,V> old = oldMap[i] ; old != null ; ) {  
  22.                 Entry<K,V> e = old;  
  23.                 old = old.next;  
  24.                 if (rehash) {  
  25.                     e.hash = hash(e.key);  
  26.                 }  
  27.                 int index = (e.hash & 0x7FFFFFFF) % newCapacity;  
  28.                 e.next = newMap[index];  
  29.                 newMap[index] = e;  
  30.             }  
  31.         }  
  32.     }  
Hashtable每次扩容,容量都为原来的2倍加2,而HashMap为原来的2倍。

接下来分析get方法:
[java]  view plain  copy
  1. public synchronized V get(Object key) {//根据键取出对应索引  
  2.       Entry tab[] = table;  
  3.       int hash = hash(key);//先根据key计算hash值  
  4.       int index = (hash & 0x7FFFFFFF) % tab.length;//再根据hash值找到索引  
  5.       for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {//遍历entry链  
  6.           if ((e.hash == hash) && e.key.equals(key)) {//若找到该键  
  7.               return e.value;//返回对应的值  
  8.           }  
  9.       }  
  10.       return null;//否则返回null  
  11.   }  
当然,如果你传的参数为null,是会抛空指针的。
至此,最重要的部分已经讲完,下面再看一些常用的方法:
[java]  view plain  copy
  1. public synchronized V remove(Object key) {//删除指定键值对  
  2.        Entry tab[] = table;  
  3.        int hash = hash(key);//计算hash值  
  4.        int index = (hash & 0x7FFFFFFF) % tab.length;//计算索引  
  5.        for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) {//遍历entry链  
  6.            if ((e.hash == hash) && e.key.equals(key)) {//找到指定键  
  7.                modCount++;  
  8.                if (prev != null) {//修改相关指针  
  9.                    prev.next = e.next;  
  10.                } else {  
  11.                    tab[index] = e.next;  
  12.                }  
  13.                count--;  
  14.                V oldValue = e.value;  
  15.                e.value = null;  
  16.                return oldValue;  
  17.            }  
  18.        }  
  19.        return null;  
  20.    }  

[java]  view plain  copy
  1. public synchronized void clear() {//清空桶数组  
  2.        Entry tab[] = table;  
  3.        modCount++;  
  4.        for (int index = tab.length; --index >= 0; )  
  5.            tab[index] = null;//直接置空  
  6.        count = 0;  
  7.    }  

下面是获取其键集(keySet)和键值集(entrySet)的方法:
[java]  view plain  copy
  1. public Set<K> keySet() {  
  2.         if (keySet == null)//通过Collections的包装,返回的是线程安全的键集  
  3.             keySet = Collections.synchronizedSet(new KeySet(), this);  
  4.         return keySet;  
  5.     }  
  6.  public Set<Map.Entry<K,V>> entrySet() {  
  7.         if (entrySet==null)//通过Collections的包装,返回的是线程安全的键值集  
  8.             entrySet = Collections.synchronizedSet(new EntrySet(), this);  
  9.         return entrySet;  
  10.     }  

这个KeySet和EntrySet是Hashtable的两个内部类:
[java]  view plain  copy
  1. private class KeySet extends AbstractSet<K> {  
  2.        public Iterator<K> iterator() {  
  3.            return getIterator(KEYS);  
  4.        }  
  5.        public int size() {  
  6.            return count;  
  7.        }  
  8.        public boolean contains(Object o) {  
  9.            return containsKey(o);  
  10.        }  
  11.        public boolean remove(Object o) {  
  12.            return Hashtable.this.remove(o) != null;  
  13.        }  
  14.        public void clear() {  
  15.            Hashtable.this.clear();  
  16.        }  
  17.    }  


总结:
1.Hashtable是个线程安全的类(HashMap线程安全);
2.Hasbtable并不允许值和键为空(null),若为空,会抛空指针(HashMap可以);
3.Hashtable不允许键重复,若键重复,则新插入的值会覆盖旧值(同HashMap);
4.Hashtable同样是通过链表法解决冲突;
5.Hashtable根据hashcode计算索引时将hashcode值先与上0x7FFFFFFF,这是为了保证hash值始终为正数;
6.Hashtable的容量为任意正数(最小为1),而HashMap的容量始终为2的n次方。Hashtable默认容量为        11,HashMap默认容量为      16;
7.Hashtable每次扩容,新容量为旧容量的2倍加2,而HashMap为旧容量的2倍;
8.Hashtable和HashMap默认负载因子都为0.75;

微信公众号:javafirst

猜你喜欢

转载自blog.csdn.net/xiamiflying/article/details/80666976