Hashtable數據存儲結構-遍歷規則,Hash類型的複雜度爲啥都是O(1)-源碼分析(阿里)

hashmap的擴容因子是0.75 緣由 參考:HashMap默認加載因子爲何選擇0.75?(阿里)html

Hashtable 是一個很常見的數據結構類型,前段時間阿里的面試官說只要搞懂了HashTable,hashMap,HashSet,treeMap,treeSet這幾個數據結構,阿里的數據結構面試沒問題。java

一查才發現,這裏面的知識確實很多,都很經典,所以作一個專題面試

經過此文章,能夠了解到一下內容(我去美團,京東,阿里基本每次都問這幾個問題)算法

(1) Hashtable的存儲結構 (數組+鏈表)數組

(2)Hashtable的擴容原理,擴容因子0.75,bucket的初始大小11.(擴容的函數爲2N+1,hashMap的擴容函數是2N,之因此是2的倍數,是由於,Hashtable爲了保證速度,擴容直接位移<<1這樣就是2的倍數)安全

(3)添加,查找操做的深層次原理,數據結構

(4)搜素的幾種方法,以及爲何會產生這幾種搜索方法。函數

 首先總覽一下:post

Hashtable與Map關係以下圖:性能

從圖中能夠看出: 
(1) Hashtable繼承於Dictionary類,實現了Map接口。Map是"key-value鍵值對"接口,Dictionary是聲明瞭操做"鍵值對"函數接口的抽象類。 
(2) Hashtable是經過"拉鍊法"實現的哈希表。它包括幾個重要的成員變量:table, count, threshold, loadFactor, modCount。
  table是一個Entry[]數組類型,而Entry實際上就是一個單向鏈表。哈希表的"key-value鍵值對"都是存儲在Entry數組中的。 
  count是Hashtable的大小,它是Hashtable保存的鍵值對的數量。 
  threshold是Hashtable的閾值,用於判斷是否須要調整Hashtable的容量。threshold的值="容量*加載因子"。
  loadFactor就是加載因子。 
  modCount是用來實現fail-fast機制的  

HashMap同樣,Hashtable 也是一個散列表,它存儲的內容是鍵值對(key-value)映射, 都是數組+鏈表的形式存儲數據:

定義以下:

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

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

}

由此能看出兩點:

(1)、Hashtable默認 bucket 容量是 11 ,擴容因子是0.75.

也就是說 若是 如今咱們建立一個Hashtable,若是裏面有8個數值 ,由於:8>=11*0.75;那麼,在添加到第8個數值的時候,Hashtable會擴容,

Hashtable 的實例有兩個參數影響其性能:初始容量 和 加載因子。容量 是哈希表中桶 的數量,初始容量 就是哈希表建立時的容量。注意,哈希表的狀態爲 open:在發生「哈希衝突」的狀況下,單個桶會存儲多個條目,這些條目必須按順序搜索。加載因子 是對哈希表在其容量自動增長以前能夠達到多滿的一個尺度。初始容量和加載因子這兩個參數只是對該實現的提示。關於什麼時候以及是否調用 rehash 方法的具體細節則依賴於該實現。一般,默認加載因子是 0.75, 這是在時間和空間成本上尋求一種折衷。加載因子太高雖然減小了空間開銷,但同時也增長了查找某個條目的時間(在大多數 Hashtable 操做中,包括 get 和 put 操做,都反映了這一點)。

這是Hashtable的構造函數:默認初始容量是11,而加載因子是0.75;   

 protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        // overflow-conscious code
        int newCapacity = (oldCapacity << 1) + 1; if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }}

紅色的字體代表 Hashtable 擴容的函數是直接左移動1位,並加一,也就是:擴大爲原來的2n+1;

(2)、Hashtable 繼承於Dictionary,實現了Map、Cloneable、java.io.Serializable接口。 

Hashtable包含的方法 :elements() ,其做用是返回「全部value」的枚舉對象  

public synchronized Enumeration<V> elements() {
    return this.<V>getEnumeration(VALUES);
 }

// 獲取Hashtable的枚舉類對象
private <T> Enumeration<T> getEnumeration(int type) {
if (count == 0) {
return Collections.emptyEnumeration();
} else {
return new Enumerator<>(type, false);
}
}

從中,咱們能夠看出:

(1) 若Hashtable的實際大小爲0,則返回「空枚舉類」對象emptyEnumerator;
(2) 不然,返回正常的Enumerator的對象。(Enumerator實現了迭代器和枚舉兩個接口,請注意這兩個接口,這是咱們後面介紹搜索方法時,會涉及到的)

