Java 數據結構——Hashbable底層源碼分析

目錄

 

1、概述

2、原理(圖解)

3、源碼分析

4、知識延伸


一、概述

  • Hashtable也稱爲散列表,它存儲的內容是鍵值對(key-value)映射,是根據關鍵字值(key value)直接進行訪問的數據結構。也就是說,它通過把關鍵字值映射到一個位置來訪問記錄,以加快查找的速度。這個映射函數稱爲哈希函數(也稱爲散列函數),映射過程稱爲哈希化,存放記錄的數組叫做散列表。
  • Hashtable繼承於Dictionary,實現了Map、Cloneable、java.io.Serializable接口。
  • Hashtable的函數都是同步的,這意味着它是線程安全的。它的key、value都不可以爲null,Hashtable中的映射不是有序的。
  • Hashtable的實例有兩個參數影響其性能:   初始容量(initialCapacity) :哈希表中桶 的數量,初始容量 就是哈希表創建時的容量;    負載因子(load Factor):負載因子哈希表是0.1 到 1.0 範圍內的數字,當容量自動增加(擴容rehash)之前允許哈希表得到滿足的度量。初始容量和負載因子這兩個參數只是對該實現的提示。通常,默認負載因子是 0.75, 這是在時間和空間成本上尋求一種折衷。負載因子過高雖然減少了空間開銷,但同時也增加了查找某個條目的時間。

 

二、原理(圖解)

三、源碼分析

1、相關參數定義:

 /**
  * 爲一個Entry[]數組類型,Entry代表了“拉鍊”的節點,每一個Entry代表了一個鍵值對,哈希表的"key- 
    value鍵值對"都是存儲在Entry數組中的。
  */
private transient Entry<?,?>[] table;

 /**
  * 哈希表中條目的總數
  */
private transient int count;

 /**
  * table數組擴容的節點數閾值,以此來判斷是否達到擴容標準
  */
private int threshold;

 /**
  * 負載因子默認0.75f
  */
private float loadFactor;

 /**
  * Hashtable被修改次數,用來實現“fail-fast”機制的(也就是快速失敗)。
  */
private transient int modCount = 0;

2、構造函數:

// 初始化默認構造函數。
public Hashtable() {
  //默認容量爲11 負載因子爲0.75
  this(11, 0.75f);
} 

// 初始化指定“容量大小”的構造函數
public Hashtable(int initialCapacity) {
   this(initialCapacity, 0.75f);
}

// 初始化指定“容量大小”和“加載因子”的構造函數
public Hashtable(int initialCapacity, float loadFactor) {
   //傳入容量不能<0,否則會報異常 
   if (initialCapacity < 0)
      throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);

   //傳入的加載因子範圍在0——1之間,默認0.75 建議設置在0.7——0.75之間,否則也會報異常
   if (loadFactor <= 0 || Float.isNaN(loadFactor))
      throw new IllegalArgumentException("Illegal Load: "+loadFactor);

   //如果傳入的容量爲0,那麼會默認把容量初始爲1,可看出容量默認爲11,最小爲1
   if (initialCapacity==0)
      initialCapacity = 1;
      this.loadFactor = loadFactor;
      //創建容量爲initialCapacity的Entry數組table
      table = new Entry<?,?>[initialCapacity];

      //計算出數組的閥值,也算一個臨界值吧(閥值表示當table的長度達到這個閥值(臨界值)之後就會觸 
      //發擴容機制(rehash))
      //計算閥值的公式:閥值=容量*負載因子 與 當前系統數組最大長度+1 的最小值
      //MAX_ARRAY_SIZE =Integer.MAX_VALUE - 8

      threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

// 初始化包含“子Map”的構造函數
public Hashtable(Map<? extends K, ? extends V> t) {

   // 初始容器取map最大尺寸*2和11取大的,也就是最小也是11,負載因子0.75
   this(Math.max(2*t.size(), 11), 0.75f);
   //將傳入的map放入數組
   putAll(t);
}

//將傳入的map放入數組
public synchronized void putAll(Map<? extends K, ? extends V> t) {
   //遍歷循環傳入的map,調用put放入數組
   for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
      //放入
      put(e.getKey(), e.getValue());
}

接下來主要分析常用的幾個方法(get / put / remove / rehash) ,在這個之前我們先看下Entry這個類

3、Entry類分析

//Entry實際上就是一個單向鏈表。哈希表的"key-value鍵值對"都是存儲在Entry數組中的。 
private static class Entry<K,V> implements Map.Entry<K,V> {
        //定義一個int類型的hash字段
        final int hash;
        //定義key字段
        final K key;
        //定義value字段
        V value;
        //存儲下一個Entry數組對象
        Entry<K,V> next;

