Java集合源碼解析(五)HashSet源碼解析

前言

今天來介紹下HashSet。前面,我們已經系統的對List和Map進行了學習。接下來,我們開始可以學習Set。相信經過Map的瞭解之後,學習Set會容易很多。畢竟,Set的實現類都是基於Map來實現的(HashSet是通過HashMap實現的)。
構造圖如下:
藍色線條:繼承
綠色線條:接口實現


正文

對於HashSet而言,它是基於HashMap來實現的,底層採用HashMap來保存元素。所以如果對HashMap比較熟悉,那麼HashSet是so easy!!

HashSet簡介

HashSet定義

public class HashSet<E>
     extends AbstractSet<E>
     implements Set<E>, Cloneable, java.io.Serializable

HashSet 是一個沒有重複元素的集合
它是由HashMap實現的,不保證元素的順序,而且HashSet允許使用 null 元素
HashSet是非同步的。如果多個線程同時訪問一個哈希 set,而其中至少一個線程修改了該 set,那麼它必須 保持外部同步。這通常是通過對自然封裝該 set 的對象執行同步操作來完成的。如果不存在這樣的對象,則應該使用 Collections.synchronizedSet 方法來“包裝” set。最好在創建時完成這一操作,以防止對該 set 進行意外的不同步訪問:

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

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

HashSet屬性

// 底層使用HashMap來保存HashSet的元素
    private transient HashMap<E,Object> map;
    // Dummy value to associate with an Object in the backing Map
    // 由於Set只使用到了HashMap的key,所以此處定義一個靜態的常量Object類,來充當HashMap的value
    private static final Object PRESENT = new Object();

看到這裏就明白了,和我們前面說的一樣,HashSet是用HashMap來保存數據,而主要使用到的就是HashMap的key。

看到private static final Object PRESENT = new Object();不知道你有沒有一點疑問呢。這裏使用一個靜態的常量Object類來充當HashMap的value,既然這裏map的value是沒有意義的,爲什麼不直接使用null值來充當value呢?比如寫成這樣子private final Object PRESENT = null;我們都知道的是,Java首先將變量PRESENT分配在棧空間,而將new出來的Object分配到堆空間,這裏的new Object()是佔用堆內存的(一個空的Object對象佔用8byte),而null值我們知道,是不會在堆空間分配內存的。那麼想一想這裏爲什麼不使用null值。想到什麼嗎,看一個異常類java.lang.NullPointerException, 噢買尬,這絕對是Java程序員的一個噩夢,這是所有Java程序猿都會遇到的一個異常,你看到這個異常你以爲很好解決,但是有些時候也不是那麼容易解決,Java號稱沒有指針,但是處處碰到NullPointerException。所以啊,爲了從根源上避免NullPointerException的出現,浪費8個byte又怎麼樣,在下面的代碼中我再也不會寫這樣的代碼啦if (xxx == null) { ... } else {....},好爽。

HashSet構造函數

/**
     * 使用HashMap的默認容量大小16和默認加載因子0.75初始化map,構造一個HashSet
     */
    public HashSet() {
        map = new HashMap<E,Object>();
    }

    /**
     * 構造一個指定Collection參數的HashSet,這裏不僅僅是Set,只要實現Collection接口的容器都可以
     */
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<E,Object>(Math. max((int) (c.size()/.75f) + 1, 16));
       // 使用Collection實現的Iterator迭代器,將集合c的元素一個個加入HashSet中
       addAll(c);
    }

    /**
     * 使用指定的初始容量大小和加載因子初始化map,構造一個HashSet
     */
    public HashSet( int initialCapacity, float loadFactor) {
        map = new HashMap<E,Object>(initialCapacity, loadFactor);
    }

    /**
     * 使用指定的初始容量大小和默認的加載因子0.75初始化map,構造一個HashSet
     */
    public HashSet( int initialCapacity) {
        map = new HashMap<E,Object>(initialCapacity);
    }

    /**
     * 不對外公開的一個構造方法(默認default修飾),底層構造的是LinkedHashMap,dummy只是一個標示參數,無具體意義
     */
    HashSet( int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}

從構造方法可以很輕鬆的看出,HashSet的底層是一個HashMap,理解了HashMap後,這裏沒什麼可說的。只有最後一個構造方法有寫區別,這裏構造的是LinkedHashMap,該方法不對外公開,實際上是提供給LinkedHashSet使用的,而第三個參數dummy是無意義的,只是爲了區分其他構造方法。

API方法摘要


HashSet源碼解析(基於JDK1.6.0_45)

增加和刪除

/**
     * 利用HashMap的put方法實現add方法
     */
    public boolean add(E e) {
        return map .put(e, PRESENT)== null;
    }

    /**
     * 利用HashMap的remove方法實現remove方法
     */
    public boolean remove(Object o) {
        return map .remove(o)==PRESENT;
    }

    /**
     * 添加一個集合到HashSet中,該方法在AbstractCollection中
     */
    public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
       // 取得集合c迭代器Iterator
       Iterator<? extends E> e = c.iterator();
       // 遍歷迭代器
        while (e.hasNext()) {
           // 將集合c的每個元素加入到HashSet中
           if (add(e.next()))
              modified = true;
       }
        return modified;
    }

    /**
     * 刪除指定集合c中的所有元素,該方法在AbstractSet中
     */
    public boolean removeAll(Collection<?> c) {
        boolean modified = false;

        // 判斷當前HashSet元素個數和指定集合c的元素個數,目的是減少遍歷次數
        if (size() > c.size()) {
            // 如果當前HashSet元素多,則遍歷集合c,將集合c中的元素一個個刪除
            for (Iterator<?> i = c.iterator(); i.hasNext(); )
                modified |= remove(i.next());
        } else {
            // 如果集合c元素多,則遍歷當前HashSet,將集合c中包含的元素一個個刪除
            for (Iterator<?> i = iterator(); i.hasNext(); ) {
                if (c.contains(i.next())) {
                    i.remove();
                    modified = true;
                }
            }
        }
        return modified;
}