咱們先看看emptyEnumerator對象是如何實現的  

private static Enumeration emptyEnumerator = new EmptyEnumerator();
// 空枚舉類                                                                                         
// 當Hashtable的實際大小爲0;此時,又要經過Enumeration遍歷Hashtable時,返回的是「空枚舉類」的對象。   
private static class EmptyEnumerator implements Enumeration<Object> {                               
                                                                                                    
    EmptyEnumerator() {                                                                             
    }                                                                                               
                                                                                                    
    // 空枚舉類的hasMoreElements() 始終返回false                                                    
    public boolean hasMoreElements() {                                                              
        return false;                                                                               
    }                                                                                               
                                                                                                    
    // 空枚舉類的nextElement() 拋出異常                                                             
    public Object nextElement() {                                                                   
        throw new NoSuchElementException("Hashtable Enumerator");                                   
    }                                                                                               
}                                                                                                   

咱們在來看看Enumeration類,Enumerator的做用是提供了「經過elements()遍歷Hashtable的接口」 和 「經過entrySet()遍歷Hashtable的接口」。由於,它同時實現了 「Enumerator接口」和「Iterator接口」。 

 private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
        Entry<?,?>[] table = Hashtable.this.table;
        int index = table.length;
        Entry<?,?> entry;
        Entry<?,?> lastReturned;
        int type;

....
}

 三、如下爲Hashtable 包含的函數,函數都是同步的,每一個前面都有synchronized,這意味着它是線程安全的。

  public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
....
}

 由此咱們也能看出:Hashtable的key、value都不能夠爲null。

看源碼:若是value爲空 拋出異常,若是 key爲空 key.hashCode會拋出異常

咱們都知道:Hashtable 的key 和value 都不能爲空,HashMap的key 和value 均可覺得空,就是這個緣由。

此外,Hashtable中的映射不是有序的。  

四、 Hashmap同樣,Hashtable也是一個散列表,它也是經過「拉鍊法」解決哈希衝突的。

Hashtable的「拉鍊法」相關內容

Hashtable數據存儲數組,是由一個Entry數組組成的,而 Entry 自己是多個key,value的鏈表,其中鏈表中的每一個值都有個next指針,指向本鏈表的下一個元素。

private transient Entry[] table; 

Hashtable中的key-value都是存儲在table數組中的。 以下所示,數據節點Entry的數據結構 

private static class Entry<K,V> implements Map.Entry<K,V> {   
  // 哈希值                                                                                       
     int hash;                                                                                       
     K key;                                                                                          
     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;                                                                           
     }                                                                                               
                                                                                                     
     protected Object clone() {                                                                      
         return new Entry<K,V>(hash, key, value,                                                     
               (next==null ? null : (Entry<K,V>) next.clone()));                                     
     }                                                                                               
                                                                                                     
     public K getKey() {                                                                             
         return key;                                                                                 
     }                                                                                               
                                                                                                     
     public V getValue() {                                                                           
         return value;                                                                               
     }                                                                                               
                                                                                                     
     // 設置value。若value是null,則拋出異常。                                                       
     public V setValue(V value) {                                                                    
         if (value == null)                                                                          
             throw new NullPointerException();                                                       
                                                                                                     
         V oldValue = this.value;                                                                    
         this.value = value;                                                                         
         return oldValue;                                                                            
     }                                                                                               
                                                                                                     
     // 覆蓋equals()方法,判斷兩個Entry是否相等。                                                    
     // 若兩個Entry的key和value都相等,則認爲它們相等。                                              
     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()));                         
     }                                                                                               
                                                                                                     
     public int hashCode() {                                                                         
         return hash ^ (value==null ? 0 : value.hashCode());                                         
     }                                                                                               
                                                                                                     
     public String toString() {                                                                      
         return key.toString()+"="+value.toString();                                                 
     }                                                                                               
 }                                                                                                    

從中,咱們能夠看出 Entry 實際上就是一個單向鏈表。這也是爲何咱們說Hashtable是經過拉鍊法解決哈希衝突的。

Entry 實現了Map.Entry 接口,即實現getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函數。這些都是基本的讀取/修改key、value值的函數。  

 拿put()方法舉例: put() 的做用是對外提供接口,讓Hashtable對象能夠經過put()將「key-value」添加到Hashtable中。  

流程大致是先判斷 hash值,而後判斷equals值

PUT流程圖:

若是對hashcode和equals 方法的區別不瞭解能夠參考:Java == ,equals 和 hashcode 的區別和聯繫(阿里面試)

