深入Java基礎(四)--哈希表(2)HashTable與HashSet應用及源碼詳解

又突然想看源碼了,繼續深入Java基礎系列。今天是研究JavaAPI的HashTable和HashSet(順帶討論線程安全問題)。

本系列:

(1)深入Java基礎(一)——基本數據類型及其包裝類

(2)深入Java基礎(二)——字符串家族

(3)深入Java基礎(三)--集合(1)集合父類以及父接口源碼及理解

(4)深入Java基礎(三)--集合(2)ArrayList和其繼承樹源碼解析以及其注意事項

(5)深入Java基礎(四)--哈希表(1)HashMap應用及源碼詳解

文章結構:(1)HashTable和HashSet概述與基本操作(含線程安全討論);(2)HashTable源碼分析;(3)HashSet源碼分析。


一、HashTable和HashSet概述與基本操作(含線程安全討論):

(1)HashTable:

(一)概述(與HashMap對比進行講述):此概述大部分參考此博客

1)同是散列表,存儲的內容是鍵值對(key-value)映射。(相同點)

HashMap 是基于“拉鏈法”實現的散列表。

Hashtable 也是基于“拉鏈法”實現的散列表。

存取的模式也是相同

2)繼承與實現的不同:

HashMap 繼承于AbstractMap,實現了Map、Cloneable、java.io.Serializable接口。

Hashtable 繼承于Dictionary,實現了Map、Cloneable、java.io.Serializable接口。

Dictionary是一個抽象類,它直接繼承于Object類,沒有實現任何接口。Dictionary類是JDK 1.0的引入的。雖然Dictionary也支持“添加key-value鍵值對”、“獲取value”、“獲取大小”等基本操作,但它的API函數比Map少;而且Dictionary一般是通過Enumeration(枚舉類)去遍歷,Map則是通過Iterator(迭代器)去遍歷。 然而由于Hashtable也實現了Map接口,所以,它即支持Enumeration遍歷,也支持Iterator遍歷。
AbstractMap是一個抽象類,它實現了Map接口的絕大部分API函數;為Map的具體實現類提供了極大的便利。它是JDK 1.2新增的類。

3)線程安全不同:

Hashtable的幾乎所有函數都是同步的,即它是線程安全的,支持多線程。

而HashMap的函數則是非同步的,它不是線程安全的。若要在多線程中使用HashMap,需要我們額外的進行同步處理(Collections類提供的synchronizedMap靜態方法或者使用ConcurrentHashMap類)。

//HashMap可以通過下面的語句進行同步:
Map m = Collections.synchronizeMap(hashMap);

4)對null的處理不同:(一會源碼解析,在put方法里)

HashMap的key、value都可以為null。

Hashtable的key、value都不可以為null。

Hashtable的key或value,都不能為null!否則,會拋出異常NullPointerException。
HashMap的key、value都可以為null。 當HashMap的key為null時,HashMap會將其固定的插入table[0]位置(即HashMap散列表的第一個位置);而且table[0]處只會容納一個key為null的值,當有多個key為null的值插入的時候,table[0]會保留最后插入的value。

5)支持的遍歷種類不同:

HashMap只支持Iterator(迭代器)遍歷。

而Hashtable支持Iterator(迭代器)和Enumeration(枚舉器)兩種方式遍歷。

6)通過Iterator迭代器遍歷時,遍歷的順序不同:

HashMap是“從前向后”的遍歷數組;再對數組具體某一項對應的鏈表,從表頭開始進行遍歷。

Hashtabl是“從后往前”的遍歷數組;再對數組具體某一項對應的鏈表,從表頭開始進行遍歷。

7)容量的初始值 和 增加方式都不一樣:

HashMap默認的容量大小是16;增加容量時,每次將容量變為“原始容量x2”。

Hashtable默認的容量大小是11;增加容量時,每次將容量變為“原始容量x2 + 1”。

8)添加key-value時的hash值算法不同

HashMap添加元素時,是使用自定義的哈希算法。

Hashtable沒有自定義哈希算法,而直接采用的key的hashCode()。

