java 深刻HashTable

     在java中與有兩個類都提供了一個多種用途的hashTable機制,他們均可以將能夠key和value結合起來構成鍵值對經過put(key,value)方法保存起來,而後經過get(key)方法獲取相對應的value值。一個是前面提到的HashMap,還有一個就是立刻要講解的HashTable。對於HashTable而言,它在很大程度上和HashMap的實現差很少,若是咱們對HashMap比較瞭解的話,對HashTable的認知會提升很大的幫助。他們二者之間只存在幾點的不一樣,這個後面會闡述。java

1、定義

      HashTable在Java中的定義以下:數組

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

      從中能夠看出HashTable繼承Dictionary類,實現Map接口。其中Dictionary類是任何可將鍵映射到相應值的類(如 Hashtable)的抽象父類。每一個鍵和每一個值都是一個對象。在任何一個 Dictionary 對象中,每一個鍵至多與一個值相關聯。Map是"key-value鍵值對"接口。安全

      HashTable採用"拉鍊法"實現哈希表,它定義了幾個重要的參數:table、count、threshold、loadFactor、modCount。多線程

      table:爲一個Entry[]數組類型,Entry表明了「拉鍊」的節點,每個Entry表明了一個鍵值對,哈希表的"key-value鍵值對"都是存儲在Entry數組中的。併發

      count:HashTable的大小,注意這個大小並非HashTable的容器大小,而是他所包含Entry鍵值對的數量。函數

      threshold:Hashtable的閾值,用於判斷是否須要調整Hashtable的容量。threshold的值="容量*加載因子"。this

      loadFactor:加載因子。spa

      modCount:用來實現「fail-fast」機制的(也就是快速失敗)。所謂快速失敗就是在併發集合中,其進行迭代操做時,如有其餘線程對其進行結構性的修改,這時迭代器會立馬感知到,而且當即拋出ConcurrentModificationException異常,而不是等到迭代完成以後才告訴你(你已經出錯了)。線程

 

2、構造方法

      在HashTabel中存在5個構造函數。經過這5個構造函數咱們構建出一個我想要的HashTable。code

public Hashtable() {
        this(11, 0.75f);
    }

      默認構造函數,容量爲11,加載因子爲0.75。

public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

      用指定初始容量和默認的加載因子 (0.75) 構造一個新的空哈希表。

複製代碼
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);

        if (initialCapacity==0)
            initialCapacity = 1;
        
        this.loadFactor = loadFactor;
        
        //初始化table,得到大小爲initialCapacity的table數組
        table = new Entry[initialCapacity];
        //計算閥值
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        //初始化HashSeed值
        initHashSeedAsNeeded(initialCapacity);
    }
複製代碼

      用指定初始容量和指定加載因子構造一個新的空哈希表。其中initHashSeedAsNeeded方法用於初始化hashSeed參數,其中hashSeed用於計算key的hash值,它與key的hashCode進行按位異或運算。這個hashSeed是一個與實例相關的隨機值,主要用於解決hash衝突。

private int hash(Object k) {
        return hashSeed ^ k.hashCode();
    }

  構造一個與給定的 Map 具備相同映射關係的新哈希表。

複製代碼
public Hashtable(Map<? extends K, ? extends V> t) {
        //設置table容器大小,其值==t.size * 2 + 1
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }
複製代碼

3、主要方法

      HashTable的API對外提供了許多方法,這些方法可以很好幫助咱們操做HashTable,可是這裏我只介紹兩個最根本的方法:put、get。

      首先咱們先看put方法:將指定 key 映射到此哈希表中的指定 value。注意這裏鍵key和值value都不可爲空。

複製代碼
public synchronized V put(K key, V value) {
        // 確保value不爲null
        if (value == null) {
            throw new NullPointerException();
        }

        /*
         * 確保key在table[]是不重複的
         * 處理過程:
         * 一、計算key的hash值,確認在table[]中的索引位置
         * 二、迭代index索引位置,若是該位置處的鏈表中存在一個同樣的key,則替換其value,返回舊值
         */
        Entry tab[] = table;
        int hash = hash(key);    //計算key的hash值
        int index = (hash & 0x7FFFFFFF) % tab.length;     //確認該key的索引位置
        //迭代,尋找該key,替換
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                V old = e.value;
                e.value = value;
                return old;
            }
        }

        modCount++;
        if (count >= threshold) {  //若是容器中的元素數量已經達到閥值,則進行擴容操做
            rehash();
            tab = table;
            hash = hash(key);
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // 在索引位置處插入一個新的節點
        Entry<K,V> e = tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        //容器中元素+1
        count++;
        return null;
    }
