Java 經常使用List集合使用場景分析

Java 經常使用List集合使用場景分析

過年前的最後一篇,本章經過介紹ArrayList,LinkedList,Vector,CopyOnWriteArrayList 底層實現原理和四個集合的區別。讓你清楚明白,爲何工做中會經常使用ArrayList和CopyOnWriteArrayList?瞭解底層實現原理,咱們能夠學習到不少代碼設計的思路,開闊本身的思惟。本章通俗易懂,還在等什麼,快來學習吧!java

知識圖解:node

JavaList集合圖解

技術:ArrayList,LinkedList,Vector,CopyOnWriteArrayList
說明:本章基於jdk1.8,github上有ArrayList,LinkedList的簡單源碼代碼
源碼:https://github.com/ITDragonBl...git

知識預覽

ArrayList : 基於數組實現的非線程安全的集合。查詢元素快,插入,刪除中間元素慢。
LinkedList : 基於鏈表實現的非線程安全的集合。查詢元素慢,插入,刪除中間元素快。
Vector : 基於數組實現的線程安全的集合。線程同步(方法被synchronized修飾),性能比ArrayList差。
CopyOnWriteArrayList : 基於數組實現的線程安全的寫時複製集合。線程安全(ReentrantLock加鎖),性能比Vector高,適合讀多寫少的場景。github

ArrayList 和 LinkedList 讀寫快慢的本質

ArrayList : 查詢數據快,是由於數組能夠經過下標直接找到元素。 寫數據慢有兩個緣由:一是數組複製過程須要時間,二是擴容須要實例化新數組也須要時間。
LinkedList : 查詢數據慢,是由於鏈表須要遍歷每一個元素直到找到爲止。 寫數據快有一個緣由:除了實例化對象須要時間外,只須要修改指針便可完成添加和刪除元素。
本章會經過源碼分析,驗證上面的說法。面試

注:這裏的塊和慢是相對的。並非LinkedList的插入和刪除就必定比ArrayList快。明白其快慢的本質:ArrayList快在定位,慢在數組複製。LinkedList慢在定位,快在指針修改數組

ArrayList

ArrayList 是基於動態數組實現的非線程安全的集合。當底層數組滿的狀況下還在繼續添加的元素時,ArrayList則會執行擴容機制擴大其數組長度。ArrayList查詢速度很是快,使得它在實際開發中被普遍使用。美中不足的是插入和刪除元素較慢,同時它並非線程安全的。
咱們能夠從源碼中找到答案安全

// 查詢元素
public E get(int index) {
    rangeCheck(index);                    // 檢查是否越界
    return elementData(index);
}
// 順序添加元素
public boolean add(E e) {
    ensureCapacityInternal(size + 1);      // 擴容機制
    elementData[size++] = e;
    return true;
}
// 從數組中間添加元素
public void add(int index, E element) {
    rangeCheckForAdd(index);            // 數組下標越界檢查
    ensureCapacityInternal(size + 1);      // 擴容機制
    System.arraycopy(elementData, index, elementData, index + 1, size - index); // 複製數組
    elementData[index] = element;        // 替換元素
    size++;
}
// 從數組中刪除元素
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

從源碼中能夠得知,
ArrayList在執行查詢操做時:
第一步:先判斷下標是否越界。
第二步:而後在直接經過下標從數組中返回元素。數據結構

ArrayList在執行順序添加操做時:
第一步:經過擴容機制判斷原數組是否還有空間,若沒有則從新實例化一個空間更大的新數組,把舊數組的數據拷貝到新數組中。
第二步:在新數組的最後一位元素添加值。併發

ArrayList在執行中間插入操做時:
第一步:先判斷下標是否越界。
第二步:擴容。
第三步:若插入的下標爲i,則經過複製數組的方式將i後面的全部元素,日後移一位。
第四步:新數據替換下標爲i的舊元素。
刪除也是同樣:只是數組往前移了一位,最後一個元素設置爲null,等待JVM垃圾回收。高併發

從上面的源碼分析,咱們能夠獲得一個結論和一個疑問。
結論是:ArrayList快在下標定位,慢在數組複製。
疑問是:可否將每次擴容的長度設置大點,減小擴容的次數,從而提升效率?其實每次擴容的長度大小是頗有講究的。若擴容的長度太大,會形成大量的閒置空間;若擴容的長度過小,會形成頻發的擴容(數組複製),效率更低。

LinkedList

LinkedList 是基於雙向鏈表實現的非線程安全的集合,它是一個鏈表結構,不能像數組同樣隨機訪問,必須是每一個元素依次遍歷直到找到元素爲止。其結構的特殊性致使它查詢數據慢。
咱們能夠從源碼中找到答案