9)部分API不同:

Hashtable支持contains(Object value)方法,而且重寫了toString()方法;

而HashMap不支持contains(Object value)方法,沒有重寫toString()方法。

但兩者都有containsKey()方法

(二)基本操作:

public class TestHashTable {
   public static void main(String[] args){
       Hashtable <String, String> hashtable = new Hashtable();

       hashtable.put("1", "aa");
       hashtable.put("2", "bb");
       hashtable.put("3", "cc");
       hashtable.put("4", "dd");
        //[1]contains與containsKey方法
       if (hashtable.contains("aa")){
           System.out.println("contains");
       }
       if (hashtable.containsKey("1")){
           System.out.println("containsKey");
       }

       System.out.println("====================================");
       //[2]toString()方式打印
       System.out.println(hashtable.toString());
       System.out.println("====================================");
       //[3]Iterator遍歷方式1--鍵值對遍歷entrySet()
       Iterator<Map.Entry<String, String>> iter = hashtable.entrySet().iterator();
       while(iter.hasNext()){
           Map.Entry<String, String> entry = (Map.Entry<String, String>)iter.next();
           String key = entry.getKey();
           String value = entry.getValue();
           System.out.println("entrySet:"+key+" "+value);
       }

       System.out.println("====================================");

       //[4]Iterator遍歷方式2--key鍵的遍歷
       Iterator<String> iterator = hashtable.keySet().iterator();
       while(iterator.hasNext()){
           String key = (String)iterator.next();
           String value = hashtable.get(key);
           System.out.println("keySet:"+key+" "+value);
       }

       System.out.println("====================================");

       //[5]通過Enumeration來遍歷Hashtable
       Enumeration<String> enu = hashtable.keys();
       while(enu.hasMoreElements()) {
           System.out.println("Enumeration:"+hashtable.keys()+" "+enu.nextElement());
       }


   }
}

(三)線程安全的討論(含例子):

1)我們先去測試探討hashmap

HashMap應用及源碼詳解本系列上篇文章講到其源碼:

HashMap實現不是同步的。如果多個線程同時訪問一個哈希映射,而其中至少一個線程從結構上修改了該映射,則它必須保持外部同步。(結構上的修改是指添加或刪除一個或多個映射關系的任何操作;僅改變與實例已經包含的鍵關聯的值不是結構上的修改。)這一般通過對自然封裝該映射的對象進行同步操作來完成。如果不存在這樣的對象,則應該使用 Collections.synchronizedMap 方法來“包裝”該映射。最好在創建時完成這一操作,以防止對映射進行意外的非同步訪問,如下所示: 
Map m = Collections.synchronizedMap(new HashMap(...));

可知大致是三個操作塊是會出現并發問題:

1. HashMap中插入數據的時候

假如A線程和B線程同時進入addEntry,然后計算出了相同的哈希值對應了相同的數組位置,因為此時該位置還沒數據,然后對同一個數組位置調用createEntry,兩個線程會同時得到現在的頭結點,然后A寫入新的頭結點之后,B也寫入新的頭結點,那B的寫入操作就會覆蓋A的寫入操作造成A的寫入操作丟失。

2.HashMap擴容的時候

當多個線程同時進來,檢測到總數量超過門限值的時候就會同時調用resize操作,各自生成新的數組并rehash后賦給該map底層的數組table,結果最終只有最后一個線程生成的新數組被賦給table變量,其他線程的均會丟失。而且當某些線程已經完成賦值而其他線程剛開始的時候,就會用已經被賦值的table作為原始數組,這樣也會有問題。

3. 刪除HashMap中數據的時候都容易出現線程安全問題。

刪除這一塊可能會出現兩種線程安全問題,第一種是一個線程判斷得到了指定的數組位置i并進入了循環,此時,另一個線程也在同樣的位置已經刪掉了i位置的那個數據了,然后第一個線程那邊就沒了。但是刪除的話,沒了倒問題不大。
再看另一種情況,當多個線程同時操作同一個數組位置的時候,也都會先取得現在狀態下該位置存儲的頭結點,然后各自去進行計算操作,之后再把結果寫會到該數組位置去,其實寫回的時候可能其他的線程已經就把這個位置給修改過了,就會覆蓋其他線程的修改。

