淺談 Java集合

城北有個混子發表於2020-10-10

Java 集合

  集合是物件的容器,定義了多個物件進行操作的常用方法,可實現陣列的功能。

  Java集合類庫所處位置:java.util.*。

  與現代的資料結構類庫的常見做法一樣,Java集合類庫也將介面與實現分離開。

集合和陣列的區別:

  1.陣列長度固定,集合長度不固定。

  2.陣列可以儲存基本型別和引用型別,集合只能儲存引用型別。


Java 集合框架中的介面體系

Java集合框架中的重要介面

  Java集合框架中,集合有兩個基本介面:Collection 和 Map。

Collection 介面

  Collection 介面實現了Iterable 介面,我們來看看Iterable 介面的定義:

public interface Iterable<T> {
    
    // 返回一個T元素型別的迭代器
    Iterator<T> iterator();

    // 其他介面方法...
    }
}

  在Java 1.8幫助文件中對Iterable 介面的說明是:實現此介面允許物件成為“for-each loop”語句的目標。

  Collection 介面擴充套件了Iterable 介面。因此,對於標準類庫中的任何實現Collection 介面的集合都可以使用“for each”迴圈來遍歷元素。

  Iterable 介面中只定義了一個抽象方法iterator(),該方法用於返回一個實現了Iterator 介面的(迭代器)物件,可以使用這個迭代器物件依次訪問集合中的元素。


Iterator 介面

我們來看看這個Iterator(迭代器)介面:

public interface Iterator<E> {
    
    // 如果迭代具有更多元素,則返回 true 。
    boolean hasNext();

    // 返回迭代中的下一個元素。
    E next();

    // 從底層集合中刪除此迭代器返回的最後一個元素(可選操作)。 
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    // 對每個剩餘元素執行給定的操作,直到所有元素都被處理或動作引發異常。
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

  通過反覆呼叫next方法,可以逐個訪問集合中的每個元素。但是,如果到達了集合的末尾,next方法將丟擲一個NoSuchElementException。因此,需要在呼叫next之前呼叫hasNext方法。如果迭代器物件還有多個供訪問的元素,這個方法就返回true。如果想要檢視集合中的所有元素,就請求一個迭代器,並在hasNext返回true時反覆地呼叫next方法。

我們來看一看案例:

public class TestList {

    public static void main(String[] args) {
        // 1.建立物件
        List list = new ArrayList();
                
        // 2.遍歷元素
        
        // 方法一:迭代器
        Iterator iterator = list.iterator();
        while(iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        // 方法二:for-each
        for (Object object : list) {
            System.out.println(object);
        }
    }
}

  我們建立了一個擴充套件了Collection 介面的介面List 的實現類ArrayList的物件,並沒有新增元素,因為我們此刻的重點是看迭代器的具體使用。上面提到過,Collection 介面 擴充套件了 Iterable 介面,所以此集合也可以使用“for-each”迴圈來遍歷。

  元素被訪問的順序取決於集合型別。如果對ArrayList進行迭代,迭代器將從索引0開始,每迭代一次,索引值加1。然而,如果訪問HashSet中的元素,每個元素將會按照某種隨機的次序出現。雖然可以確定在迭代過程中能夠遍歷到集合中的所有元素,但卻無法預知元素被訪問的次序。


 Java集合類庫中的迭代器與其他類庫中的迭代器的區別

  在傳統的集合類庫中,例如,C++的標準模版庫,迭代器是根據陣列索引建模的。如果給定這樣一個迭代器,就可以檢視指定位置上的元素,就像知道陣列索引i就可以檢視陣列元素a[i]一樣。不需要查詢元素,就可以將迭代器向前移動一個位置。這與不需要執行查詢操作就可以通過i++將陣列索引向前移動一樣。

  但是,Java迭代器並不是這樣操作的。查詢操作與位置變更是緊密相連的。查詢一個元素的唯一方法是呼叫next,而在執行查詢操作的同時,迭代器的位置隨之向前移動。因此,應該將Java迭代器認為是位於兩個元素之間。當呼叫next時,迭代器就越過下一個元素,並返回剛剛越過的那個元素的引用。


 Collection 介面中的方法

方法

描述

boolean add(E e)

確保此集合包含指定的元素。

boolean addAll(Collection<? extends E> c)

將指定集合中的所有元素新增到此集合。

void clear()

從此集合中刪除所有元素。

boolean contains(Object o)

如果此集合包含指定的元素,則返回 true 。

boolean containsAll(Collection<?> c)

如果此集合包含指定 集合中的所有元素,則返回true。

boolean equals(Object o)

將指定的物件與此集合進行比較以獲得相等性。

int hashCode()

返回此集合的雜湊碼值。

boolean isEmpty()

如果此集合不包含元素,則返回 true 。

Iterator<E> iterator()

返回此集合中的元素的迭代器。

boolean remove(Object o)

從該集合中刪除指定元素的單個例項(如果存在)。

int size()

返回此集合中的元素數。

Collection 介面常用方法

  這兒只列出來了一些常用的方法,這些方法被Collection 下屬的三個集合介面List、Set、Queue所繼承,它們又繼承給自己打下屬介面。注意,此時並沒有實現這些方法,因為Java集合類庫是將介面和實現分離的,後續我們會講到方法的實現。


Map 介面

  Map集合用於儲存具有對映關係的資料,儲存鍵(key)值(value)對,一對一對往裡存,保證鍵(key)的唯一性。


Map 介面中的方法

方法

描述

boolean containsKey(Object key)

如果此對映包含指定鍵的對映,則返回 true 。

void clear()

從該地圖中刪除所有的對映。

boolean containsValue(Object value)

如果此地圖將一個或多個鍵對映到指定的值,則返回 true 。

Set<Map.Entry<K,V>> entrySet()

返回此地圖中包含的對映的Set檢視。

boolean equals(Object o)

將指定的物件與此對映進行比較以獲得相等性。

V get(Object key)

返回到指定鍵所對映的值,或 null如果此對映包含該鍵的對映。

int hashCode()

返回此地圖的雜湊碼值。

boolean isEmpty()

如果此地圖不包含鍵值對映,則返回 true 。

Set<K> keySet()

返回此地圖中包含的鍵的Set檢視。

V put(K key, V value)

將指定的值與該對映中的指定鍵相關聯。

V remove(Object key)

如果存在,從該地圖中刪除一個鍵的對映。

int size()

size()返回此地圖中鍵值對映的數量。

Collection<V> values()

返回此地圖中包含的值的Collection檢視。

Map介面常用方法


ListIterator 介面

  ListIterator是一個功能更加強大的迭代器, 它繼承於Iterator介面,只能用於各種List型別的訪問。可以通過呼叫listIterator()方法產生一個指向List開始處的ListIterator, 除此以外還可以呼叫listIterator(n)方法建立一個一開始就指向列表索引為n的元素處的ListIterator。

ListIterator的特點:

  (1) 雙向移動(向前/向後遍歷)。

  (2) 可以產生相對於迭代器在列表中指向的當前位置的前一個和後一個元素的索引。

  (3) 可以使用set()方法替換它訪問過的最後一個元素。

  (4) 可以使用add()方法在next()方法返回的元素之前或previous()方法返回的元素之後插入一個元素。

Iterator和ListIterator區別

  集合List和Set都有iterator()方法來取得其迭代器。對List來說,你也可以通過listIterator()取得其迭代器,兩種迭代器在有些時候是不能通用的,Iterator和ListIterator主要區別在以下方面:

  ① ListIterator有add()方法,可以向List中新增物件,而Iterator不能。

  ② ListIterator和Iterator都有hasNext()和next()方法,可以實現順序向後遍歷,但是ListIterator有hasPrevious()和previous()方法,可以實現逆向(順序向前)遍歷;Iterator就不可以。

  ③ ListIterator可以定位當前的索引位置,用nextIndex()和previousIndex()來實現,但是Iterator沒有此功能。

  ④ 都可實現刪除物件,但是ListIterator可以實現物件的修改,使用set()方法實現,但是Iierator僅能遍歷,不能修改。


RandomAccess 介面

  實際中有兩種有序集合,其效能開銷有很大差異。由陣列支援的有序集合可以快速地隨機訪問,因此適合使用List方法並提供一個整數索引來訪問。與之不同,連結串列儘管也是有序的,但是隨機訪問很慢,所以最好使用迭代器來遍歷。

  為了避免對連結串列完成隨機訪問操作,Java SE 1.4引入了一個標記介面RandomAccess。這個介面不包含任何方法,不過可以用它來測試一個特定的集合是否支援高效的隨機訪問。

我們來看一看如何應用它:

if (o instanceof RandomAccess) {
    System.out.println("o支援隨機訪問");
}else {
    System.out.println("o不至此隨機訪問");
}

Java集合框架中的實現類體系

  與現代的資料結構類庫的常見做法一樣,Java集合類庫也將介面與實現分離開。我們來看看有哪些實現類:

Java 集合框架中的實現類

  我們在上節看到的集合的幾種介面在上圖中都有其對應的抽象類,這些抽象類中實現了其子類共有的方法,繼承了介面中的其他方法。抽象類下面有各自具體的實現子類,在其中定義了各自特有的方法和實現。

我們來梳理一下集合具體實現類:

★ 集合中有兩大基本的介面:Collection 和 Map。

★ Collection 介面下有兩個重要集合:List 和 Set。

☆ List 集合的實現類有ArrayList、Vector 和 LinkedList。

☆ Set 集合的常用實現類有HashSet、TreeSet 和 LinkedHashSet。

★ Map 介面下只有Map集合,Map集合重要的實現類有:HashMap、TreeMap 和 LinkedHashMap。


List 集合

  List 是用於存放多個元素的容器,它允許有重複的元素,並保證元素之間的先後順序。List 有3個主要的實現類:ArrayList、Vector 和 LinkedList。


ArrayList

   ArrayList 類又稱為動態陣列,該容器類實現了列表的相關操作。ArrayList的內部資料結構由陣列實現,因此可對容器內元素實現快速隨機訪問。但因為在ArrayList 中插入或刪除一個元素需要移動其他元素,所以不適合在插入和刪除操作頻繁的場景下使用 ArrayList。與此同時,ArrayList的容量可以隨著元素的增加而自動增加,所以不用擔心ArrayList容量不足的問題。另外 ArrayList是非執行緒安全的。

ArrayList的常用方法總結如下:

新增元素

  ♦ boolean add(E e):將指定的元素e新增到此列表的尾部。

  ♦ void add(int index,E element):將指定的元素 element 插入此列表中的指定位置index.

  ♦ boolean addAll(Collection<? extends E> c):將該Collection 中的所有元素新增到此列表的尾部。

  ♦ boolean addAll(int index,Collection<?extends E> c):從指定的位置index開始,將指定Collection 中的所有元素插入此列表中。

刪除元素

  ♦ E remove(int index):移除此列表中指定位置index上的元素。

  ♦ boolean remove(Object o):移除此列表中首次出現的指定元素。(如果存在的話)。

  ♦ void clear():移除此列表中的所有元素。

查詢元素

  ♦ boolean contains(Object o):如果此列表中包含指定的元素o,則返回true.

  ♦ E get(int index):返回此列表中指定位置index上的元素。

  ♦ int indexOf(Object o):返回此列表中首次出現的指定元素o的索引,或如果此列表不包含元素o,則返回-1.

  ♦ boolean isEmpty():如果此列表中沒有元素,則返回true,否則返回 false.

  ♦ int lastIndexOf(Object o):返回此列表中最後一次出現指定元素o的索引,如果此列表不包含則返回-1.

其他方法

  ♦ E set(int index,E element):用指定的元素element 替代此列表中指定位置index上的元素。注意它與 void add (int index,E element)方法的區別:add方法是新增一個元素,原來index 位置上的元素要向後移動;而set方法是將原來index 位置上的元素替換為element.

  ♦ int size():返回此列表中的元素數。

  ♦ Object[] toArray():按適當順序(從第一個元素到最後一個元素)返回包含此列表中所有元素構成的陣列。


重點:ArrayList 的擴容機制

問題描述:

ArrayList list= new ArrayList(20);

  在建立 list 時擴充容量幾次?

問題分析:  

  ArrayList 類又稱為動態陣列,內部資料結構由陣列實現,陣列的容量可以自動增長,當陣列容量不足以存放新增元素時,需要進行陣列的擴容,擴容的基本策略如下:

  每次向陣列中新增元素時,要檢查新增元素後的容量是否超過當前陣列的長度,如果沒有超過,則新增該元素,不做其他操作;如果超過,則陣列就會進行自動擴容,每次擴充原容量的1.5倍。另外 ArrayList 提供了3個建構函式,在初始擴容時有略微不同,因為篇幅問題,在這不做過多闡述,具體請看這篇文章:淺談ArrayList的擴容機制

問題結果:

  擴容0次。


Vector

   Vector類又稱為向量類,也實現了類似動態陣列的功能,內部資料結構也由陣列實現。與ArrayList 不同的是,Vector是執行緒安全的,它的方法都是同步方法,所以訪問效率低於ArrayList。另外 Vector 是非泛型集合,可以往其中隨意插入不同類的物件,不需要考慮型別和預先選定向量的容量,可方便地進行查詢等操作。當然也可以使用Vector 的泛型取代非泛型型別(例如 Vectort<String>)。

Vector 的常用方法總結如下:

新增元素

  ♦ public synchronized boolean add(E e):在最後位置新增元素e.

  ♦ public void add(int index, E element):在具體的索引位置index 上新增元素 element,因為該函式內部呼叫了同步方法synchronized void insertElementAt(),所以該方法依然是同步的。

  ♦ public synchronized boolean addAll(Collection<?extends E>c):將一個容器c的所有元素新增到向量的尾部。

刪除元素

  ♦ public boolean remove(Object o):刪除元素o,方法內部呼叫了另一個同步方法 public synchronized boolean removeElement(Object obj),所以該方法依然是同步的。

  ♦ public synchronized void removeElementAt(int index):刪除指定位置的元素。

查詢元素

  ♦ public synchronized E elementAt(int index):查詢指定位置的元素。

其他方法

  ♦ public synchronized E get(int index):獲取指定位置index的元素。

  ♦ public synchronized E set(int index,E element):用指定的元素element 替代 Vector 中指定位置index上的元素。

  對比Vector 和 ArrayList 的主要方法,可以發現:Vector的方法與ArrayList 的方法的主要差異是增加了 synchronized 關鍵字,這樣保證了執行的執行緒安全,但也勢必會影響Vector的效能。

  所以在不要求執行緒安全的場景中,推薦使用效能更好的 ArrayList。


LinkedList

  LinkedList 也是List 的一個重要實現類。它的內部資料結構由連結串列結構實現,並且是非執行緒安全的,適合資料的動態插入和刪除,插入和刪除元素時不需要對資料進行移動、所以插入、刪除效率較高,但隨機訪問速度較慢。


Set 集合

  HashSet 和 TreeSet 是Set介面的兩個最重要的實現類,在Set容器類中得到廣泛使用。其中 TreeSet是 Set的子介面 SortedSet的實現類。


HashSet

  HashSet 是java.util包中的類,實現了Set介面,封裝了HashMap,元素是通過HashMap來儲存的。

關於 HashSet 有以下幾點需要說明:

  ① HashSet 中的元素可以是null,但只能有一個null(因為實現了Set介面,所以不允許有重複的值)。

  ② HashSet 是非執行緒安全的。

  ③ 插入HashSet 中的物件不保證與插入的順序一致,元素的排列順序可能改變。

  ④ 向HashSet 中新增新的物件時,HashSet類會進行重複物件元素判斷:判斷新增物件和容器內已有物件是否重複,如果重複則不新增,如果不重複則新增。


 HashSet 是怎麼區分重複元素的?

  在上面已經說過,HashSet實現了Set介面,所以不能儲存重複元素。在向 HashSet 中新增元素時會呼叫 HashSet的boolean add(E e)方法,在該方法中,HashSet 會首先判斷所要新增的元素是否與容器內已存在的元素有重複,如果沒有重複則添該元索並返回true,否則不新增元素並返回 false。

  那麼如何判斷所要新增的元素是否與容器內已存在的元素有重複呢?在 HashSet 內部,HashSet 封裝了 HashMap,在呼叫add()方法時,實際上是呼叫了HashMap 的 put()方法新增元素,原始碼如下:

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

  我們來看上面的程式碼,其中新增的元素e就是HashMap的key(put方法的第一個引數)。HashMap 的 put()方法首先會呼叫元素e的hashCode()得到其在 HashMap 中的索引,如果在該索引位置上已存在其他元素(即兩個元素的hashCode()返回值相等),則再呼叫e的equals()方法判斷該索引位置上的元素是否與要新增的元素e相等。只有上述兩個條件都滿足,才能確認該HashMap 中已經包含元素e.

  總結一下,要準確判斷HashSet 中兩個物件重複與否,需要hashCode()和 equals()這兩個方法共同來確定,即如果 hashCode()返回值相等並且 equals()返回true,則判定兩個物件重複,只要任一條件不滿足,則判定兩個物件不重複。

  如果大家還對此過程有疑惑,我們可以進一步瞭解HashSet 中的 HashMap.put()方法,看看它是如何實現新增元素功能的,原始碼如下:

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

  進一步來看putVal 方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; 
    Node<K,V> p; 
    int n, i;
    //  如果儲存元素的table為空,則進行必要欄位的初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        // 獲取長度(預設初始容量:16)
        n = (tab = resize()).length;
    // 如果根據hash值獲取的結點為空,則新建一個結點
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 如果該節點不為空,則進行進一步判斷
    else {
        Node<K,V> e; 
        K k;
        // 1.找到與插入節點相對映的節點,如果沒有則建立
        // 這裡的p結點是根據hash值算出來對應在陣列中的元素
        // 如果新插入的結點和table中p結點的hash值,key值相同的話,將節點p賦值給節點e
        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 {
            for (int binCount = 0; ; ++binCount) {
                // 如果遍歷完了單連結串列,直接新增節點到連結串列末尾,並退出迴圈
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 連結串列長度大於8時,將連結串列轉紅黑樹
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                        break;
                }
                // 如果需要插入的結點和table中p結點的hash值,key值相同的話,直接退出迴圈
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // 遍歷下一個節點
                p = e;
            }
        }
        // 2.如果存在這個對映就覆蓋
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // 判斷是否允許覆蓋,並且value是否為空
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 回撥以允許LinkedHashMap後置操作
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 更改操作次數
    ++modCount;
    // 如果長度大於臨界值
    if (++size > threshold)
        // 將陣列大小設定為原來的2倍,並將原先的陣列中的元素放到新陣列中
        // 因為有連結串列,紅黑樹之類,因此還要調整他們
        resize();
    // 回撥以允許LinkedHashMap後置操作
    afterNodeInsertion(evict);
    return null;
}

  通過程式碼的描述能夠看出,這裡向HashSet中新增的元素實際上就是HashMap 中的key,而不是value。程式碼中首先獲取物件key的hash值,然後以此hash值得到key 在HashMap 中的索引。因為HashMap 本質上是一個陣列連結串列紅黑樹的資料結構,所以在同一個索引i下可能存在多個元素,因此這裡通過一個迴圈遍歷該索引i下的每一個元素。如果發現要新增到HashMap 中的元素key與該索引上的某個元素重複(hash值相等並且equals 返回值為true),則用新的value覆蓋掉舊的value,並返回oldValue,這樣呼叫它的HashSet.add()方法則返回false,表示新增元素不成功;如果發現要新增到HashMap 中的元素key與該索引上的任何元素都不重複,則把(key,value)鍵值對插入該索引下,然後返回null,這樣呼叫它的HashSet.add()方法則返回true,表示新增元素成功。


型別轉換問題

我們來看一段程式碼:

public static void main(String[] args) {
    Set<Short> set = new HashSet<Short>();
    for (short i = 0; i < 10; i++) {
        set.add(i);
        set.remove(i - 1);
    }
    System.out.println(set.size());
}

  大家可以看一下上面的程式碼,然後算一下輸出的結果應該是多少?下面我們來一一分析:

HashSet 的常用方法總結如下:

  ① add(Ee):返回boolean型值,其功能是在HashSet 中新增指定元素e.

  說明:新增元素e時HashSet類先進行重複元素判斷:如果要新增的元素和已有元素重複,則新增不成功,返回false;否則新增元素e,返回true.

  ② remove(Object o):返回boolean 型值,其功能是從HashSet 中移除指定元素o.

  說明:如果元素o不包含在HashSet中,則未刪除任何元素,返回false;否則,刪除元素o,返回true.

下面我們來分析程式碼的執行情況:

  先建立了一個 Short型別的 HashSet 物件。然後呼叫add()方法向HashSet中插入元素。因為這裡插入的是short型別的值,而add()方法的引數是引用型別Short,所以 short型別的引數會自動裝箱成其包裝類Short型別。呼叫remove(i-1),i為short型,1為int型別,因為short、byte 型在進行數字運算時,會自動轉換為int,所以i-1返回一個int型別值。

  執行 set.remove(i-1)時,i-1會自動裝箱為Integer物件。雖然一個Short型別物件和一個Integer型別物件的值可能相等,但是它們的型別不同,所以比較時認為物件不相等,因此這裡的remove操作其實都不成功,因為set中不存在元素i-1,所以沒有刪除任何物件,同時編譯器不報錯。

  綜上所述,該段程式碼的實質是,set中加入了10個元素,每一個都成功新增沒有被刪除,所以輸出大小結果為10。

特別提示:  

  對於泛型集合HashSet<E>的add和remove方法,add()方法傳遞的引數型別一定要和Set中定義的型別一致,否則會報編譯錯誤;但是remove()方法的引數型別是Objeet,所以可以接受任何型別,不會報編譯錯誤。


TreeSet

  TreeSet 是java.util 包中的類,也實現了Set介面,因此TreeSet中同樣不能有重複元素。TreeSet 封裝了TreeMap,所以是一個有序的容器,容器內的元素是排好序的。

關於TreeSet,有以下幾點需要說明:

  ① TreeSet 中的元素是一個有序的集合(在插入元素時會進行排序),不允許放入null值。

  ② TreeSet 是非執行緒安全的。

  ③ 向TreeSet 中新增新的物件時,TreeSet 會將新增物件和已有物件進行比較,存在重複物件則不進行新增,不存在重複物件的情況下,新插入物件和已有物件根據比較結果排序再進行儲存。


TreeSet 是如何保證元素有序的?

   TreeSet 底層資料結構是一種自平衡的二叉搜尋樹,即紅黑樹。紅黑樹保證了元素的有序性,TreeSet 是按照紅黑樹的結點進行儲存和取出資料的。

注意:

  HashSet中元素的無序和LinkedHashSet中元素按照插入的順序有序是指向容器中插入元素的順序是否與遍歷順序一致。例如,向一個HashSet中順序插入元素a、b、c,而遍歷 HashSet時訪問元素的順序就不一定是a、b、c了,這叫作不保證遍歷順序和插入順序一致。而TreeSet 中元素的有序是指元素按照CompareTo(Object obj)方法來比較元素之間大小關係後的順序進行的排序,它與按照插入的順序有序不是一個概念。


LinkedHashSet

  LinkedHashSet 類是HashSet 的子類,它的元素也是唯一的,LinkedHashSet 是基於 HashMap和雙向連結串列的實現類。LinkedHashSet 與HashSet 相比,它底層是用雙向連結串列實現,用連結串列記錄資料,實現了按照插入的順序有序,也就是說,遍歷順序和插人順序是一致的。LinkedHashSet在迭代訪問Set 中全部元素時效能會比HashSet好,但插人元素時效能不如 HashSet.


Map 集合

   HashMap 和 Hashtable 是Map介面的主要實現類。


HashMap

  HashMap 又稱為雜湊表,它是根據鍵key的 hashCode值來儲存資料的它儲存的是鍵值對(key-value)對映,具有快速定位的特點。HashMap 繼承於 AbstractMap,實現了Map等介面。它的實現是不同步的,因此不是執行緒安全的。它的key、value 都可以為null,而key最多隻能出現一個 null。同時 HashMap 中的對映不是有序的(儲存序不等於插入序)。

HashMap 的主要方法總結:

新增元素

  ♦ public V put(K key,V value):向 HashMap 中插入結點<key,value>,若key已經存在,則覆蓋相同key的結點。

  ♦ public void putAll(Map<?extends K,?extends V>m):將指定的map m中的<key,value>插入HashMap 中。

刪除元素

  ♦ public V remove(Object key):移除key指定的鍵值對<key,value>。

查詢元素

  ♦ public V get(Object key):返回鍵key對應的值 value。

  ♦ final Entry<K,V> getEntry(Object key):根據鍵key查詢鍵值對。

  ♦ public boolean containsKey(Object key):判斷是否包含鍵key指定的鍵值對。

  ♦ public boolean contains Value(Object value):判斷是否包含 value 對應的鍵值對。

其他方法

  ♦ public int size():返回雜湊表中鍵值對個數。

  ♦ public Set<Map.Entry<K,V>>entrySet():返回一個鍵值對集合。


HashMap 的資料結構:

  HashMap 的資料結構是由陣列連結串列紅黑樹來實現的。HashMap 底層是一個陣列Entry[] table,陣列中的每個元素Entry都是一個單項鍊表的引用,從JDK1.8開始,當連結串列長度大於8時,連結串列會調整為紅黑樹結構。

從JDK1.8開始,HashMap 為什麼要引入紅黑樹結構?

  HashMap 採用陣列和連結串列相結合的資料結構,底層是一個陣列,每個陣列元素都是一個連結串列結構,連結串列的每個結點就是HashMap 中的每個元素(鍵值對)。當要向 HashMap 中新增一個鍵值對時,會先呼叫該鍵值對的key的 hashCode()方法計算出 hashCode值,從而得到該元素在陣列中的下標。如果陣列在該位置上已儲存有元素(已存在一個連結串列),則說明發生了衝突(不同的key值對應了同一個hash值,所以對映的陣列下標也相同),接下來就要按照HashMap 衝突管理演算法進行處理。

  HashMap 採用鏈地址法,即用單連結串列將所有衝突的元素連結起來,通過這種方法來進行衝突管理。但是這個連結串列並不會無限地增長,當連結串列中元素個數大於8時,這個連結串列會自動轉為紅黑樹結構。之所以引入紅黑樹結構是因為在連結串列中查詢每個元素的時間複雜度都是O(n),而在紅黑樹中查詢元素的時間複雜度為O(logn),這樣當HashMap 中元素量較多併產生了大量Hash 衝突時,紅黑樹的快速增刪改查的特點能提高 HashMap 的效能。

  紅黑樹(Red Black Tree)是一種自平衡二叉查詢樹,它是在1972年由 Rudolf Bayer 發明的。紅黑樹用紅色和黑色來標記結點,並且有以下三個特點:

  ① 根和葉子結點都是黑色的。

  ② 從每個葉子到根的所有路徑上不能有兩個連續的紅色結點。

  ③ 從任一結點到它所能到達的葉子結點的所有簡單路徑都包含相同數目的黑色結點。

  以上三個特性保證了紅黑樹比其他的二叉查詢樹有更好的結點查詢穩定性、查詢效率和增刪結點時的效率。鑑於以上原因,引入了紅黑樹來解決HashMap 的雜湊衝突效率等問題。

紅黑樹結構這麼好,為什麼在元素個數小於8時還要用連結串列,而不直接使用紅黑樹?

  當元素數目較少時,連結串列的效率更高,而紅黑樹的實現和調整都更復雜,反而會影響整體效能。


HashMap 物件的兩個重要屬性和擴容:

  HashMap 物件的兩個重要屬性:初始容量和載入因子。初始容量是指 HashMap 在建立時的容量,載入因子是 HashMap 在其容量自動增加之前可以達到多滿的一種尺度。HashMap的初始容量預設值為16,預設載入因子是0.75,當 HashMap 中的元素數目超出載入因子與當前容量的乘積時,則要對該 HashMap 進行擴容操作,擴容後陣列大小為當前的2倍。


HashMap 的衝突管理:

  HashMap 採用“hash 演算法”來決定每個元素的儲存位置,當新增新的元素時,系統會呼叫 hashCode()方法得到一個 hashCode值,再根據這個hashCode值決定這個元素在 HashMap 中的儲存位置。當不同的物件的hashCode值相同時,就出現了衝突,HashMap 採用鏈地址法,即用單連結串列將所有衝突的元素連結起來,通過這種方法來進行衝突管理。當連結串列的元素個數大於8時,會自動轉為紅黑樹結構,這樣會提升查詢效能,把順序搜尋連結串列記錄的時間複雜度從O(n)提高到O(logn)。


HashTable

   Hashtable 類與 HashMap 類似,不同的是Hashtable是執行緒安全的,而且屬於遺留類。需要注意的是,如果對同步性和遺留程式碼的相容性沒有特殊要求,建議使用HashMap類,這是因為 Hashtable 雖然有執行緒安全的優點,但是效率較低。Hashtable 的方法類似於HashMap。在實際應用中如果需要執行緒安全的場景,推薦使用 ConcurrentHashMap 代替 Hashtable。


ConcurrentHashMap

  ConcurrentHashMap 和 HashMap 的定義類似,但它是執行緒安全的,和Hashtable相比更加細化,而且效能優勢更大。學習時需要掌握ConcurrentHashMap 的加鎖原理。


 HashMap 和 Hashtable 的主要區別:

  ① HashMap 和Hashtable 是Map 介面的兩個重要實現類,HashMap是Hashtable的輕量級實現。

  ② HashMap 是非執行緒安全的,Hashtable是執行緒安全的。

  ③ HashMap的鍵(key)和值(value)都支援null,Hashtable 鍵(key)和值(value)都不支援null。

  ④ 關於 fail-fast(快速失敗)機制,HashMap 使用 Iterator 進行遍歷,Iterator 迭代器支援fail-fast(快速失敗)機制;而 Hashtable 用Iterator 遍歷時支援 fail-fast,但是用 Enumerationr 遍歷時不支援 fail-fast。

注意:

  快速失效機制(fail-fast):快速失效機制fail-fast是Java容器中的一種錯誤檢測機制。當使用迭代器迭代容器類的過程中,如果同時該容器在結構上發生改變,就有可能觸發fail-fast,丟擲 ConcurrentModificationException 異常。這種機制一般僅用於檢測bug。

  例如:當使用Iterator迭代容器類 ArrayList 時,在迴圈送代過程中每次呼叫 ArrayList的remove()方法刪除元素,讓容器結構發生了改變,就可能引起快速失效機制,從而丟擲 ConcurrentModificationException 異常。

 

相關文章