1. 程式人生 > >Java集合之HashSet、LinkedHashSet、TreeSet

Java集合之HashSet、LinkedHashSet、TreeSet

討論集合關注的問題:

  1. 底層資料結構
  2. 增刪改查方式
  3. 初始容量,擴容方式,擴容時機
  4. 執行緒安全與否
  5. 是否允許空,是否允許重複,是否有序

1. 概述

前篇,我寫了關於Map系列的集合(點選跳轉);本篇重新回顧Collection三大類Set、List、Queue中的Set。
timg

Set可以視作是數學中集合的概念,也即集合中不能有重複的元素。Set集合中的各種實現集合,其內部都與Map有關,先對Map有了解更好。常見的Set集合有HashSet、LinkedHashSet和TreeSet,下面通過原始碼試著分析其內部構造。

 

2. HashSet

HashSet繼承自AbstractSet,實現了Set介面,同時也是可克隆物件和進行序列化的。其內部的資料儲存區通過一個transient修飾的HashMap維護,也就是說HashSet中的資料是存放在HashMap中(回憶:HashMap中是通過一個transient的陣列來儲存不同的Hash值的key,相同的Key鏈成一個連結串列)。進行序列化時,不會序列化空的值。它維持它自己的內部排序,所以隨機訪問沒有任何意義。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

    /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }

HashSet進行構造時,除了可以使用Collection進行構造外,基本都呼叫了HashMap的建構函式完成。主要的引數是基礎容量為16個單位,載入因子是0.75。

HashSet基於HashMap,所以其對資料的訪問基本都是用HashMap的方法,包括獲取size、載入迭代器等。我們知道存入Set中的資料本身是無序的,維護訪問順序沒有意義。

public int size() {
        return map.size();
    }
public boolean isEmpty() {
        return map.isEmpty();
    }
public boolean contains(Object o) {
        return map.containsKey(o);
    }    
 /**
     * Returns an iterator over the elements in this set.  The elements
     * are returned in no particular order.
     *
     * @return an Iterator over the elements in this set
     * @see ConcurrentModificationException
     */
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }    

新增元素時,HashSet會呼叫HashMap的add方法,但在操作時做了一點細微的處理。HashSet類中維護了一個final的空物件Object,每次加入集合中的資料,其實是儲存在Map中的key中,而map中的Value都是這個物件。所以相同元素插入時,此時會發生value的替換,因為所有entry的value一樣,所以和沒有插入時一樣的。HashSet在新增元素或移除時,同樣會run一遍HashMap中那些操作(查詢、成鏈等),只是內容變化不大。

方式:hash(key的hash碼處理得到)———(相同與否)———key(相同與否)————替換value或鏈成表

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
* @param e element to be added to this set
 * @return <tt>true</tt> if this set did not already contain the specified
 * element
 */
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

 

3. LinkedHashSet

LinkedHashSet是HashSet的一個“擴充套件版本”,HashSet並不管什麼順序,不同的是LinkedHashSet會維護“插入順序”。HashSet內部使用HashMap物件來儲存它的元素,而LinkedHashSet內部使用LinkedHashMap物件來儲存和處理它的元素。

LinkedHashSet直接繼承自HashSet,能夠維護基礎的有序性。

LinkedHashSet使用LinkedHashMap物件來儲存它的元素,插入到LinkedHashSet中的元素實際上是被當作LinkedHashMap的鍵儲存起來的。LinkedHashMap的每一個鍵值對都是通過內部的靜態類Entry。

其內部只有幾個簡單的建構函式,使用了父類的一個比較特殊的建構函式:

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
-----------------------------------------------------------------------
public LinkedHashSet(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor, true);
}

public LinkedHashSet(int initialCapacity) {
    super(initialCapacity, .75f, true);
}

public LinkedHashSet() {
    super(16, .75f, true);
}

 

4. TreeSet

TreeSet與TreeMap實現類似。

TreeMap是一個有序的二叉樹,TreeSet同樣也是一個有序的,它的作用是提供有序的Set集合。TreeSet繼承自AbstractSet,實現了Set介面、Cloneable和Serializable、NavigableSet介面。其內部主要是通過一個NavigableMap的map維護資料儲存。

private transient NavigableMap<E,Object> m;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
 * Constructs a set backed by the specified navigable map.
 */
TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
}
public TreeSet() {
    this(new TreeMap<E,Object>());
}
//構造一個包含指定 collection 元素的新 TreeSet,它按照其元素的自然順序進行排序。
public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
}

//構造一個新的空 TreeSet,它根據指定比較器進行排序。
public TreeSet(Collection<? extends E> c) {
    this();
    addAll(c);
}

//構造一個與指定有序 set 具有相同對映關係和相同排序的新 TreeSet。
public TreeSet(SortedSet<E> s) {
    this(s.comparator());
    addAll(s);
}

NavigableSet是 SortedSet的子類,具有了為給定搜尋目標報告最接近匹配項的導航方法,這就意味著它支援一系列的導航方法——比如查詢與指定目標最匹配項。

TreeSet內部通過維護了一顆樹,來保證資料的有序性。

public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
    //空樹時,判斷節點是否為空
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    //非空樹,根據傳入比較器進行節點的插入位置查詢
    if (cpr != null) {
        do {
            parent = t;
            //節點比根節點小,則找左子樹,否則找右子樹
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
                //如果key的比較返回值相等,直接更新值(一般compareto相等時equals方法也相等)
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {
    //如果沒有傳入比較器,則按照自然排序
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    //查詢的節點為空,直接插入,預設為紅節點
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
        //插入後進行紅黑樹調整
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}   

 

5. 小結

HashSet/TreeSet和LinkedHashSet,其內部都是基於Map來實現的。TreeSet和LinkedHashSet分別使用了TreeMap和LinkedHashMap來控制訪問資料的有序性。

三者都屬於Set的範疇,都是沒有重複元素的集合。基於HashMap和TreeMap,所以都是非執行緒安全的。

參考文章:https://blog.csdn.net/a724888/article/details/80295328