例子驗證:

//(注釋的方式為線程不安全,沒注釋的是線程安全做法)
public class TestHashMapConcurrent {

    public static final HashMap<String, String> hashMap = new HashMap<String, String>();

    public static void main(String[] args) throws InterruptedException {
//就是在這里使用集合工具類,包裝hashmap,使其線程安全
        //Collections.synchronizedMap(hashMap);
        //線程一
        Thread t1 = new Thread() {
            public void run() {
                for (int i = 0; i < 25; i++) {
                    hashMap.put(String.valueOf(i), String.valueOf(i));
                }
            }
        };
        //線程二
        Thread t2 = new Thread() {
            public void run() {
                for (int j = 25; j < 50; j++) {
                    hashMap.put(String.valueOf(j), String.valueOf(j));
                }
            }
        };

        t1.start();
        t2.start();

        //主線程休眠1秒鐘,以便t1和t2兩個線程將firstHashMap填裝完畢。
        Thread.currentThread().sleep(1000);

        for (int l = 0; l < 50; l++) {
            //如果key和value不同,說明在兩個線程put的過程中出現異常。
            if (!String.valueOf(l).equals(hashMap.get(String.valueOf(l)))) {
                System.err.println(String.valueOf(l) + ":" + hashMap.get(String.valueOf(l)));
            }
        }

    }
}

用這個例子跑多幾次,就可以發現:key與value不對應。也可發現,是擴容或者增加鍵值導致的線程問題。

這里寫圖片描述

2)我們說過Hashtable是線程安全的,,例子說明,無論跑多少次都是key與value對應

public class ConcurrentTestHashTable {
    private static Map<String, String> hashtable = new Hashtable<String, String>();//定義個屬性變量,方便內部多線程訪問

    public static void main(String[] args) throws InterruptedException {

        //線程一
        Thread t1 = new Thread() {
            public void run() {
                for (int i = 0; i < 25; i++) {
                    hashtable.put(String.valueOf(i), String.valueOf(i));
                }
            }
        };
        //線程二
        Thread t2 = new Thread() {
            public void run() {
                for (int j = 25; j < 50; j++) {
                    hashtable.put(String.valueOf(j), String.valueOf(j));
                }
            }
        };

        t1.start();
        t2.start();

        //主線程休眠1秒鐘,以便t1和t2兩個線程將firstHashMap填裝完畢。
        Thread.currentThread().sleep(1000);

        for (int l = 0; l < 50; l++) {
            //如果key和value不同,說明在兩個線程put的過程中出現異常。
            if (!String.valueOf(l).equals(hashtable.get(String.valueOf(l)))) {
                System.err.println(String.valueOf(l) + ":" + hashtable.get(String.valueOf(l)));
            }
        }

    }
}

(2)HashSet:

(一)概述:

1)一個沒有重復元素的集合。

2)由HashMap實現的,不保證元素的順序,而且HashSet允許使用 null 元素。

3)HashSet是非同步的。如果多個線程同時訪問一個哈希 set,而其中至少一個線程修改了該 set,那么它必須 保持外部同步。這通常是通過對自然封裝該 set 的對象執行同步操作來完成的。如果不存在這樣的對象,則應該使用 Collections.synchronizedSet 方法來“包裝” set。最好在創建時完成這一操作,以防止對該 set 進行意外的不同步訪問:

Set s = Collections.synchronizedSet(new HashSet(...));

4)HashSet通過iterator()返回的迭代器是fail-fast的。

5)HashSet繼承于AbstractSet,并且實現了Set接口。HashSet中含有一個"HashMap類型的成員變量"map,HashSet的操作函數,實際上都是通過map實現的。

(二)基本操作:

public class TestHashSet {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add("a");
        hashSet.add("b");
        hashSet.add("c");
        hashSet.add("d");
        hashSet.add("e");
        System.out.println("創建一個ArrayList對象,添加兩個元素");
        ArrayList list = new ArrayList();
        list.add("第6個元素");
        list.add("第7個元素");
        System.out.println("把ArrayList對象添加到HashSet中");
        hashSet.addAll(list);
        System.out.println("添加一個null元素");
        hashSet.add(null);
        System.out.println("==================Iterator遍歷==================");
        /*
          * 通過Iterator遍歷HashSet。推薦方式
          */
        Iterator iter = hashSet.iterator();
        while(iter.hasNext()){
            String value = (String)iter.next();
            System.out.println(value);
        }
        System.out.println("==================for-each遍歷==================");
        /*
            通過for-each遍歷HashSet。不推薦!此方法需要先將Set轉換為數組
         */
        String[] arr = (String[])hashSet.toArray(new String[0]);
        for (String str:arr) {
            System.out.printf("for each : %s\n", str);
        }
    }
}

(三)關于HashSet線程安全問題討論:

public class ConcurrentTestHashSet {
    private static final HashSet hashSet = new HashSet();
    public static void main(String[] args) throws InterruptedException {
        //線程一
        Thread t1 = new Thread() {
            public void run() {
                for (int i = 0; i < 50; i++) {
                    hashSet.add( String.valueOf(i));
                }
            }
        };

        //線程二
        Thread t2 = new Thread() {
            public void run() {
                Iterator iter = hashSet.iterator();
                while(iter.hasNext()){
                    String value = (String)iter.next();
                    System.out.print(value+"  ");
                }
                System.out.println();
            }
        };
        //線程3
        Thread t3 = new Thread() {
            public void run() {
                for (int i = 50; i < 100; i++) {
                    hashSet.add( String.valueOf(i));
                }
            }
        };

        t1.start();
        t2.start();
        t3.start();

        //主線程休眠1秒鐘,以便t1、t2兩個線程將firstHashMap填裝完畢。
        Thread.currentThread().sleep(1000);


    }
}

當我們跑多幾次就容易遇到。

如果多個線程同時訪問一個哈希 set,而其中至少一個線程修改了該 set,那么就很容易發送以下異常:

這里寫圖片描述

二、HashTable源碼分析:

我們來看下hashtable的重要代碼:

其實,跟HashMap差不多,擴容機制,增刪原理等等,只不過就是在方法前加了synchronized 進行同步(注意hashtable在jdk1.8還沒更新到紅黑樹結構)。不同的只是做了一些值限定。

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
    // 保存key-value的數組。    
    // Hashtable同樣采用單鏈表解決沖突,每一個Entry本質上是一個單向鏈表 
    private transient Entry<?,?>[] table;
     // Hashtable中鍵值對的數量 
    private transient int count;
    // 閾值,用于判斷是否需要調整Hashtable的容量(threshold = 容量*加載因子)
    private int threshold;
     // 加載因子 
    private float loadFactor;
     // Hashtable被改變的次數,用于fail-fast機制的實現 
    private transient int modCount = 0;
    // 指定“容量大小”和“加載因子”的構造函數  
    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 = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }
    // 指定“容量大小”的構造函數
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }
    // 默認構造函數。    
    public Hashtable() {    
        // 默認構造函數,指定的容量大小是11;加載因子是0.75    
        this(11, 0.75f);    
    }    
   
    // 包含“子Map”的構造函數    
    public Hashtable(Map<? extends K, ? extends V> t) {    
        this(Math.max(2*t.size(), 11), 0.75f);    
        // 將“子Map”的全部元素都添加到Hashtable中    
        putAll(t);    
    }    
   
    public synchronized int size() {    
        return count;    
    }    
   
    public synchronized boolean isEmpty() {    
        return count == 0;    
    }    
    // 判斷Hashtable是否包含“值(value)”    
    public synchronized boolean contains(Object value) {    
        //注意,Hashtable中的value不能是null,    
        // 若是null的話,拋出異常!    
        if (value == null) {    
            throw new NullPointerException();    
        }    
   
        // 從后向前遍歷table數組中的元素(Entry)    
        // 對于每個Entry(單向鏈表),逐個遍歷,判斷節點的值是否等于value    
        Entry tab[] = table;    
        for (int i = tab.length ; i-- > 0 ;) {    
            for (Entry<K,V> e = tab[i] ; e != null ; e = e.next) {    
                if (e.value.equals(value)) {    
                    return true;    
                }    
            }    
        }    
        return false;    
    }    
   public boolean containsValue(Object value) {    
        return contains(value);    
    } 
    // 判斷Hashtable是否包含key    
    public synchronized boolean containsKey(Object key) {    
        Entry tab[] = table;    
        //計算hash值,直接用key的hashCode代替  
        int hash = key.hashCode();      
        // 計算在數組中的索引值   
        int index = (hash & 0x7FFFFFFF) % tab.length;    
        // 找到“key對應的Entry(鏈表)”,然后在鏈表中找出“哈希值”和“鍵值”與key都相等的元素    
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {    
            if ((e.hash == hash) && e.key.equals(key)) {    
                return true;    
            }    
        }    
        return false;    
    }    
   
    // 返回key對應的value,沒有的話返回null    
    public synchronized V get(Object key) {    
        Entry tab[] = table;    
        int hash = key.hashCode();    
        // 計算索引值,    
        int index = (hash & 0x7FFFFFFF) % tab.length;    
        // 找到“key對應的Entry(鏈表)”,然后在鏈表中找出“哈希值”和“鍵值”與key都相等的元素    
        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;    
    }    
    // 調整Hashtable的長度,將長度變成原來的2倍+1   
    protected void rehash() {    
        int oldCapacity = table.length;    
        Entry[] oldMap = table;    
   
        //創建新容量大小的Entry數組  
        int newCapacity = oldCapacity * 2 + 1;    
        Entry[] newMap = new Entry[newCapacity];    
   
        modCount++;    
        threshold = (int)(newCapacity * loadFactor);    
        table = newMap;    
          
        //將“舊的Hashtable”中的元素復制到“新的Hashtable”中  
        for (int i = oldCapacity ; i-- > 0 ;) {    
            for (Entry<K,V> old = oldMap[i] ; old != null ; ) {    
                Entry<K,V> e = old;    
                old = old.next;    
                //重新計算index  
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;    
                e.next = newMap[index];    
                newMap[index] = e;    
            }    
        }    
    }    
    // 將“key-value”添加到Hashtable中    
    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的鍵值對”,  
        // 將“修改統計數”+1    
        modCount++;    
        //  若“Hashtable實際容量” > “閾值”(閾值=總的容量 * 加載因子)    
        //  則調整Hashtable的大小    
        if (count >= threshold) {  
            rehash();    
   
            tab = table;    
            index = (hash & 0x7FFFFFFF) % tab.length;    
        }    
   
        //將新的key-value對插入到tab[index]處(即鏈表的頭結點)  
        Entry<K,V> e = tab[index];           
        tab[index] = new Entry<K,V>(hash, key, value, e);    
        count++;    
        return null;    
    }    
   
    // 刪除Hashtable中鍵為key的元素    
    public synchronized V remove(Object key) {    
        Entry tab[] = table;    
        int hash = key.hashCode();    
        int index = (hash & 0x7FFFFFFF) % tab.length;    
          
        //從table[index]鏈表中找出要刪除的節點,并刪除該節點。  
        //因為是單鏈表,因此要保留帶刪節點的前一個節點,才能有效地刪除節點  
        for (Entry<K,V> e = tab[index], 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;    
    }    
     // 清空Hashtable    
    // 將Hashtable的table數組的值全部設為null    
    public synchronized void clear() {    
        Entry tab[] = table;    
        modCount++;    
        for (int index = tab.length; --index >= 0; )    
            tab[index] = null;    
        count = 0;    
    }  
    .
    .
    .
    .
    .
    //其余都是跟HashMap的處理基本一樣的,只是加了同步鎖。同樣使用鏈地址法處理沖突。
}

再次總結重申與HashMap的的比對:

1)二者的存儲結構和解決沖突的方法都是相同的。(jdk1.7之前,jdk1.8后,hashmap加入了結構轉化--紅黑樹)

2)HashTable在不指定容量的情況下的默認容量為11,而HashMap為16,Hashtable不要求底層數組的容量一定要為2的整數次冪,而HashMap則要求一定為2的整數次冪。

3)Hashtable中key和value都不允許為null,而HashMap中key和value都允許為null(key只能有一個為null,而value則可以有多個為null)。但是如果在Hashtable中有類似put(null,null)的操作,編譯同樣可以通過,因為key和value都是Object類型,但運行時會拋出NullPointerException異常,這是JDK的規范規定的。

源碼見上面。

如果value為null,會直接拋出NullPointerException異常,但源碼中并沒有對key是否為null判斷!不過NullPointerException屬于RuntimeException異常,是可以由JVM自動拋出的,也許對key的值在JVM中有所限制吧。

4)Hashtable擴容時,將容量變為原來的2倍加1,而HashMap擴容時,將容量變為原來的2倍。

5)Hashtable計算hash值,直接用key的hashCode(),而HashMap重新計算了key的hash值,Hashtable在求hash值對應的位置索引時,用取模運算,而HashMap在求位置索引時,則用與運算,且這里一般先用hash&0x7FFFFFFF后,再對length取模,&0x7FFFFFFF的目的是為了將負的hash值轉化為正值,因為hash值有可能為負數,而&0x7FFFFFFF后,只有符號外改變,而后面的位都不變。

6)還有遍歷方式,因為與HashMap繼承不同。


三、HashSet源碼分析:

它是基于HashMap實現的。HashSet底層采用HashMap來保存元素,請先閱讀我的另一篇博客:哈希表(1)HashMap應用及源碼詳解

注意:

對于HashSet中保存的對象,請注意正確重寫其equals和hashCode方法,以保證放入的對象的唯一性。

所有放入HashSet中的集合元素實際上由HashMap的key來保存,而HashMap的value則存儲了一個PRESENT,它是一個靜態的Object對象。

將一個key-value對放入HashMap中時,首先根據key的hashCode()返回值決定該Entry的存儲位置,如果兩個key的hash值相同,那么它們的存儲位置相同。如果這個兩個key的equalus比較返回true。那么新添加的Entry的value會覆蓋原來的Entry的value,key不會覆蓋。因此,如果向HashSet中添加一個已經存在的元素,新添加的集合元素不會覆蓋原來已有的集合元素。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;
 // HashSet是通過map(HashMap對象)保存內容的
    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    // 定義一個虛擬的Object PRESENT是向map中插入key-value對應的value
    // 因為HashSet中只需要用到key,而HashMap是key-value鍵值對;
    // 所以,向map中添加鍵值對時,鍵值對的值固定是PRESENT
    private static final Object PRESENT = new Object();
    // 默認構造函數 底層創建一個HashMap
    public HashSet() {
    // 調用HashMap的默認構造函數,創建map
        map = new HashMap<>();
    }
    // 帶集合的構造函數
    public HashSet(Collection<? extends E> c) {
        /* 創建map。
         為什么要調用Math.max((int) (c.size()/.75f) + 1, 16),從 (c.size()/.75f) + 1 和 16 中選擇一個比較大的樹呢?        
         首先,說明(c.size()/.75f) + 1
           因為從HashMap的效率(時間成本和空間成本)考慮,HashMap的加載因子是0.75。
           當HashMap的“閾值”(閾值=HashMap總的大小*加載因子) < “HashMap實際大小”時,
           就需要將HashMap的容量翻倍。
           所以,(c.size()/.75f) + 1 計算出來的正好是總的空間大小。
         接下來,說明為什么是 16 。
           HashMap的總的大小,必須是2的指數倍。若創建HashMap時,指定的大小不是2的指數倍;
           HashMap的構造函數中也會重新計算,找出比“指定大小”大的最小的2的指數倍的數。
           所以,這里指定為16是從性能考慮。避免重復計算。
           */
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        // 將集合(c)中的全部元素添加到HashSet中
        addAll(c);
    }
    // 指定HashSet初始容量和加載因子的構造函數
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    // 指定HashSet初始容量的構造函數
     public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
    // 返回HashSet的迭代器
    public Iterator<E> iterator() {
    // 實際上返回的是HashMap的“key集合的迭代器”
        return map.keySet().iterator();
    }
    //調用HashMap的size()方法返回Entry的數量,得到該Set里元素的個數
    public int size() {
        return map.size();
    }
    //調用HashMap的isEmpty()來判斷HaspSet是否為空
   //HashMap為null。對應的HashSet也為空
     public boolean isEmpty() {
        return map.isEmpty();
    }
     //調用HashMap的containsKey判斷是否包含指定的key
    //HashSet的所有元素就是通過HashMap的key來保存的
    public boolean contains(Object o) {
        return map.containsKey(o);
    }
    // 將元素(e)添加到HashSet中,也就是將元素作為Key放入HashMap中
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    // 刪除HashSet中的元素(o),其實是在HashMap中刪除了以o為key的Entry
     public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }
//清空HashMap的clear方法清空所有Entry
    public void clear() {
        map.clear();
    }
    
    // 克隆一個HashSet,并返回Object對象
    public Object clone() {
        try {
            HashSet<E> newSet = (HashSet<E>) super.clone();
            newSet.map = (HashMap<E, Object>) map.clone();
            return newSet;
        } catch (CloneNotSupportedException e) {
            throw new InternalError(e);
        }
    }
    // java.io.Serializable的寫入函數
    // 將HashSet的“總的容量,加載因子,實際容量,所有的元素”都寫入到輸出流中
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // Write out any hidden serialization magic
        s.defaultWriteObject();

        // Write out HashMap capacity and load factor
        s.writeInt(map.capacity());
        s.writeFloat(map.loadFactor());

        // Write out size
        s.writeInt(map.size());

        // Write out all elements in the proper order.
        for (E e : map.keySet())
            s.writeObject(e);
    }
    
    // java.io.Serializable的讀取函數
    // 將HashSet的“總的容量,加載因子,實際容量,所有的元素”依次讀出
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in any hidden serialization magic
        s.defaultReadObject();

        // Read capacity and verify non-negative.
        int capacity = s.readInt();
        if (capacity < 0) {
            throw new InvalidObjectException("Illegal capacity: " +
                                             capacity);
        }

        // Read load factor and verify positive and non NaN.
        float loadFactor = s.readFloat();
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new InvalidObjectException("Illegal load factor: " +
                                             loadFactor);
        }

        // Read size and verify non-negative.
        int size = s.readInt();
        if (size < 0) {
            throw new InvalidObjectException("Illegal size: " +
                                             size);
        }

        // Set the capacity according to the size and load factor ensuring that
        // the HashMap is at least 25% full but clamping to maximum capacity.
        capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
                HashMap.MAXIMUM_CAPACITY);

        // Create backing HashMap
        map = (((HashSet<?>)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));

        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            @SuppressWarnings("unchecked")
                E e = (E) s.readObject();
            map.put(e, PRESENT);
        }
    }
}

再次總結重申與HashMap的的比對:

1)HashMap實現了Map接口,HashSet實現了Set接口

2)HashMap儲存鍵值對,HashSet僅僅存儲對象

3)HashMap使用put()方法將元素放入map中,HashSet使用add()方法將元素放入set中

4)HashMap中使用鍵對象來計算hashcode值,HashSet使用成員對象來計算hashcode值,對于兩個對象來說hashcode可能相同,所以equals()方法用來判斷對象的相等性,如果兩個對象不同的話,那么返回false

5)HashMap比較快,因為是使用唯一的鍵來獲取對象;HashSet較HashMap來說比較慢


好了,深入Java基礎(四)--哈希表(2)HashTable與HashSet應用及源碼詳解講完了,又是一篇源碼閱讀記錄,這是積累的必經一步,我會繼續出這個系列文章,分享經驗給大家。歡迎在下面指出錯誤,共同學習!!你的點贊是對我最好的支持??!

更多內容,可以訪問JackFrost的博客

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內容