複製代碼

      put方法的整個處理流程是:計算key的hash值,根據hash值得到key在table數組中的索引位置,而後迭代該key處的Entry鏈表(咱們暫且理解爲鏈表),若該鏈表中存在一個這個的key對象,那麼就直接替換其value值便可,不然在將改key-value節點插入該index索引位置處。以下:

      首先咱們假設一個容量爲5的table,存在八、十、1三、1六、1七、21。他們在table中位置以下:

繪圖1

     而後咱們插入一個數:put(16,22),key=16在table的索引位置爲1,同時在1索引位置有兩個數,程序對該「鏈表」進行迭代,發現存在一個key=16,這時要作的工做就是用newValue=22替換oldValue16,並將oldValue=16返回。

2014040302

      在put(33,33),key=33所在的索引位置爲3,而且在該鏈表中也沒有存在某個key=33的節點,因此就將該節點插入該鏈表的第一個位置。

2014040303vsd

         在HashTabled的put方法中有兩個地方須要注意:

        一、HashTable的擴容操做,在put方法中,若是須要向table[]中添加Entry元素,會首先進行容量校驗,若是容量已經達到了閥值,HashTable就會進行擴容處理rehash(),以下:

複製代碼
protected void rehash() {
        int oldCapacity = table.length;
        //元素
        Entry<K,V>[] oldMap = table;

        //新容量=舊容量 * 2 + 1
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        
        //新建一個size = newCapacity 的HashTable
        Entry<K,V>[] newMap = new Entry[];

        modCount++;
        //從新計算閥值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        //從新計算hashSeed
        boolean rehash = initHashSeedAsNeeded(newCapacity);

        table = newMap;
        //將原來的元素拷貝到新的HashTable中
        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;

                if (rehash) {
                    e.hash = hash(e.key);
                }
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = newMap[index];
                newMap[index] = e;
            }
        }
    }
複製代碼

      在這個rehash()方法中咱們能夠看到容量擴大兩倍+1,同時須要將原來HashTable中的元素一一複製到新的HashTable中,這個過程是比較消耗時間的,同時還須要從新計算hashSeed的,畢竟容量已經變了。這裏對閥值囉嗦一下:好比初始值十一、加載因子默認0.75,那麼這個時候閥值threshold=8,當容器中的元素達到8時,HashTable進行一次擴容操做,容量 = 8 * 2 + 1 =17,而閥值threshold=17*0.75 = 13,當容器元素再一次達到閥值時,HashTable還會進行擴容操做,一次類推。

     二、其實這裏是個人一個疑問,在計算索引位置index時,HashTable進行了一個與運算過程(hash & 0x7FFFFFFF),爲何須要作一步操做,這麼作有什麼好處?若是哪位知道,望指導,LZ不勝感激!!下面是計算key的hash值,這裏hashSeed發揮了做用。

private int hash(Object k) {
        return hashSeed ^ k.hashCode();
    }

     相對於put方法,get方法就會比較簡單,處理過程就是計算key的hash值,判斷在table數組中的索引位置,而後迭代鏈表,匹配直到找到相對應key的value,若沒有找到返回null。

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

4、HashTable與HashMap的區別

      HashTable和HashMap存在不少的相同點,可是他們仍是有幾個比較重要的不一樣點。

      第一:咱們從他們的定義就能夠看出他們的不一樣,HashTable基於Dictionary類,而HashMap是基於AbstractMap。Dictionary是什麼?它是任何可將鍵映射到相應值的類的抽象父類,而AbstractMap是基於Map接口的骨幹實現,它以最大限度地減小實現此接口所需的工做。

      第二:HashMap能夠容許存在一個爲null的key和任意個爲null的value,可是HashTable中的key和value都不容許爲null。以下:

      當HashMap遇到爲null的key時,它會調用putForNullKey方法來進行處理。對於value沒有進行任何處理,只要是對象均可以。

if (key == null)
            return putForNullKey(value);

      而當HashTable遇到null時,他會直接拋出NullPointerException異常信息。

if (value == null) {
            throw new NullPointerException();
        }

      第三:Hashtable的方法是同步的,而HashMap的方法不是。因此有人通常都建議若是是涉及到多線程同步時採用HashTable,沒有涉及就採用HashMap,可是在Collections類中存在一個靜態方法:synchronizedMap(),該方法建立了一個線程安全的Map對象,並把它做爲一個封裝的對象來返回,因此經過Collections類的synchronizedMap方法是能夠咱們你同步訪問潛在的HashMap。這樣君該如何選擇呢???

相關文章
相關標籤/搜索