// 查詢元素
public E get(int index) {
    checkElementIndex(index);     // 檢查是否越界
    return node(index).item;
}
Node<E> node(int 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;
    }
}
// 插入元素
public void add(int index, E element) {
    checkPositionIndex(index);    // 檢查是否越界
    if (index == size)            // 在鏈表末尾添加
        linkLast(element);
    else                        // 在鏈表中間添加
        linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
    final Node<E> pred = succ.prev; 
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)    
        first = newNode;
    else                
        pred.next = newNode;
    size++;
    modCount++;
}

從源碼中能夠得知,
LinkedList在執行查詢操做時:
第一步:先判斷元素是靠近頭部,仍是靠近尾部。
第二步:若靠近頭部,則從頭部開始依次查詢判斷。和ArrayList的elementData(index)相比固然是慢了不少。

LinkedList在插入元素的思路:
第一步:判斷插入元素的位置是鏈表的尾部,仍是中間。
第二步:若在鏈表尾部添加元素,直接將尾節點的下一個指針指向新增節點。
第三步:若在鏈表中間添加元素,先判斷插入的位置是否爲首節點,是則將首節點的上一個指針指向新增節點。不然先獲取當前節點的上一個節點(簡稱A),並將A節點的下一個指針指向新增節點,而後新增節點的下一個指針指向當前節點。

Vector

Vector 的數據結構和使用方法與ArrayList差很少。最大的不一樣就是Vector是線程安全的。從下面的源碼能夠看出,幾乎全部的對數據操做的方法都被synchronized關鍵字修飾。synchronized是線程同步的,當一個線程已經得到Vector對象的鎖時,其餘線程必須等待直到該鎖被釋放。從這裏就能夠得知Vector的性能要比ArrayList低。
若想要一個高性能,又是線程安全的ArrayList,可使用Collections.synchronizedList(list);方法或者使用CopyOnWriteArrayList集合

public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}
public synchronized boolean removeElement(Object obj) {
    modCount++;
    int i = indexOf(obj);
    if (i >= 0) {
        removeElementAt(i);
        return true;
    }
    return false;
}

CopyOnWriteArrayList

在這裏咱們先簡單瞭解一下CopyOnWrite容器。它是一個寫時複製的容器。當咱們往一個容器添加元素的時候,不是直接往當前容器添加,而是先將當前容器進行copy一份,複製出一個新的容器,而後對新容器裏面操做元素,最後將原容器的引用指向新的容器。因此CopyOnWrite容器是一種讀寫分離的思想,讀和寫不一樣的容器。
應用場景:適合高併發的讀操做(讀多寫少)。若寫的操做很是多,會頻繁複制容器,從而影響性能。

CopyOnWriteArrayList 寫時複製的集合,在執行寫操做(如:add,set,remove等)時,都會將原數組拷貝一份,而後在新數組上作修改操做。最後集合的引用指向新數組。
CopyOnWriteArrayList 和Vector都是線程安全的,不一樣的是:前者使用ReentrantLock類,後者使用synchronized關鍵字。ReentrantLock提供了更多的鎖投票機制,在鎖競爭的狀況下能表現更佳的性能。就是它讓JVM能更快的調度線程,纔有更多的時間去執行線程。這就是爲何CopyOnWriteArrayList的性能在大併發量的狀況下優於Vector的緣由。

private E get(Object[] a, int index) {
    return (E) a[index];
}
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
private boolean remove(Object o, Object[] snapshot, int index) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        ......
        Object[] newElements = new Object[len - 1];
        System.arraycopy(current, 0, newElements, 0, index);
        System.arraycopy(current, index + 1, newElements, index, len - index - 1);
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

總結

看到這裏,若是面試官問你ArrayList和LinkedList有什麼區別時
若是你回答:ArrayList查詢快,寫數據慢;LinkedList查詢慢,寫數據快。面試官只能算你勉強合格。
若是你回答:ArrayList查詢快是由於底層是由數組實現,經過下標定位數據快。寫數據慢是由於複製數組耗時。LinkedList底層是雙向鏈表,查詢數據依次遍歷慢。寫數據只需修改指針引用。
若是你繼續回答:ArrayList和LinkedList都不是線程安全的,小併發量的狀況下可使用Vector,若併發量不少,且讀多寫少能夠考慮使用CopyOnWriteArrayList。
由於CopyOnWriteArrayList底層使用ReentrantLock鎖,比使用synchronized關鍵字的Vector能更好的處理鎖競爭的問題。
面試官會認爲你是一個基礎紮實,內功深厚的人才!!!

到這裏Java 經常使用List集合使用場景分析就結束了。過年前的最後一篇博客,有點浮躁,可能身在職場,心在老家!最後祝你們新年快樂!!!狗年吉祥!!!大富大貴!!!可能都沒人看博客了 ⊙﹏⊙‖∣ 哈哈哈哈(ಡωಡ)hiahiahia

相關文章
相關標籤/搜索