是否包含

/**
     * 利用HashMap的containsKey方法實現contains方法
     */
    public boolean contains(Object o) {
        return map .containsKey(o);
    }

    /**
     * 檢查是否包含指定集合中所有元素,該方法在AbstractCollection中
     */
    public boolean containsAll(Collection<?> c) {
       // 取得集合c的迭代器Iterator
       Iterator<?> e = c.iterator();
       // 遍歷迭代器,只要集合c中有一個元素不屬於當前HashSet,則返回false
        while (e.hasNext())
           if (!contains(e.next()))
               return false;
        return true;
}

由於HashMap基於hash表實現,hash表實現的容器最重要的一點就是可以快速存取,那麼HashSet對於contains方法,利用HashMap的containsKey方法,效率是非常之快的。在我看來,這個方法也是HashSet最核心的賣點方法之一。

容量檢查

/**
     * Returns the number of elements in this set (its cardinality).
     *
     * @return the number of elements in this set (its cardinality)
     */
    public int size() {
        return map .size();
    }

    /**
     * Returns <tt>true</tt> if this set contains no elements.
     *
     * @return <tt> true</tt> if this set contains no elements
     */
    public boolean isEmpty() {
        return map .isEmpty();
    }

HashSet遍歷方式

通過Iterator遍歷HashSet

第一步:根據iterator()獲取HashSet的迭代器。
第二步:遍歷迭代器獲取各個元素

// 假設set是HashSet對象
for(Iterator iterator = set.iterator();
       iterator.hasNext(); ) { 
    iterator.next();
}

通過for-each遍歷HashSet

第一步:根據toArray()獲取HashSet的元素集合對應的數組。
第二步:遍歷數組,獲取各個元素。

// 假設set是HashSet對象,並且set中元素是String類型
String[] arr = (String[])set.toArray(new String[0]);
for (String str:arr)
    System.out.printf("for each : %s\n", str);

HashSet示例

下面我們通過實例學習如何使用HashSet

import java.util.Iterator;
import java.util.HashSet;

/*
 * @desc HashSet常用API的使用。
 *
 * @author skywang
 */
public class HashSetTest {
    public static void main(String[] args) {
        // HashSet常用API
        testHashSetAPIs() ;
    }
    /*
     * HashSet除了iterator()和add()之外的其它常用API
     */
    private static void testHashSetAPIs() {
        // 新建HashSet
        HashSet set = new HashSet();
        // 將元素添加到Set中
        set.add("a");
        set.add("b");
        set.add("c");
        set.add("d");
        set.add("e");
        // 打印HashSet的實際大小
        System.out.printf("size : %d\n", set.size());
        // 判斷HashSet是否包含某個值
        System.out.printf("HashSet contains a :%s\n", set.contains("a"));
        System.out.printf("HashSet contains g :%s\n", set.contains("g"));
        // 刪除HashSet中的“e”
        set.remove("e");
        // 將Set轉換爲數組
        String[] arr = (String[])set.toArray(new String[0]);
        for (String str:arr)
            System.out.printf("for each : %s\n", str);
        // 新建一個包含b、c、f的HashSet
        HashSet otherset = new HashSet();
        otherset.add("b");
        otherset.add("c");
        otherset.add("f");
        // 克隆一個removeset,內容和set一模一樣
        HashSet removeset = (HashSet)set.clone();
        // 刪除“removeset中,屬於otherSet的元素”
        removeset.removeAll(otherset);
        // 打印removeset
        System.out.printf("removeset : %s\n", removeset);
        // 克隆一個retainset,內容和set一模一樣
        HashSet retainset = (HashSet)set.clone();
        // 保留“retainset中,屬於otherSet的元素”
        retainset.retainAll(otherset);
        // 打印retainset
        System.out.printf("retainset : %s\n", retainset);
        // 遍歷HashSet
        for(Iterator iterator = set.iterator();iterator.hasNext();) 
            System.out.printf("iterator : %s\n", iterator.next());
        // 清空HashSet
        set.clear();
        // 輸出HashSet是否爲空
        System.out.printf("%s\n", set.isEmpty()?"set is empty":"set is not empty");
    }

}

運行結果:

size : 5
HashSet contains a :true
HashSet contains g :false
for each : d
for each : b
for each : c
for each : a
removeset : [d, a]
retainset : [b, c]
iterator : d
iterator : b
iterator : c
iterator : a
set is empty

總結

HashSet和HashMap、Hashtable的區別


參考

該文爲本人學習的筆記,方便以後自己跳槽前複習。參考網上各大帖子,取其精華整合自己的理解而成。集合框架源碼面試經常會問,所以解讀源碼十分必要,希望對你有用。
java提高篇(二四)-----HashSet
Java 集合系列16之 HashSet詳細介紹(源碼解析)和使用示例
給jdk寫註釋系列之jdk1.6容器(6)-HashSet源碼解析&Map迭代器



文/嘟嘟MD(簡書作者)
原文鏈接:http://www.jianshu.com/p/c5f85e9c0098
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。
發佈了39 篇原創文章 · 獲贊 52 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章