Hashtable源碼分析(基於jdk1.8,推薦)

Hashtable也算是集合系列中一個比較重要的集合類了,不過在介紹Hashtable的時候,老是不可避免的談到HashMap,在面試的時候Hashtable每每也會結合HashMap一塊來問。這篇文章就來好好地分析一下Hashtablejava

1、認識Hashtable面試

一、繼承關係數組

爲了能好好的理解Hashtable,咱們先看一下他在整個集合體系中的位置:安全

從上面的圖咱們會發現,Hashtable和HashMap師出同門,不過這張圖太宏觀,咱們仍是放小了看:架構

這張圖已經很清晰了,繼承了Dictionary,實現了Map接口。併發

二、與HashMap的區別源碼分析

若是你以前看過我寫的那篇HashMap文章的話,在這裏對他們倆的區別必定有了解,如今咱們對其進行一個整理(這裏只看區別):this

(1)HashMap容許key和value爲空,可是Hashtable不容許。spa

(2)Hashtable是線程安全的,HashMap不是線程安全。線程

(3)ashtable繼承自Dictionary,HashMap繼承自AbstractMap。

(4)迭代器不一樣,Hashtable是enumerator迭代器,HashMap是Iterator迭代器。

三、Hashtable基本使用

public class HashtableTest {
    public static void main(String [] args){
    	//新建方式
        Hashtable<String, String> table=new Hashtable<>();
        Hashtable<String, String> table1=new Hashtable<>(16);
        Hashtable<String, String> table2=new Hashtable<>(16, 0.75f);
        HashMap<String,String>  map=new HashMap<>();
        Hashtable<String,String> table3=new Hashtable<>(map);
        table.put("張三", "1");
        table.put("李四", "2");
        //這種方式會出現空指針異常,由於Hashtable的key不能爲空
        table.put(null, "3");
        System.out.println(table.toString());
    }
}
複製代碼

下面咱們就經過源碼來分析一下Hashtable。

2、源碼分析

對於集合類的源碼分析,通常都是從參數、構造方法、還有增刪改查的基礎上進行分析,而後就是增長元素,增多了怎麼處理。刪除元素,刪多了怎麼辦等等。下面咱們就按照這個思路一步一步分析:

一、參數

HashTable的底層採用"拉鍊法"哈希表,而且提供了5個主要的參數:

(1)table:爲一個Entry[]數組類型,Entry表明了「拉鍊」的節點,每個Entry表明了一個鍵值對。

(2)count:容器中包含Entry鍵值對的數量。

(3)threshold:閾值,大於這個閾值時須要調整容器容量。值="容量*加載因子"。

(4)loadFactor:加載因子。這個比較重要。

(5)modCount:用來實現「fail-fast」機制的。對容器任何增刪改操做都會修改modCount。若是出錯當即拋出ConcurrentModificationException異常。

private transient Entry<?,?>[] table;
private transient int count;
private int threshold;
private float loadFactor;
private transient int modCount = 0;
複製代碼

上面的是源碼,你會發現table、count、modCount還都是transient修飾的,這也就意味着這三個參數是不能被系列化的。

二、構造方法

下面咱們看看其構造方法,源碼中一共提供了4個構造方法:

(1)構造方法1

//構造一個空的Hashtable
//默認容量是11,加載因子是0.75
public Hashtable() {
    this(11, 0.75f);
} 
複製代碼

(2)構造方法2

//在構造Hashtable的時候,指定容量
//加載因子仍是0.75
public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}
複製代碼

(3)構造方法3

