Collection淺析

一:Collection
在這裏插入圖片描述


二:List,有序, 可重複,有下標索引
1.ArrayList

底層動態數組,單線程,線程不安全,初始容量10,1.5倍擴容,查詢效率高,增/刪/改效率低

public class ArrayList<E>
{

    transient Object[] elementData; 

    private int size;
}

  • 擴容:
private void grow(int minCapacity) {
       // overflow-conscious code
       int oldCapacity = elementData.length;
       //1.5倍擴容
       int newCapacity = oldCapacity + (oldCapacity >> 1);
       if (newCapacity - minCapacity < 0)
           newCapacity = minCapacity;
       if (newCapacity - MAX_ARRAY_SIZE > 0)
           newCapacity = hugeCapacity(minCapacity);
       // minCapacity is usually close to size, so this is a win:
       elementData = Arrays.copyOf(elementData, newCapacity);
   }
   private static int hugeCapacity(int minCapacity) {
       if (minCapacity < 0) // overflow
           throw new OutOfMemoryError();
       return (minCapacity > MAX_ARRAY_SIZE) ?
           Integer.MAX_VALUE :
           MAX_ARRAY_SIZE;
   }

2.LinkedList

雙向鏈表實現,單線程,線程不安全,無限擴容,增刪快,查詢慢,

public class LinkedList<E>
{
    transient int size = 0;

    /**
     * 頭節點
     */
    transient Node<E> first;

    /**
     * 尾節點
     */
    transient Node<E> last;

    private static class Node<E> {
    	//當前節點
        E item;
        //下一個節點
        Node<E> next;
        //前一個節點
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
}

3.Vector

底層動態數組,多線程(Synchronized同步方法),線程安全,初始容量10,2倍擴容,查詢效率比LinkedList高,比ArrayList低(線程安全,消耗資源)

4.ArrayList比LinkedList查詢效率高的原因:

ArrayList在內存中是連續、成塊的,只需要根據首地址+偏移量即可快速計算出查找節點的內存地址,LinkedList在內存中是不連續的,每一個節點都需要2個指針分別指向上一個節點和下一個節點,只能通過遍歷的方式獲取元素(源碼採用二分查找算法,以提高效率)

  • ArrayList查找源碼:
@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];
}
  • LinkedList查找源碼:
/**
 * Returns the (non-null) Node at the specified element index.
 */
Node<E> node(int index) {
    // assert isElementIndex(index);
	//二分查找,先定位index的位置,再循環查找
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

5.ArrayList比LinkedList修改效率低的原因:

ArrayList在插入/刪除元素時,該元素插入位置後的數據的內存地址要依次向後移動,而LinkedList是雙向鏈表最多隻會影響到插入/刪除元素前後2個位置節點的信息

  • LinkedList插入/刪除源碼:
/**
 * 節點尾部插入元素
 */
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
}

/**
 * 刪除元素
 */
E unlink(Node<E> x) {

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    return element;
}

  • ArrayList插入/刪除源碼:
// ensureCapacityInternal() 方法內部會調用 System.arraycopy()
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  
    elementData[size++] = e;
    return true;
}