put 方法的整個流程爲:

  1. 判斷 value 是否爲空,爲空則拋出異常;
  2. 計算 key 的 hash 值,並根據 hash 值得到 key 在 table 數組中的位置 index,若是 table[index] 元素不爲空,則進行迭代,若是遇到相同的 key,則直接替換,並返回舊 value;
  3. 不然,咱們能夠將其插入到 table[index] 位置。

 

public synchronized V put(K key, V value) {   
 // Hashtable中不能插入value爲null的元素!!!                                                                                             
    if (value == null) { throw new NullPointerException(); } // 若「Hashtable中已存在鍵爲key的鍵值對」, // 則用「新的value」替換「舊的value」 Entry tab[] = table; int hash = key.hashCode(); 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)) { V old = e.value; e.value = value; return old; } } // 若「Hashtable中不存在鍵爲key的鍵值對」, // (01) 將「修改統計數」+1 modCount++; // (02) 若「Hashtable實際容量」 > 「閾值」(閾值=總的容量 * 加載因子) // 則調整Hashtable的大小 if (count >= threshold) { // Rehash the table if the threshold is exceeded  rehash(); tab = table; index = (hash & 0x7FFFFFFF) % tab.length; } // (03) 將「Hashtable中index」位置的Entry(鏈表)保存到e中 Entry<K,V> e = tab[index]; // (04) 建立「新的Entry節點」,並將「新的Entry」插入「Hashtable的index位置」,並設置e爲「新的Entry」的下一個元素(即「新Entry」爲鏈表表頭)。 tab[index] = new Entry<K,V>(hash, key, value, e); // (05) 將「Hashtable的實際容量」+1 count++; return null; } 

 

經過一個實際的例子來演示一下這個過程:

假設咱們如今Hashtable的容量爲5,已經存在了(5,5),(13,13),(16,16),(17,17),(21,21)這 5 個鍵值對,目前他們在Hashtable中的位置以下:

如今,咱們插入一個新的鍵值對,put(16,22),假設key=16的索引爲1.但如今索引1的位置有兩個Entry了,因此程序會對鏈表進行迭代。迭代的過程當中,發現其中有一個Entry的key和咱們要插入的鍵值對的key相同,因此如今會作的工做就是將newValue=22替換oldValue=16,而後返回oldValue=16.

而後咱們如今再插入一個,put(33,33),key=33的索引爲3,而且在鏈表中也不存在key=33的Entry,因此將該節點插入鏈表的第一個位置。

 

 

 再看一下Get()方法,咱們知道Hashtable的時間複雜度是O(1),但你知道它是如何經過散列碼的方式作到O(1)的嗎?

Hashtable 直接用hash取了hashtable模,用模作了index,而後定位到bucket桶的數組位置,這個位置上面可能有一個hashcode相同的entry鏈表;而後對這鏈表進行遍歷,找到key等於指定值的entry,所以 時間複雜度爲O(1),HashMap,HashTable,HashSet 只要是以Hash爲基礎的數據結構都是O(1)

參考:HashMap, HashTable,HashSet,TreeMap 的時間複雜度

get() 的做用就是獲取key對應的value,沒有的話返回null 

   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; }

  

相比較於 put 方法,get 方法則簡單不少。其過程就是首先經過 hash()方法求得 key 的哈希值,而後根據 hash 值獲得 index 索引(上述兩步所用的算法與 put 方法都相同)。而後迭代鏈表,返回匹配的 key 的對應的 value;找不到則返回 null。  

五、剛纔提到 Hashtable 繼承了 繼承了字典類型:Dictionary類型。而字典類型依賴於: Enumerator 

Enumerator實現了方法:Enumeration<T>, Iterator<T> 

private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
        Entry<?,?>[] table = Hashtable.this.table;
        int index = table.length;
        Entry<?,?> entry;
        Entry<?,?> lastReturned;
        int type;

        /**
         * Indicates whether this Enumerator is serving as an Iterator
         * or an Enumeration.  (true -> Iterator).
         */
        boolean iterator;

 所以:搜索有五種方法進行搜素:

(1) 利用Iterator迭代器,遍歷Hashtable的鍵值對

第一步:根據entrySet()獲取Hashtable的「鍵值對」的Set集合。
第二步:經過Iterator迭代器遍歷「第一步」獲得的集合。   

    Iterator iter=table.entrySet().iterator();
        
        while(iter.hasNext()){
            Entry entry =(Entry) iter.next();
            //獲取key
            String key=(String)entry.getKey();
            
            Object value=entry.getValue();
            
            System.out.println("key="+key+"  value="+value);
        }
        

 

(2) 經過Iterator遍歷Hashtable的鍵