public Hashtable(int initialCapacity, float loadFactor) {
    //確保初始容量符合
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
    //確保加載因子符合
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load: "+loadFactor);
	//若是初始容量爲0,那就賦值爲1
    if (initialCapacity==0)  initialCapacity = 1;
    this.loadFactor = loadFactor;
    //新建一個table
    table = new Entry<?,?>[initialCapacity];
    //設置閾值:initialCapacity * loadFactor和MAX_ARRAY_SIZE + 1的最小者
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
複製代碼

這個構造方法首先排除掉一些異常狀況,而後新建一個table數組來裝數據。

(4)構造方法4

//構造給定的 Map 具備相同映射關係的新哈希表:也就是下面這種
//HashMap<String,String> map=new HashMap<>();
//Hashtable<String,String> table3=new Hashtable<>(map);
public Hashtable(Map<? extends K, ? extends V> t) {
    this(Math.max(2*t.size(), 11), 0.75f);
    putAll(t);
}
複製代碼

這個構造方法咱們可就要稍微注意了,真正實現這個操做的是putAll(t)。想要弄清楚咱們不妨跟進去看看。

//把map經過for循環一個一個存放在另一個map中
public synchronized void putAll(Map<? extends K, ? extends V> t) {
    for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
         put(e.getKey(), e.getValue());
}
複製代碼

上面的代碼使用了泛型,並且仍是泛型通配符,不過意思很明確,就是經過for循環一個一個轉移到新的map中。

以上所述就是整個構造方法的機制。

三、增長一個元素

public synchronized V put(K key, V value) {
    //第一部分:確保value不爲空
    if (value == null) {
        throw new NullPointerException();
    }
    //第二部分:確保table中沒有當前的key
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
	//第三部分:增長一個元素的核心
    addEntry(hash, key, value, index);
    return null;
}
複製代碼

這些代碼第一部分和第二部分都是爲了沒有異常,若是當前容器有這個key,那麼直接以新值代替舊值便可,最主要的仍是第三部分,添加一個元素的核心addEntry方法。進去看看:

private void addEntry(int hash, K key, V value, int index) {
   //modCount是爲了安全機制
   modCount++;
   Entry<?,?> tab[] = table;
   if (count >= threshold) {
        //若是大於閾值,就會從新hash擴容
        rehash();
        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
   }
   //沒有異常狀況,新建一個Entry根據hash值插入到指定位置便可
   @SuppressWarnings("unchecked")
   Entry<K,V> e = (Entry<K,V>) tab[index];
   tab[index] = new Entry<>(hash, key, value, e);
   count++;
}
複製代碼

上面的這些代碼的大體意思就是若是容器裏面沒有滿,那就新建一個Entry根據hash值插入到指定位置。並且一開始還提供了modCount確保安全(快速失敗機制)。如何去擴容呢?下面咱們接着講。

二、擴容

擴容就是當容器中放滿了,須要把容器擴大。咱們看看這個rehash是如何擴容的。

protected void rehash() {
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;
    //新容量=舊容量 * 2 + 1:實現方式就是oldCapacity << 1
    int newCapacity = (oldCapacity << 1) + 1;
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        if (oldCapacity == MAX_ARRAY_SIZE)  return;
        newCapacity = MAX_ARRAY_SIZE;
    }
    //根據新容量建一個新Map,並賦值給table
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
    modCount++;
    //從新計算閾值
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;
	//將原來的元素拷貝到新的HashTable中 
    for (int i = oldCapacity ; i-- > 0 ;) {
        for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;
            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = (Entry<K,V>)newMap[index];
            newMap[index] = e;
        }
    }
}
複製代碼

上面代碼的註釋已經很清楚了,不過上面相信你會有一個疑問,不論是put一個元素仍是擴容,在計算hash的時候都出現了(e.hash & 0x7FFFFFFF) ,它的做用是什麼呢?

你能夠這樣理解,hash值是int類型,並且必定是正數,和0x7FFFFFFF作與操做便是將負數變成正數,確保了獲取到的index是正數。

三、刪除一個元素

public synchronized V remove(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>)tab[index];
    for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            modCount++;
            if (prev != null) {
                prev.next = e.next;
            } else {
                tab[index] = e.next;
            }
            count--;
            V oldValue = e.value;
            e.value = null;
            return oldValue;
        }
    }
    return null;
}
複製代碼

刪除一個元素就比較簡單,核心就是經過key計算在容器中的位置,而後把這個位置上的Entry刪除便可。因爲使用的鏈表刪除起來會更簡單。將前一個元素指針直接指向下一個元素,跳過當前元素e.next。

四、查詢一個元素

public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}
複製代碼

這個就太簡單了,經過key計算hash值找到位置,直接經過e.value獲取值便可。

五、迭代器

與HashMap不一樣的是,它的迭代器是Enumeration。在這裏咱們不會講解Enumeration,只是給出其基本的使用方法,由於Enumeration我會在專門的文章裏面會介紹。這裏就再也不重複了。

public static void main(String[] args) {
      Hashtable hashTable = new Hashtable();
      hashTable.put("張三", "1");
      hashTable.put("李四", "2");
      hashTable.put("java的架構師技術棧", "3");
      Enumeration e = hashTable.elements();
      while(e.hasMoreElements()){
         System.out.println(e.nextElement());
      }
}
複製代碼

這就是一個最簡單的使用方法,

六、其餘方法

public synchronized boolean contains(Object value) {
    if (value == null) {
        throw new NullPointerException();
    }
    Entry<?,?> tab[] = table;
    for (int i = tab.length ; i-- > 0 ;) {
        for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
            if (e.value.equals(value)) {
                return true;
            }
        }
    }
    return false;
}
複製代碼

這個方法是判斷這個容器中是否有value這個值,判斷的方法特別死板,就是經過for循環一個一個比較。

3、總結

說實話這個Hashtable使用的場景仍是很侷限的,因此通常狀況下基本上不會用到,不論是效率仍是空間特性。由於上面的增刪改查方法你會發現,全是一個一個比對的,對於數據量大的時候這是很是耗時的,並且存儲空間也是採用的鏈表。

通常來講非併發場景使用HashMap,併發場景可使用Hashtable,可是推薦使用ConcurrentHashMap,由於它鎖粒度更低、效率更高。

Hashtable每每會結合者HashMap來出問題,但願你們注意,最核心的仍是HashMap。

相關文章
相關標籤/搜索