public void add(int index, E element) {
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

public E remove(int index) {
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; 

    return oldValue;
}

6.LinkedList比ArrayList佔用更多的內存:

ArrayList每個節點存儲的是實際數據內容,而LinkedList每個節點存儲的是實際數據及前後節點的內存地址

7.擴容的目的:

減少鏈表的長度,元素分配更均勻

8.ArrayList是線程不安全的,請編寫一個不安全的案例,並給出解決方案?

  • 故障現象
  • 導致原因
  • 解決方案
  • 優化建議

三:Set,無序, 不重複,無下標索引
1.HashSet

底層是HashMap,數組+鏈表結構,初始容量16,加載因子0.75,擴容1倍,存儲元素時先計算該元素的HashCode值,根據HashCode值和集合的長度計算出該元素在集合中的下標,如果該下標處沒有元素就直接存儲,如果有通過equals方法比較是否是同一個對象,如果是不存儲,如果不是通過鏈接的方式在該下標處存儲元素

2.LinkedHashSet

HashSet的子類,比HashSet多了一個存儲順序的維護

3.TreeSet

  • 底層是TreeMap,根據元素的compareTo()方法或者是根據指定的比較器對象的compare()來決定元素的存儲的位置,數據結構使用樹的結構,
  • 存儲到TreeSet的元素,要可比較的,要麼元素本身是實現了Comparable接口的,要麼爲這個元素的類型單獨指定一個比較器,這個比較器實現了Comparator接口
  • 元素如果實現了Comparable接口,重寫了compareTo()方法,那麼按照邏輯角度,應該也重寫equals方法,使得e1.compareTo(e2)==0,e1.equals(e2)==true保持一致,那麼equals重寫,往往hashcode方法也要重寫

四:Map
1.HashMap

  • 底層數組+鏈表結構,線程不安全,entry類型數組,初始容量16,加載因子0.75,擴容1倍,key、value允許爲null值
  • 存儲過程:計算key的HashCode值,根據HashCode值計算出該元素在entry數組中對應的索引下標,如果該下標處沒有元素就直接存儲,如果有通過equal方法比較,如果相同就不存儲,如果不相同就以鏈接的形式在該下標處存儲元素
  • 擴容:可能造成無效擴容,在插入元素後再判斷是否需要擴容(有可能插入元素後擴容了,但不再有元素進來)
  • index計算: hash & (tab.length – 1)
  • 1.7和1.8區別
    1.7的數據結構爲:動態數組+單鏈表,1.8的數據結構爲:動態數組+(鏈表+紅黑樹),鏈表長度>8轉換爲紅黑樹

2.LinkedHashMap

  • HashMap的子類,用一個鏈表維護了元素的存儲順序,可以按照存儲順序遍歷取出存儲的元素

3.TreeMap

  • 底層實現:紅黑樹
    線程非安全,不允許null,key不可以重複,value允許重複
  • key類型必須是可排序的,
    (1)key類型本身或它的父類實現了Comparable接口,自然排序,實現了int compareTo(T t);
    (2)爲key類型實現了一個Comparator比較器,定製排序,實現了int compare(T t1,T t2)
  • key調用compare或compareTo方法比較,決定存儲的位置

4.ConcurrentHashMap

  • JDK1.7 實現
  • 數據結構:分段數組+鏈表
  • 底層實現: 分段鎖機制,Segment+ReentrantLock+HashEntyy
    將Map分爲N個Segment,線程安全,但效率提升N倍,默認16倍
  • 核心靜態內部類:Segment、HashEntry
    1)Segment繼承ReentrantLock充當鎖的角色
    2)HashEntry封裝表的鍵值對
  • JDK1.8 實現
  • 數據結構:接近HashMap的結構,動態數組+鏈表
  • 底層實現:synchronized+cas+HashEntry+紅黑樹
  • put 操作:
    1)如果沒有初始化,就調用initTable()進行初始化
    2)如果沒有Hash衝突,就直接cas插入
    3)如果存在hash衝突,synchronized加鎖保證線程安全(鎖住鏈表或紅黑樹的頭節點),有2種情況,一種是鏈表形式直接遍歷尾端插入,一種是紅黑樹結構插入
    4)如果鏈表的長度>8,就先轉換爲紅黑樹
    5)添加成功,就調用addCount()方法,統計size(),並檢查是否需要擴容
  • get 操作:
    1)計算hash值,定位到在數組中的下標,如果是下標處鏈表的首節點就返回
    2)如果遇到擴容,會調用標誌正在擴容節點ForwardingNode的find方法,查找該節點,匹配就返回
    3)以上都不符合,遍歷鏈表,匹配就返回,不匹配返回null
  • 區別:
  • 鎖的粒度
    1.7鎖的粒度是segment,包含多個Hashentry,1.8只鎖一個HashEntry,首節點
  • 鏈表遍歷效率
    1.8使用紅黑樹優化鏈表,當鏈表的長度>8,就轉換爲紅黑樹,鏈表的查詢效率提高了
public V put(K key, V value) {
    return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode()); //兩次hash,減少hash衝突,可以均勻分佈
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) { //對這個table進行迭代
        Node<K,V> f; int n, i, fh;
        //這裏就是上面構造方法沒有進行初始化,在這裏進行判斷,爲null就調用initTable進行初始化,屬於懶漢模式初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果i位置沒有數據,就直接無鎖插入
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)//如果在進行擴容,則先進行擴容操作
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //如果以上條件都不滿足,那就要進行加鎖操作,也就是存在hash衝突,鎖住鏈表或者紅黑樹的頭結點
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) { //表示該節點是鏈表結構
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //這裏涉及到相同的key進行put就會覆蓋原先的value
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {  //插入鏈表尾部
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {//紅黑樹結構
                        Node<K,V> p;
                        binCount = 2;
                        //紅黑樹結構旋轉插入
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) { //如果鏈表的長度大於8時就會進行紅黑樹的轉換
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);//統計size,並且檢查是否需要擴容
    return null;
}
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode()); //計算兩次hash
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {//讀取首節點的Node元素
        if ((eh = e.hash) == h) { //如果該節點就是首節點就返回
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //hash值爲負值表示正在擴容,這個時候查的是ForwardingNode的find方法來定位到nextTable來
        //查找,查找到就返回
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {//既不是首節點也不是ForwardingNode,那就往下遍歷
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

5.HashTable

  • 底層實現:數組+鏈表,線程安全(相比ConcurrentHashMap效率低,鎖住整張表),key和value不能爲null,
  • 擴容:初始容量11,2倍+1擴容
  • index計算:(hash & 0x7FFFFFFF) % tab.length

6.HashTable與HashMap的區別

  • 線程安全性
    HashTable線程安全(synchronized,1.5之後提供了ConcurrentHashMap,它是HashTable的替代,比HashTable擴展性更好),HashMap線程不安全
  • null值
    HashTable中key和value都不允許出現null值,HashMap中null可以作爲主鍵(只有一個),當使用get()方法返回null值,不一定表示該HashMap沒有該值,需要用containsKey()方法來判斷
  • 性能
    當線程場景下,HashMap性能好於HashTable
  • 哈希值的使用
    HashTable直接使用對象的HashCode值,HashMap重新計算
  • 初始化大小和擴容方式不同,
    HashTable數組默認爲11,old*2+1擴容,加載因子0.75,HashMap默認數組大小爲16,2倍擴容,加載因子0.75
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章