第一步:根據keySet()獲取Hashtable的「鍵」的Set集合。
第二步:經過Iterator迭代器遍歷「第一步」獲得的集合。 

// 假設table是Hashtable對象
// table中的key是String類型,value是Integer類型
String key = null;
Integer integ = null;
Iterator iter = table.keySet().iterator();
while (iter.hasNext()) {
        // 獲取key
    key = (String)iter.next();
        // 根據key,獲取value
    integ = (Integer)table.get(key);
}

 (3)、經過Iterator遍歷Hashtable的值

第一步:根據value()獲取Hashtable的「值」的集合。
第二步:經過Iterator迭代器遍歷「第一步」獲得的集合。 

// 假設table是Hashtable對象
// table中的key是String類型,value是Integer類型
Integer value = null;
Collection c = table.values();
Iterator iter= c.iterator();
while (iter.hasNext()) {
    value = (Integer)iter.next();
}

 (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());
}

遍歷測試程序以下: 

import java.util.Collection;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map.Entry;

public class hashtabletest {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        
        Hashtable table =new Hashtable();
        
        table.put("張三",20);
        table.put("李四",30);
        table.put("王五", 50);
         
        
//        4.1 遍歷Hashtable的鍵值對
//
//        第一步:根據entrySet()獲取Hashtable的「鍵值對」的Set集合。
//        第二步:經過Iterator迭代器遍歷「第一步」獲得的集合。
        Iterator iter=table.entrySet().iterator();
        
        while(iter.hasNext()){
            Entry entry =(Entry) iter.next();
            //獲取key
            String key=(String)entry.getKey();
            
            Object value=entry.getValue();
            
            System.out.println("key="+key+"  value="+value);
        }
        
//        //4.2 經過Iterator遍歷Hashtable的鍵

//第一步:根據keySet()獲取Hashtable的「鍵」的Set集合。
//第二步:經過Iterator迭代器遍歷「第一步」獲得的集合。
        
        Iterator itkey=table.keySet().iterator();
        while(itkey.hasNext()){
            
      String key=(String)        itkey.next();
      Object value=table.get(key);
      
      System.out.println("key=="+key+"  value="+value);
        }
        
        
//        4.3 經過Iterator遍歷Hashtable的值
//
//        第一步:根據value()獲取Hashtable的「值」的集合。
//        第二步:經過Iterator迭代器遍歷「第一步」獲得的集合。
        
    Collection c=      table.values();
    Iterator itvalue=c.iterator();
    while(itvalue.hasNext()){
        
        Object value =itvalue.next();        
        
        System.out.println(" value="+value);
    }
    
//    4.4 經過Enumeration遍歷Hashtable的鍵
//
//    第一步:根據keys()獲取Hashtable的集合。
//    第二步:經過Enumeration遍歷「第一步」獲得的集合。
    
    Enumeration enu=table.keys();
    while(enu.hasMoreElements()){
        
        System.out.println("elements="+enu.nextElement());
    }
    
//    4.5 經過Enumeration遍歷Hashtable的值
//    第一步:根據elements()獲取Hashtable的集合。
//    第二步:經過Enumeration遍歷「第一步」獲得的集合。
    
    Enumeration entry=table.elements();
    while(entry.hasMoreElements()){
        
        System.out.println(" element111s ="+entry.nextElement());
    }
        
        

    }

}

 結果爲:

key=王五  value=50
key=張三  value=20
key=李四  value=30
key==王五  value=50
key==張三  value=20
key==李四  value=30
 value=50
 value=20
 value=30
elements=王五
elements=張三
elements=李四
 element111s =50
 element111s =20
 element111s =30

  六、其餘的函數  

(1) contains() 和 containsValue()

contains() 和 containsValue() 的做用都是判斷Hashtable是否包含「值(value)」

public boolean containsValue(Object value) {
    return contains(value);
  }

 

remove()   remove() 的做用就是刪除Hashtable中鍵爲key的元素  

 Hashtable實現的Cloneable接口  Hashtable實現了Cloneable接口,即實現了clone()方法。

clone()方法的做用很簡單,就是克隆一個Hashtable對象並返回。  

 Hashtable實現的Serializable接口,分別實現了串行讀取、寫入功能。

串行寫入函數就是將Hashtable的「總的容量,實際容量,全部的Entry」都寫入到輸出流中
串行讀取函數:根據寫入方式讀出將Hashtable的「總的容量,實際容量,全部的Entry」依次讀出   

參考:深刻Java集合學習系列:Hashtable的實現原理
參考
HashTable的實現原理分析

相關文章
相關標籤/搜索