天天看點

Java集合(七)HashSet、TreeSet與LinkedHashSet1 HashSet2 TreeSet3 LinkedHashSet

本文目錄

1 HashSet

1.1 HashSet的定義

1.2 HashSet的方法

2 TreeSet

2.1 TreeSet的定義

2.2 TreeSet的方法

3 LinkedHashSet

3.1 LinkedHashSet的定義

3.2 LinkedHashSet是如何維護插入順序的

1 HashSet

1.1 HashSet的定義

(1)JDK中對HashSet的定義如下:

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

HashSet繼承AbstractSet類,實作Set、Cloneable、Serializable接口。其中AbstractSet提供 Set 接口的骨幹實作,進而最大限度地減少了實作此接口所需的工作。Set接口是一種不包括重複元素的Collection,它維持它自己的内部排序,是以随機通路沒有任何意義。

(2)HashSet的基本屬性:

基于HashMap實作,底層使用HashMap儲存所有元素:

private transient HashMap<E,Object> map;

//定義一個Object對象作為HashMap的value
private static final Object PRESENT = new Object();
           

 (3)構造函數:

/**
     * 預設構造函數
     * 初始化一個空的HashMap,并使用預設初始容量為16和加載因子0.75。
     */
    public HashSet() {
        map = new HashMap<>();
    }

    /**
     * 構造一個包含指定 collection 中的元素的新 set。
     */
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }

    /**
     * 構造一個新的空 set,其底層 HashMap 執行個體具有指定的初始容量和指定的加載因子
     */
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }

    /**
     * 構造一個新的空 set,其底層 HashMap 執行個體具有指定的初始容量和預設的加載因子(0.75)。
     */
    public HashSet(int initialCapacity) {
       map = new HashMap<>(initialCapacity);
    }

    /**
     * 在API中我沒有看到這個構造函數,今天看源碼才發現(原來通路權限為包權限,不對外公開的)
     * 以指定的initialCapacity和loadFactor構造一個新的空連結哈希集合。
     * dummy 為辨別 該構造函數主要作用是對LinkedHashSet起到一個支援作用
     */
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
       map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
           

  從構造函數中可以看出HashSet所有的構造都是構造出一個新的HashMap,其中最後一個構造函數,為包通路權限是不對外公開,僅僅隻在使用LinkedHashSet時才會發生作用。

1.2 HashSet的方法

既然HashSet是基于HashMap,那麼對于HashSet而言,其方法的實作過程是相對比較簡單的。

(1)Iterator

public Iterator<E> iterator() {
        return map.keySet().iterator();
    }
           

iterator()方法傳回對此 set 中元素進行疊代的疊代器。傳回元素的順序并不是特定的。底層調用HashMap的keySet傳回所有的key,這點反應了HashSet中的所有元素都是儲存在HashMap的key中,value則是使用的PRESENT對象,該對象為static final。

(2)size()

public int size() { return map.size(); }
           

size()傳回此 set 中的元素的數量(set 的容量)。底層調用HashMap的size方法,傳回HashMap容器的大小。

(3)isEmpty(),contains()

public boolean isEmpty() {
        return map.isEmpty();
    }
    

public boolean contains(Object o) {
        return map.containsKey(o);
}

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

//最終調用該方法進行節點查找
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //先檢查桶的頭結點是否存在
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
            //不是頭結點,則周遊連結清單,如果是樹節點則使用樹節點的方法周遊,直到找到,或者為null
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
           

isEmpty(),判斷HashSet()集合是否為空,為空傳回 true,否則傳回false。contains(),判斷某個元素是否存在于HashSet()中,存在傳回true,否則傳回false。更加确切的講應該是要滿足這種關系才能傳回true:(o==null ? e==null : o.equals(e))。底層調用containsKey判斷HashMap的key值是否為空。

(4)add()、put()方法

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

map的put方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    //确認初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
        
    //如果桶為空,直接插入新元素,也就是entry
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //如果沖突,分為三種情況
        //key相等時讓舊entry等于新entry即可
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //紅黑樹情況
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //如果key不相等,則連成連結清單
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
           

這裡注意一點,hashset隻是不允許重複的元素加入,而不是不允許元素連成連結清單,因為隻要key的equals方法判斷為true時它們是相等的,此時會發生value的替換,因為所有entry的value一樣,是以和沒有插入時一樣的。而當兩個hashcode相同但key不相等的entry插入時,仍然會連成一個連結清單,長度超過8時依然會和HashMap一樣擴充成紅黑樹。

當add方法發生沖突時,如果key相同,則替換value,如果key不同,則連成連結清單。

add()如果此 set 中尚未包含指定元素,則添加指定元素。如果此Set沒有包含滿足(e==null ? e2==null : e.equals(e2)) 的e2時,則将e2添加到Set中,否則不添加且傳回false。

由于底層使用HashMap的put方法将key = e,value=PRESENT建構成key-value鍵值對,當此e存在于HashMap的key中,則value将會覆寫原有value,但是key保持不變,是以如果将一個已經存在的e元素添加中HashSet中,新添加的元素是不會儲存到HashMap中,是以這就滿足了HashSet中元素不會重複的特性。

(5)remove()

public boolean remove(Object o) {    return map.remove(o)==PRESENT;}
           

remove如果指定元素存在于此 set 中,則将其移除。底層使用HashMap的remove方法删除指定的Entry。

(6)clear()

public void clear() {
    map.clear();
}
           

clear從此 set 中移除所有元素。底層調用HashMap的clear方法清除所有的Entry。

(7)clone()方法

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

clone傳回此 HashSet 執行個體的淺表副本:并沒有複制這些元素本身。

2 TreeSet

2.1 TreeSet的定義

源碼定義如下:

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
           

我們知道TreeMap是一個有序的二叉樹,那麼同理TreeSet同樣也是一個有序的,它的作用是提供有序的Set集合。通過源碼我們知道TreeSet基礎AbstractSet,實作NavigableSet、Cloneable、Serializable接口。

其中AbstractSet提供 Set 接口的骨幹實作,進而最大限度地減少了實作此接口所需的工作。

NavigableSet是擴充的 SortedSet,具有了為給定搜尋目标報告最接近比對項的導航方法,這就意味着它支援一系列的導航方法。比如查找與指定目标最比對項。Cloneable支援克隆,Serializable支援序列化。

同時在TreeSet中定義了如下幾個變量:

private transient NavigableMap<E,Object> m;
    
//PRESENT會被當做Map的value與key建構成鍵值對
 private static final Object PRESENT = new Object();
           

其構造方法如下:

//預設構造方法,根據其元素的自然順序進行排序
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);
}

TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
}
           

2.2 TreeSet的方法

(1)add:将指定的元素添加到此 set(如果該元素尚未存在于 set 中)。

public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
 
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;
}    
           

(2)get:擷取元素

public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}
           

該方法與put的流程類似,隻不過是把插入換成了查找。

(3)ceiling:傳回此 set 中大于等于給定元素的最小元素;如果不存在這樣的元素,則傳回 null。

public E ceiling(E e) {
           

(4)clear:移除此 set 中的所有元素。

public void clear() {
        m.clear();
    }
           

(5)clone:傳回 TreeSet 執行個體的淺表副本。屬于淺拷貝。

public Object clone() {
        TreeSet<E> clone = null;
        try {
            clone = (TreeSet<E>) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }

        clone.m = new TreeMap<>(m);
        return clone;
    }
           

(6)comparator:傳回對此 set 中的元素進行排序的比較器;如果此 set 使用其元素的自然順序,則傳回 null。

public Comparator<? super E> comparator() {
        return m.comparator();
    }
           

(7)contains:如果此 set 包含指定的元素,則傳回 true。

public boolean contains(Object o) {
        return m.containsKey(o);
    }
           

(8)descendingIterator:傳回在此 set 元素上按降序進行疊代的疊代器。

public Iterator<E> descendingIterator() {
        return m.descendingKeySet().iterator();
    }
           

(9)descendingSet:傳回此 set 中所包含元素的逆序視圖。

public NavigableSet<E> descendingSet() {
        return new TreeSet<>(m.descendingMap());
    }
           

(10)first:傳回此 set 中目前第一個(最低)元素。

public E first() {
        return m.firstKey();
    }
           

(11)floor:傳回此 set 中小于等于給定元素的最大元素;如果不存在這樣的元素,則傳回 null。

public E floor(E e) {
        return m.floorKey(e);
    }
           

(12)headSet:傳回此 set 的部分視圖,其元素嚴格小于 toElement。

public SortedSet<E> headSet(E toElement) {
        return headSet(toElement, false);
    }
           

(13)higher:傳回此 set 中嚴格大于給定元素的最小元素;如果不存在這樣的元素,則傳回 null。

public E higher(E e) {
        return m.higherKey(e);
    }
           

(14)isEmpty:如果此 set 不包含任何元素,則傳回 true。

public boolean isEmpty() {
        return m.isEmpty();
    }
           

(15)iterator:傳回在此 set 中的元素上按升序進行疊代的疊代器。

public Iterator<E> iterator() {
        return m.navigableKeySet().iterator();
    }
           

(16)last:傳回此 set 中目前最後一個(最高)元素。

public E last() {
        return m.lastKey();
    }
           

(17)lower:傳回此 set 中嚴格小于給定元素的最大元素;如果不存在這樣的元素,則傳回 null。

public E lower(E e) {
        return m.lowerKey(e);
    }
           

(18)pollFirst:擷取并移除第一個(最低)元素;如果此 set 為空,則傳回 null。

public E pollFirst() {
        Map.Entry<E,?> e = m.pollFirstEntry();
        return (e == null) ? null : e.getKey();
    }
           

(19)pollLast:擷取并移除最後一個(最高)元素;如果此 set 為空,則傳回 null。

public E pollLast() {
        Map.Entry<E,?> e = m.pollLastEntry();
        return (e == null) ? null : e.getKey();
    }
           

(20)remove:将指定的元素從 set 中移除(如果該元素存在于此 set 中)

public boolean remove(Object o) {
        return m.remove(o)==PRESENT;
    }
           

該方法與put類似,隻不過把插入換成了删除,并且要進行删除後調整 。

(21)size:傳回 set 中的元素數(set 的容量)。

public int size() {
        return m.size();
    }
           

(22)subSet:傳回此 set 的部分視圖

/**
     * 傳回此 set 的部分視圖,其元素範圍從 fromElement 到 toElement。
     */
     public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
             E toElement,   boolean toInclusive) {
             return new TreeSet<>(m.subMap(fromElement, fromInclusive,
                  toElement,   toInclusive));
     }
     
     /**
      * 傳回此 set 的部分視圖,其元素從 fromElement(包括)到 toElement(不包括)。
      */
     public SortedSet<E> subSet(E fromElement, E toElement) {
         return subSet(fromElement, true, toElement, false);
     }
           

(23)tailSet:傳回此 set 的部分視圖

/**
     * 傳回此 set 的部分視圖,其元素大于(或等于,如果 inclusive 為 true)fromElement。
     */
    public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
        return new TreeSet<>(m.tailMap(fromElement, inclusive));
    }
    
    /**
     * 傳回此 set 的部分視圖,其元素大于等于 fromElement。
     */
    public SortedSet<E> tailSet(E fromElement) {
        return tailSet(fromElement, true);
    }
           

3 LinkedHashSet

3.1 LinkedHashSet的定義

LinkedHashSet是HashSet的一個“擴充版本”,HashSet并不管什麼順序,不同的是LinkedHashSet會維護“插入順序”。HashSet内部使用HashMap對象來存儲它的元素,而LinkedHashSet内部使用LinkedHashMap對象來存儲和處理它的元素。這篇文章,我們将會看到LinkedHashSet内部是如何運作的及如何維護插入順序的。

我們首先着眼LinkedHashSet的構造函數。在LinkedHashSet類中一共有4個構造函數。這些構造函數都隻是簡單地調用父類構造函數(如HashSet類的構造函數)。 下面看看LinkedHashSet的構造函數是如何定義的。

//Constructor --- 1
 
public LinkedHashSet(int initialCapacity, float loadFactor)
{
      super(initialCapacity, loadFactor, true);              //Calling super class constructor
}
 
//Constructor --- 2
 
public LinkedHashSet(int initialCapacity)
{
        super(initialCapacity, .75f, true);             //Calling super class constructor
}
 
//Constructor --- 3
 
public LinkedHashSet()
{
        super(16, .75f, true);                //Calling super class constructor
}
 
//Constructor --- 4
 
public LinkedHashSet(Collection<? extends E> c)
{
        super(Math.max(2*c.size(), 11), .75f, true);          //Calling super class constructor
        addAll(c);
}
           

在上面的代碼片段中,你可能注意到4個構造函數調用的是同一個父類的構造函數。這個構造函數(父類的,譯者注)是一個包内私有構造函數(見下面的代碼,HashSet的構造函數沒有使用public公開,譯者注),它隻能被LinkedHashSet使用。

這個構造函數需要初始容量,負載因子和一個boolean類型的啞值(沒有什麼用處的參數,作為标記,譯者注)等參數。這個啞參數隻是用來差別這個構造函數與HashSet的其他擁有初始容量和負載因子參數的構造函數,下面是這個構造函數的定義:

HashSet(int initialCapacity, float loadFactor, boolean dummy)
{
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
           

顯然,這個構造函數内部初始化了一個LinkedHashMap對象,這個對象恰好被LinkedHashSet用來存儲它的元素。

LinkedHashSet并沒有自己的方法,所有的方法都繼承自它的父類HashSet,是以,對LinkedHashSet的所有操作方式就好像對HashSet操作一樣。唯一的不同是内部使用不同的對象去存儲元素。在HashSet中,插入的元素是被當做HashMap的鍵來儲存的,而在LinkedHashSet中被看作是LinkedHashMap的鍵。

這些鍵對應的值都是常量PRESENT(PRESENT是HashSet的靜态成員變量,譯者注)。

3.2 LinkedHashSet是如何維護插入順序的

LinkedHashSet使用LinkedHashMap對象來存儲它的元素,插入到LinkedHashSet中的元素實際上是被當作LinkedHashMap的鍵儲存起來的。LinkedHashMap的每一個鍵值對都是通過内部的靜态類Entry<K, V>執行個體化的。這個 Entry<K, V>類繼承了HashMap.Entry類。這個靜态類增加了兩個成員變量,before和after來維護LinkedHasMap元素的插入順序。這兩個成員變量分别指向前一個和後一個元素,這讓LinkedHashMap也有類似雙向連結清單的表現。

private static class Entry<K,V> extends HashMap.Entry<K,V>
{
        // These fields comprise the doubly linked list used for iteration.
        Entry<K,V> before, after;
 
        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }
}
           

從上面代碼看到的LinkedHashMap内部類的前面兩個成員變量——before和after負責維護LinkedHashSet的插入順序。LinkedHashMap定義的成員變量header儲存的是 這個雙向連結清單的頭節點。

接下來看一個例子就知道LinkedHashSet内部是如何工作的了:

public class LinkedHashSetExample
{
    public static void main(String[] args)
    {
        //Creating LinkedHashSet
 
        LinkedHashSet<String> set = new LinkedHashSet<String>();
 
        //Adding elements to LinkedHashSet
 
        set.add("BLUE");
 
        set.add("RED");
 
        set.add("GREEN");    
 
        set.add("BLACK");
    }
}
           

如果你知道LinkedHashMap内部是如何工作的,就非常容易明白LinkedHashSet内部是如何工作的了。 https://blog.csdn.net/qq_41969790/article/details/106154225

附言:

本文整理來源于網絡、部落格等資源,僅做個人學習筆記複習所用。

如果對你學習有用,請點贊共同學習!

如有侵權,請聯系我修改或删除!

繼續閱讀