        //構造函數
        protected Entry(int hash, K key, V value, Entry<K,V> next) {
            this.hash = hash;
            this.key =  key;
            this.value = value;
            this.next = next;
        }
        
        //克隆
        @SuppressWarnings("unchecked")
        protected Object clone() {
            return new Entry<>(hash, key, value,
                                  (next==null ? null : (Entry<K,V>) next.clone()));
        }

        // 獲取Entry中的key
        public K getKey() {
            return key;
        }

        //獲取Entry中的value值
        public V getValue() {
            return value;
        }
        
        //設置Entry中的value值  可以看出hashtable的key和value都不能爲null,否則會拋異常
        public V setValue(V value) {
            if (value == null)
                throw new NullPointerException();

            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }
        
        //比較equals
        public boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;

            return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
               (value==null ? e.getValue()==null : value.equals(e.getValue()));
        }
        
        //獲取hashCode
        public int hashCode() {
            return hash ^ Objects.hashCode(value);
        }
        
        
        public String toString() {
            return key.toString()+"="+value.toString();
        }
    }

4、get方法

//根據key從哈希表中獲取對應的value值
//獲取方法同步鎖synchronized 
public synchronized V get(Object key) {

        //創建Entry[]數組類型
        Entry<?,?> tab[] = table;

        //獲取key的hash值
        int hash = key.hashCode();

        //再根據hash值和table的長度計算出在table數組中的索引位置
        //擴展:hash值爲int類型 4個字節 32bit.
        //     爲了在hash爲負值的情況下,去掉起符號位,所以和0x7FFFFFFF進行&操作
        //     0x7FFFFFFF 二進制 0111 1111 1111 1111 1111 1111 1111 1111
        //     負數與其進行&操作將產生一個正整數

        int index = (hash & 0x7FFFFFFF) % tab.length;

        //遍歷對應位置的鏈表
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {

            //在鏈表中查找hash值和key值都相等的元素
            if ((e.hash == hash) && e.key.equals(key)) {

                //返回節點的值
                return (V)e.value;
            }
        }

        //沒有找到則返回null
        return null;
    }

5、put放入哈希表

 

 //添加鍵值對   
 public synchronized V put(K key, V value) {
        //判斷value是否爲空
        if (value == null) {
            throw new NullPointerException();
        }

        Entry<?,?> tab[] = table;

        //先獲取key的hash值
        int hash = key.hashCode();

        //再根據hash值和table的長度計算出在table數組中的索引位置
        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)) {
                //若添加的key在Hashtable已經存在,則用新value覆蓋原有的value
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        //添加鏈表節點
        addEntry(hash, key, value, index);
        return null;
    }


//添加鏈表節點
private void addEntry(int hash, K key, V value, int index) {

        //更改次數加1
        modCount++;

        Entry<?,?> tab[] = table;

        //當哈希表實際容量>=哈希表的閥值(臨界值),觸發擴容操作
        if (count >= threshold) {
            //進行擴容操作
            rehash();
            
            //將擴容後的table賦值給新創建的Entry數組tab[]
            tab = table;

            //先獲取key的hash值
            hash = key.hashCode();

            //再根據hash值和table的長度計算出在table數組中的索引位置
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // 創建一個新的Entry數組
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);

        //哈希表哈希表實際容量+1
        count++;
    }

6、rehash 擴容解析 

//擴容操作
protected void rehash() {
        //獲得擴容前數組容量
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        //計算得出新的數組容量,新數組容量=舊數組容量*2+1(<<1表示右移一位 <<1=2的一次方=2)
        int newCapacity = (oldCapacity << 1) + 1;
        
        //如果新數組容量>數組規定的最大容量限制,則使用最大限制容器值 MAX_ARRAY_SIZE
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            //如果舊的數組容量=MAX_ARRAY_SIZE則
            if (oldCapacity == MAX_ARRAY_SIZE)

                return;
            newCapacity = MAX_ARRAY_SIZE;
        }

        //創建一個容量爲newCapacity的新的數組對象
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

        //容器更改次數+1
        modCount++;

        //獲取新的數組閥值(臨界值)
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);

        //將新的數組賦值給table參數
        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;
            }
        }
    }

四、Hashtable知識延伸

1)Java中==,equals和hashCode有什麼區別和聯繫?https://www.cnblogs.com/aspirant/p/7079538.html

2)爲什麼加載因子要用0.75? https://www.cnblogs.com/aspirant/p/11470928.html

3)爲什麼獲取數組下標時要 key.hashCode() & 0x7fffffff ?https://blog.csdn.net/weixin_39590058/article/details/88925659

4)爲什麼擴容是2N+1?

5)負載因子值的大小,對HashMap有什麼影響?

6) Hashtable的複雜度爲什麼是O(1)?

7)Hashtable最大容量爲什麼是2^31-8?https://blog.csdn.net/ChineseYoung/article/details/80787071

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章