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


hashmap的擴容因子是0.75 原因 參考:HashMap默認加載因子為什么選擇0.75?(阿里)

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

一查才發現,這里面的知識確實不少,都很經典,因此做一個專題

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

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

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

(3)添加,查找操作的深層次原理,

(4)搜素的幾種方法,以及為什么會產生這幾種搜索方法。

 首先總覽一下:

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;

....
}

 3、以下為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中的映射不是有序的。  

4、 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。  

5、剛才提到 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

  6、其他的函數  

(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的實現原理分析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM