Java集合類學習總結

Java集合類學習總結

1. Collection詳解

2. List詳解

3. Set詳解

4. Map詳解

一 Collection

1.Collection接口繼承樹

2.List接口和Set接口簡單對比

在這裏插入圖片描述

3.Collection的常用方法

在這裏插入圖片描述

二 List

1.ArrayList、LinkedList、Vector三者的異同

相同:
三個類都實現了List接口,存儲數據的特點相同:存儲有序的、可重複的數據
不同:
在這裏插入圖片描述
在這裏插入圖片描述

2.ArrayList底層實現

JDK 7 情況下:
ArrayList list = new ArrayList(); //底層創建了長度是10的Object[] 數組elementData
list.add(1); //elementData[0] = new Integer(1);

list.add(11); //如果此次的添加導致底層elementData數組容量不夠,則擴容,默認爲原理容量的1.5倍,同時需要將原有數組中的數據複製到新的數組中
建議:在開發中使用ArrayList list = new ArrayList(int capacity);
JDK 8 情況下:
ArrayList list = new ArrayList(); //底層Object[] elementData初始化爲{},並沒有創建數組
list.add(12); //第一次調用add()時,底層才創建了長度10的數組,並將12添加到elementData[0] 中
後續添加擴容與JKD7無異

3.LinkedList底層實現

LikedList list = new LinkedList(); 內部聲明瞭first和last屬性,默認值爲null
list.add(1); 將1封裝到Node中,創建了Node對象
其中Node節點源代碼爲:
在這裏插入圖片描述
add()源碼:
在這裏插入圖片描述

4.Vector底層實現

JDK7和JDK8中通過Vector()構造器創建對象時,底層都創建了長度爲10的數組
在擴容方面,默認擴容爲原理的數組的長度的2倍

5.HashSet、LinkedHashSet、TreeSet異同

在這裏插入圖片描述

6.HashSet添加元素過程

我們向HashSet中添加元素a,首先調用元素a所在類的hashCode()方法,計算元素a的哈希值,此哈希值接着通過某種算法計算出在HashSet底層數組中的存放位置(即爲:索引位置),判斷數組此位置上是否已經有元素:
-----------如果此位置上沒有其他元素,則元素a添加成功
-----------如果此位置上有其他元素b(或以鏈表形式存在的多個元素),則標膠元素a與元素b的hash值:
----------------------如果hash值不相同,則元素a添加成功 ------> 情況2
----------------------如果hash值相同,進而需要調用元素a所在類的equlas()方法:
---------------------------------equals()返回true,元素a添加失敗
---------------------------------equals()返回false,則元素a添加成功 ---->情況3
對於添加成功的情況2和情況3而言:元素a與已經存在指定索引位置上數據以鏈表方式存儲。
jdk7:元素a放到數組中,指向原來的元素
jdk8:原來的元素在數組中,指向元素a
總結:七上八下
HashSet底層:數組+鏈表的結構
要求:向Set添加的數據,其所在類一定要重寫hashCode()和equals()方法

7.LinkedHashSet底層實現

在這裏插入圖片描述作爲HashSet的子類,在添加數據的同時,每個數據維護了兩個引用,記錄前一個數據和後一個數據
優點:對於頻繁的遍歷,LinkedHashSet比HashSet效率高

8.TreeSet

TreeSet添加的數據要求爲同一個類的對象,在TreeSet中比較兩個對象是否相同的標準是比較各自重寫的compareTo()或compare()方法返回的值,若爲0,則兩個對象相同,不再是比較equals()方法了
底層採用紅黑樹的存儲結構
①TreeSet自然排序
在要添加的類中實現一下Comparable接口,重寫compareTo方法
在這裏插入圖片描述
若姓名有重複還可以指定二級排序
在這裏插入圖片描述
②TreeSet定製排序
TreeSet treeSet = new TreeSet(com);
在這裏插入圖片描述

三Map

在這裏插入圖片描述Map接口和Collection接口是並列的
| Map:雙列數據,存儲K-V對的數據
|-------- HashMap:作爲Map的主要實現類;線程是不安全的,效率高,可存儲null的key和value
|---------------- LinkedHashMap:保證在遍歷map元素時,可以按照添加的順序實現遍歷。原因:在原有的HashMap底層結構基礎上,添加---------------- 了一對指針,指向前一個和後一個元素,對於頻繁的遍歷操作,執行效率要高於HashMap
|-------- TreeMap:保證按照添加的K-V對進行排序,實現排序遍歷,此時考慮key的自然排序或定製排序,底層使用紅黑樹
|-------- Hashtable:作爲古老的實現類,線程安全的,效率低,不能存儲null的key和value
|---------------- Properties:常用來處理配置文件。key和value都是String類型
Map結構理解:
Map中的key:無序的、不可重複的,使用Set存儲所有的key ----->key所在的類要重寫equals()和hashCode()【以HashMap爲例】
Map中的value:無序的、可重複的,使用Collection存儲所有的value ---->value所在的類藥重寫equals()
一個鍵值對構成了一個Entry對象
Map中的entry:無序的、不可重複的,使用Set存儲所有的entry
在這裏插入圖片描述

1.HashMap詳解

底層實現原理【以jdk7爲例】
HashMap map = new HashMap();
在實例化以後,底層創建了長度16的一維數組Entry[] table.
…可能已經執行過多次put…
map.put(key1,value1);
首先,調用key1所在類的hashCode()計算key1哈希值,此哈希值經過某種算法計算以後,得到在Entry數組中的存放位置
如果此位置上的數據爲空,此時的entry(key1-value1)添加成功 -----情況1
如果此位置上的數據不爲空(意味着此位置上存在一個或多個數據【以鏈表形式存在】),比較key1和已經存在的一個或多個數據的哈希值:
------ 如果key1的哈希值與已經存在的數據的哈希值都不相同,此時key1-value1添加成功 -----情況2
------ 如果key1的哈希值和已經存在的某一個數據(key2-value2)的哈希值相同,繼續比較,調用key1所在類的equals(key2)方法比較:
------------ 如果equals()返回false:則添加成功 -----情況3
------------ 如果equals()返回true:使用value1替換value2

補充:對於情況2和情況3:此時key1-value1和原來的數據以鏈表的方式存儲。
在不斷的添加的過程中,會涉及到擴容問題,默認的擴容方式:擴容爲原容量的2倍,並將原有數據複製過來

jdk8 相較於jdk7在底層實現方面的不同:

  1. new HashMap():底層沒有創建一個長度16的數組
  2. jdk8 底層的數組時Node[] , 而非Entry[]
  3. 首次調用put方法時,底層創建長度爲16的數組
  4. jdk7 底層結構只有:數組+鏈表; jdk8 中底層結構:數組+鏈表+紅黑樹
  5. 當數組的某一個索引位置上的元素以鏈表以鏈表形式存在的數據個數>8 且當前數組長度 >64 時,此時此索引上的所有數據改爲使用紅黑樹存儲。
    更多細節請參考下方鏈接
2.ConcurrentHashMap詳解

JDK7下的ConcurrentHashMap
在JDK1.7版本中,ConcurrentHashMap的數據結構是由一個Segment數組和多個HashEntry組成,主要實現原理是實現了鎖分離的思路解決了多線程的安全問題,如下圖所示:
在這裏插入圖片描述
Segment數組的意義就是將一個大的table分割成多個小的table來進行加鎖,也就是上面的提到的鎖分離技術,而每一個Segment元素存儲的是HashEntry數組+鏈表,這個和HashMap的數據存儲結構一樣。
ConcurrentHashMap 與HashMap和Hashtable 最大的不同在於:put和 get 兩次Hash到達指定的HashEntry,第一次hash到達Segment,第二次到達Segment裏面的Entry,然後在遍歷entry鏈表
** 初始化 **
ConcurrentHashMap的初始化是會通過位與運算來初始化Segment的大小,用ssize來表示,源碼如下所示

private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // For serialization compatibility
        // Emulate segment calculation from previous version of this class
        int sshift = 0;
        int ssize = 1;
        while (ssize < DEFAULT_CONCURRENCY_LEVEL) {
            ++sshift;
            ssize <<= 1;
        }
        int segmentShift = 32 - sshift;
        int segmentMask = ssize - 1;

因爲ssize用位於運算來計算(ssize <<=1),所以Segment的大小取值都是以2的N次方,無關concurrencyLevel的取值,當然concurrencyLevel最大隻能用16位的二進制來表示,即65536,換句話說,Segment的大小最多65536個,沒有指定concurrencyLevel元素初始化,Segment的大小ssize默認爲 DEFAULT_CONCURRENCY_LEVEL =16
每一個Segment元素下的HashEntry的初始化也是按照位與運算來計算,用cap來表示,如下:

int cap = 1;
while (cap < c)
    cap <<= 1

上所示,HashEntry大小的計算也是2的N次方(cap <<=1), cap的初始值爲1,所以HashEntry最小的容量爲2
put操作

 static class Segment<K,V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        final float loadFactor;
        Segment(float lf) { this.loadFactor = lf; }
    }

從上Segment的繼承體系可以看出,Segment實現了ReentrantLock,也就帶有鎖的功能,當執行put操作時,會進行第一次key的hash來定位Segment的位置,如果該Segment還沒有初始化,即通過CAS操作進行賦值,然後進行第二次hash操作,找到相應的HashEntry的位置,這裏會利用繼承過來的鎖的特性,在將數據插入指定的HashEntry位置時(鏈表的尾端),會通過繼承ReentrantLock的tryLock()方法嘗試去獲取鎖,如果獲取成功就直接插入相應的位置,如果已經有線程獲取該Segment的鎖,那當前線程會以自旋的方式(如果不瞭解自旋鎖,請參考:自旋鎖原理及java自旋鎖)去繼續的調用tryLock()方法去獲取鎖,超過指定次數就掛起,等待喚醒.
get
ConcurrentHashMap的get操作跟HashMap類似,只是ConcurrentHashMap第一次需要經過一次hash定位到Segment的位置,然後再hash定位到指定的HashEntry,遍歷該HashEntry下的鏈表進行對比,成功就返回,不成功就返回null
size 返回ConcurrentHashMap元素大小
計算ConcurrentHashMap的元素大小是一個有趣的問題,因爲他是併發操作的,就是在你計算size的時候,他還在併發的插入數據,可能會導致你計算出來的size和你實際的size有相差(在你return size的時候,插入了多個數據),要解決這個問題,JDK1.7版本用兩種方案

try {
    for (;;) {
        if (retries++ == RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j) ensureSegment(j).lock(); // force creation
        }
        sum = 0L;
        size = 0;
        overflow = false;
        for (int j = 0; j < segments.length; ++j) {
            Segment<K,V> seg = segmentAt(segments, j);
            if (seg != null) { sum += seg.modCount; int c = seg.count; if (c < 0 || (size += c) < 0)
               overflow = true;
            } }
        if (sum == last) break;
        last = sum; } }
finally {
    if (retries > RETRIES_BEFORE_LOCK) {
        for (int j = 0; j < segments.length; ++j)
            segmentAt(segments, j).unlock();
    }
}

1、第一種方案他會使用不加鎖的模式去嘗試多次計算ConcurrentHashMap的size,最多三次,比較前後兩次計算的結果,結果一致就認爲當前沒有元素加入,計算的結果是準確的.
2、第二種方案是如果第一種方案不符合,他就會給每個Segment加上鎖,然後計算ConcurrentHashMap的size返回.

JDK8的ConcurrentHashMap詳解:
JDK1.8的實現已經摒棄了Segment的概念,而是直接用Node數組+鏈表+紅黑樹的數據結構來實現,併發控制使用Synchronized和CAS來操作,整個看起來就像是優化過且線程安全的HashMap,雖然在JDK1.8中還能看到Segment的數據結構,但是已經簡化了屬性,只是爲了兼容舊版本.
在這裏插入圖片描述Node是ConcurrentHashMap存儲結構的基本單元,繼承於HashMap中的Entry,用於存儲數據,Node數據結構很簡單,就是一個鏈表,但是隻允許對數據進行查找,不允許進行修改
源代碼如下:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;

        Node(int hash, K key, V val, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }

        public final K getKey()       { return key; }
        public final V getValue()     { return val; }
        public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
        public final String toString(){ return key + "=" + val; }
        public final V setValue(V value) {
            throw new UnsupportedOperationException();
        }

        public final boolean equals(Object o) {
            Object k, v, u; Map.Entry<?,?> e;
            return ((o instanceof Map.Entry) &&
                    (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                    (v = e.getValue()) != null &&
                    (k == key || k.equals(key)) &&
                    (v == (u = val) || v.equals(u)));
        }

        /**
         * Virtualized support for map.get(); overridden in subclasses.
         */
        Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }
    }

TreeNode繼承於Node,但是數據結構換成了二叉樹結構,它是紅黑樹的數據的存儲結構,用於紅黑樹中存儲數據,當鏈表的節點數大於8時會轉換成紅黑樹的結構,他就是通過TreeNode作爲存儲結構代替Node來轉換成黑紅樹源代碼如下

static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;

        TreeNode(int hash, K key, V val, Node<K,V> next,
                 TreeNode<K,V> parent) {
            super(hash, key, val, next);
            this.parent = parent;
        }

        Node<K,V> find(int h, Object k) {
            return findTreeNode(h, k, null);
        }

        /**
         * Returns the TreeNode (or null if not found) for the given key
         * starting at given root.
         */
        final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
            if (k != null) {
                TreeNode<K,V> p = this;
                do  {
                    int ph, dir; K pk; TreeNode<K,V> q;
                    TreeNode<K,V> pl = p.left, pr = p.right;
                    if ((ph = p.hash) > h)
                        p = pl;
                    else if (ph < h)
                        p = pr;
                    else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                        return p;
                    else if (pl == null)
                        p = pr;
                    else if (pr == null)
                        p = pl;
                    else if ((kc != null ||
                              (kc = comparableClassFor(k)) != null) &&
                             (dir = compareComparables(kc, k, pk)) != 0)
                        p = (dir < 0) ? pl : pr;
                    else if ((q = pr.findTreeNode(h, k, kc)) != null)
                        return q;
                    else
                        p = pl;
                } while (p != null);
            }
            return null;
        }
    }

TreeBin從字面含義中可以理解爲存儲樹形結構的容器,而樹形結構就是指TreeNode,所以TreeBin就是封裝TreeNode的容器,它提供轉換黑紅樹的一些條件和鎖的控制.
ConcurrentHashMap只是增加了同步的操作來控制併發,從JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+紅黑樹

3.LinkedHashMap詳解

在這裏插入圖片描述

//LinkedHashMap#newNode,構造一個新的節點
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}
//LinkedHashMap#linkNodeLast,插入到鏈表尾部
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;        //LinkedHashMap定義了tail尾指針和head頭指針,且鏈表爲雙向鏈表
    tail = p;
    if (last == null)
        head = p;
    else {
//雙向鏈表的插入
        p.before = last;
        last.after = p;
    }
}

對於LinkedHashMap插入,散列表部分和HashMap一致,而雙向鏈表部分則是來一個就插到尾部,這樣就保證了保持插入順序。
  通過插入基本瞭解了LinkedHashMap的內部實現,get方法很簡單,同樣是計算出key的hash和對應散列表的下標即可。
  在LinkedHashMap還需要提到三個方法,這三個方法在HashMap中定義,但是並沒有具體實現,具體實現放到了LinkedHashMap中。

void afterNodeAccess(Node<K,V> p)
  此方法可以實現通過訪問順序排序,方法中如果定義accessOrder=true,則會將訪問(get)過的元素放到鏈表尾部。accessOrder設置可以通過構造方法傳遞。

void afterNodeInsertion(boolean evict)
  這個方法在LinkedHashMap並無意義,因爲它調用的removeEldestEntry始終返回false,此時程序就會返回不會執行。但如果重寫了removeEldestEntry方法,則可以實現LRU(最近最少使用)緩存。

void afterNodeRemoval(Node<K,V> p)
  移除Map中的元素時調用,更新雙向鏈表。

4.TreeMap詳解

在這裏插入圖片描述

5.Hashtable詳解

在這裏插入圖片描述

6.IdentityHashMap詳解

1.對於要保存的key,k1和k2,當且僅當k1== k2的時候,IdentityHashMap纔會相等,而對於HashMap來說,相等的條件則是:對比兩個key的hashCode等
IdentityHashMap不是Map的通用實現,它有意違反了Map的常規協定。並且IdentityHashMap允許key和value都爲null。
同HashMap,IdentityHashMap也是無序的,並且該類不是線程安全的,如果要使之線程安全,可以調用Collections.synchronizedMap(new IdentityHashMap(…))方法來實現。
2.IdentityHashMap重寫了equals和hashcode方法,不過需要注意的是hashCode方法並不是藉助Object的hashCode來實現的,而是通過System.identityHashCode方法來實現的。
hashCode的生成是與key和value都有關係的,這就間接保證了key和value這對數據具備了唯一的hash值。同時通過重寫equals方法,判定只有key值全等情況下才會判斷key值相等。這就是IdentityHashMap與普通HashMap不同的關鍵所在。

參考:
WeakHashMap詳解
Java集合超詳細講解
Java中IdentityHashMap使用詳解—允許key重複
經常用 HashMap?這 6 個問題回答下

特別聲明:此文章轉載了別人的優秀文章中的部分內容用來學習,侵權請聯繫刪除

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章