1. 程式人生 > >Java集合之ArrayList與LinkList

Java集合之ArrayList與LinkList

math 容量 將不 ext rev prev first 位運算 異常

註:示例基於JDK1.8版本

參考資料:Java知音公眾號

本文超長,也是搬運的幹貨,希望小夥伴耐心看完。

Collection集合體系

技術分享圖片

List、Set、Map是集合體系的三個接口。

其中List和Set繼承了Collection接口。

List有序且元素可以重復,默認大小為10;ArrayList、LinkedList和Vector是三個主要的實現類。

Set元素不可以重復,HashSet和TreeSet是兩個主要的實現類。

Map也屬於集合系統,但和Collection接口不同。Map是key-value鍵值對形式的集合,key值不能重復,value可以重復;HashMap、TreeMap和Hashtable是三個主要的實現類。

--------------------------------------------------------------------------------------------------------------------

一、ArrayList

ArrayList基層是以數組實現的,可以存儲任何類型的數據,但數據容量有限制,超出限制時會擴增50%容量,查找元素效率高。

ArrayList是一個簡單的數據結構,因超出容量會自動擴容,可認為它是常說的動態數組。

源碼分析

A、屬性分析

/**
 * 默認初始化容量
 */
private static final int
DEFAULT_CAPACITY = 10; /** * 如果自定義容量為0,則會默認用它來初始化ArrayList。或者用於空數組替換。 */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * 如果沒有自定義容量,則會使用它來初始化ArrayList。或者用於空數組比對。 */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * 這就是ArrayList底層用到的數組 * 非私有,以簡化嵌套類訪問 * transient 在已經實現序列化的類中,不允許某變量序列化
*/ transient Object[] elementData; /** * 實際ArrayList集合大小 */ private int size; /** * 可分配的最大容量 */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

B、構造方法分析

1、不帶參數初始化,默認容量為10

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

2、根據initialCapacity初始化一個空數組,如果值為0,則初始化一個空數組

/**
 * 根據initialCapacity 初始化一個空數組
 */
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

3、通過集合做參數的形式初始化,如果集合為空,則初始化為空數組

/**
 * 通過集合做參數的形式初始化
 */
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

C、主要方法

1、trimToSize()方法:

用來最小實例化存儲,將容器大小調整為當前元素所占用的容量大小。

/**
 * 這個方法用來最小化實例存儲。
 */
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

判斷size的值,若為0,則將elementData置為空集合,若大於0,則將一份數組容量大小的集合復制給elementData。

2、clone()方法

克隆一個新數組。

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn‘t happen, since we are Cloneable
        throw new InternalError(e);
    }
}

通過調用Objectclone()方法來得到一個新的ArrayList對象,然後將elementData復制給該對象並返回。

3、add(E e)

在ArrayList末尾添加元素。

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
}

該方法首先調用了ensureCapacityInternal()方法,註意參數是size+1(數組已有參數個數+1個新參數),先來看看ensureCapacityInternal的源碼:

private void ensureCapacityInternal(int minCapacity) {
   if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
       minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
   }
   ensureExplicitCapacity(minCapacity);
}

方法說明:計算容量+確保容量(上上代碼)

計算容量:若elementData為空,則minCapacity值為默認容量和size+1(minCapacity)的最大值;若elementData不為空,minCapacity(size+1)不用進行操作

確保容量:如果size+1 > elementData.length證明數組已經放滿,則增加容量,調用grow()

private void ensureExplicitCapacity(int minCapacity) {
   modCount++;//計算修改次數

   // overflow-conscious code
   if (minCapacity - elementData.length > 0)
       grow(minCapacity);
}

private void grow(int minCapacity) {
   // overflow-conscious code
   int oldCapacity = elementData.length;

   // 擴展為原來的1.5倍
   int newCapacity = oldCapacity + (oldCapacity >> 1);

   // 如果擴為1.5倍還不滿足需求,直接擴為需求值
   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);
}

增加容量grow():默認1.5倍擴容。

  1. 獲取當前數組長度=>oldCapacity

  2. oldCapacity>>1 表示將oldCapacity右移一位(位運算),相當於除2。再加上1,相當於新容量擴容1.5倍。

  3. 如果newCapacity<mincapacity`,則`newcapacity mincapacity="size+1=2" elementdata="1" newcapacity="1+1""">&gt;1=1,1&lt;2所以如果不處理該情況,擴容將不能正確完成。

  4. 如果新容量比最大值還要大,則將新容量賦值為VM要求最大值。

  5. 將elementData拷貝到一個新的容量中。

也就是說,當增加數據的時候,如果ArrayList的大小已經不滿足需求時,那麽就將數組變為原長度的1.5倍,之後的操作就是把老的數組拷到新的數組裏面。

例如,默認的數組大小是10,也就是說當我們add10個元素之後,再進行一次add時,就會發生自動擴容,數組長度由10變為了15具體情況如下所示:

技術分享圖片

4、add(int index, E element)方法

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

rangeCheckForAdd()是越界異常檢測方法。

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

System.arraycopy方法:

public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
  • Object src : 原數組

  • int srcPos : 從元數據的起始位置開始

  • Object dest : 目標數組

  • int destPos : 目標數組的開始起始位置

  • int length : 要copy的數組的長度

5、set(int index, E element)方法:

用指定元素替換此列表中指定位置的元素。

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

E elementData(int index) {
    return (E) elementData[index];
}

6、indexOf(Object o):

返回數組中第一個與參數相等的值的索引,允許null。

public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

7、get(int index)方法:

返回指定下標處的元素的值。

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

rangeCheck(index)會檢測index值是否合法,如果合法則返回索引對應的值。

8、remove(int index):

刪除指定下標的元素。

public E remove(int index) {
    // 檢測index是否合法
    rangeCheck(index);
    // 數據結構修改次數
    modCount++;
    E oldValue = elementData(index);

    // 記住這個算法
    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

    return oldValue;
}

ArrayList優缺點

優點:

1、因為其底層是數組,所以修改和查詢效率高。

2、自動擴容(1.5倍)。

缺點:

1、插入和刪除效率不高。(文末對比LinkedList)

2、線程不安全。

二、LinkedList

LinkList以雙向鏈表實現,鏈表無容量限制,但雙向鏈表本身使用了更多空間,每插入一個元素都要構造一個額外的Node對象,也需要額外的鏈表指針操作。允許元素為null,線程不安全。

技術分享圖片

源碼分析

1、變量

/**
 * 集合元素數量
 **/
transient int size = 0;

/**
 * 指向第一個節點的指針
 * Invariant: (first == null && last == null) ||
 *            (first.prev == null && first.item != null)
 */
transient Node<E> first;

/**
 * 指向最後一個節點的指針
 * Invariant: (first == null && last == null) ||
 *            (last.next == null && last.item != null)
 */
transient Node<E> last;

2、構造方法

/**
 * 無參構造方法
 */
public LinkedList() {
}

/**
 * 將集合c所有元素插入鏈表中
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

3、Node節點

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

Node既有prev也有next,所以證明它是一個雙向鏈表。

4、添加元素

a、addAll(Collection c)方法

將集合c添加到鏈表,如果不傳index,則默認是添加到尾部。如果調用addAll(int index, Collection<? extends E> c)方法,則添加到index後面。

/**
 * 將集合添加到鏈尾
 */
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

/** 
 * 
 */
public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);

    // 拿到目標集合數組
    Object[] a = c.toArray();
    //新增元素的數量
    int numNew = a.length;
    //如果新增元素數量為0,則不增加,並返回false
    if (numNew == 0)
        return false;

    //定義index節點的前置節點,後置節點
    Node<E> pred, succ;
    // 判斷是否是鏈表尾部,如果是:在鏈表尾部追加數據
    //尾部的後置節點一定是null,前置節點是隊尾
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        // 如果不在鏈表末端(而在中間部位)
        // 取出index節點,並作為後繼節點
        succ = node(index);
        // index節點的前節點 作為前驅節點
        pred = succ.prev;
    }

    // 鏈表批量增加,是靠for循環遍歷原數組,依次執行插入節點操作
    for (Object o : a) {
        @SuppressWarnings("unchecked") 
        // 類型轉換
        E e = (E) o;
        // 前置節點為pred,後置節點為null,當前節點值為e的節點newNode
        Node<E> newNode = new Node<>(pred, e, null);
        // 如果前置節點為空, 則newNode為頭節點,否則為pred的next節點
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        pred = newNode;
    }

    // 循環結束後,如果後置節點是null,說明此時是在隊尾追加的
    if (succ == null) {
        // 設置尾節點
        last = pred;
    } else {
    //否則是在隊中插入的節點 ,更新前置節點 後置節點
        pred.next = succ;
        succ.prev = pred;
    }

    // 修改數量size
    size += numNew;
    //修改modCount
    modCount++;
    return true;
}

/**
  * 取出index節點
  */ 
Node<E> node(int index) {
    // assert isElementIndex(index);

    // 如果index 小於 size/2,則從頭部開始找
    if (index < (size >> 1)) {
        // 把頭節點賦值給x
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            // x=x的下一個節點
            x = x.next;
        return x;
    } else {
        // 如果index 大與等於 size/2,則從後面開始找
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}


// 檢測index位置是否合法
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

// 檢測index位置是否合法
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}  

假設我們要在index=2處添加{1,2}到鏈表中,圖解如下:

第一步:拿到index=2的前驅節點 prev=ele1

第二步:遍歷集合prev.next=newNode,並實時更新prev節點以便下一次遍歷:prev=newNode

第三步:將index=2的節點ele2接上:prev.next=ele2,ele2.prev=prev

技術分享圖片

註意node(index)方法:方法:尋找處於index的節點,有一個小優化,結點在前半段則從頭開始遍歷,在後半段則從尾開始遍歷,這樣就保證了只需要遍歷最多一半結點就可以找到指定索引的結點。

b、addFirst(E e)方法

將e元素添加到鏈表並設置其為頭節點(first)。

public void addFirst(E e) {
    linkFirst(e);
}

//將e鏈接成列表的第一個元素
private void linkFirst(E e) {

    final Node<E> f = first;
    // 前驅為空,值為e,後繼為f
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    //若f為空,則表明列表中還沒有元素,last也應該指向newNode
    if (f == null)
        last = newNode;
    else
    //否則,前first的前驅指向newNode
        f.prev = newNode;
    size++;
    modCount++;
}
  1. 拿到first節點命名為f

  2. 新創建一個節點newNode設置其next節點為f節點

  3. 將newNode賦值給first

  4. 若f為空,則表明列表中還沒有元素,last也應該指向newNode;否則,前first的前驅指向newNode。

  5. 圖解如下:

技術分享圖片

技術分享圖片

c、addLast(E e)方法

將e元素添加到鏈表並設置其為尾節點(last)。

public void addLast(E e) {
    linkLast(e);
}
/**
 * 將e鏈接成列表的last元素
 */
void linkLast(E e) {
    final Node<E> l = last;
    // 前驅為前last,值為e,後繼為null
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    //最後一個節點為空,說明列表中無元素
    if (l == null)
        //first同樣指向此節點
        first = newNode;
    else
        //否則,前last的後繼指向當前節點
        l.next = newNode;
    size++;
    modCount++;
}

過程與linkFirst()方法類似,這裏略過。

d、add(E e)方法

在尾部追加元素e。

public boolean add(E e) {
    linkLast(e);
    return true;
}

void linkLast(E e) {
    final Node<E> l = last;
    // 前驅為前last,值為e,後繼為null
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    //最後一個節點為空,說明列表中無元素
    if (l == null)
        //first同樣指向此節點
        first = newNode;
    else
        //否則,前last的後繼指向當前節點
        l.next = newNode;
    size++;
    modCount++;
}

e、add(int index, E element)方法

在鏈表的index處添加元素element.

public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
/**
 * 在succ節點前增加元素e(succ不能為空)
 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    // 拿到succ的前驅
    final Node<E> pred = succ.prev;
    // 新new節點:前驅為pred,值為e,後繼為succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 將succ的前驅指向當前節點
    succ.prev = newNode;
    // pred為空,說明此時succ為首節點
    if (pred == null)
        // 指向當前節點
        first = newNode;
    else
        // 否則,將succ之前的前驅的後繼指向當前節點
        pred.next = newNode;
    size++;
    modCount++;
}

5、獲取/查詢元素

a、get(int index)

根據索引獲取鏈表中的元素。

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

// 檢測index合法性
private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

// 根據index 獲取元素
Node<E> node(int index) {
    // assert isElementIndex(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;
    }
}

b、getFirst()方法

獲取頭節點。

public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

c、getLast()方法

獲取尾節點。

public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

6、刪除元素

a、remove(Object o)

根據Object對象刪除元素。

public boolean remove(Object o) {
    // 如果o是空
    if (o == null) {
        // 遍歷鏈表查找 item==null 並執行unlink(x)方法刪除
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

E unlink(Node<E> x) {
    // assert x != null;
    // 保存x的元素值
    final E element = x.item;
    //保存x的後繼
    final Node<E> next = x.next;
    //保存x的前驅
    final Node<E> prev = x.prev;

    //如果前驅為null,說明x為首節點,first指向x的後繼
    if (prev == null) {
        first = next;
    } else {
        //x的前驅的後繼指向x的後繼,即略過了x
        prev.next = next;
        // x.prev已無用處,置空引用
        x.prev = null;
    }

    // 後繼為null,說明x為尾節點
    if (next == null) {
        // last指向x的前驅
        last = prev;
    } else {
        // x的後繼的前驅指向x的前驅,即略過了x
        next.prev = prev;
        // x.next已無用處,置空引用
        x.next = null;
    }
    // 引用置空
    x.item = null;
    size--;
    modCount++;
    // 返回所刪除的節點的元素值
    return element;
}
  1. 遍歷鏈表查找 item==null 並執行unlink(x)方法刪除

  2. 如果前驅為null,說明x為首節點,first指向x的後繼,x的前驅的後繼指向x的後繼,即略過了x.

  3. 如果後繼為null,說明x為尾節點,last指向x的前驅;否則x的後繼的前驅指向x的前驅,即略過了x,置空x.next

  4. 引用置空:x.item = null

  5. 圖解:

技術分享圖片

b、remove(int index)方法

根據鏈表的索引刪除元素。

public E remove(int index) {
    checkElementIndex(index);
    //node(index)會返回index對應的元素
    return unlink(node(index));
}

c、removeLast()方法

刪除尾節點。

public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    // 取出尾節點中的元素
    final E element = l.item;
    // 取出尾節點中的後繼
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null; // help GC
    // last指向前last的前驅,也就是列表中的倒數2號位
    last = prev;
    // 如果此時倒數2號位為空,那麽列表中已無節點
    if (prev == null)
        // first指向null
        first = null;
    else
        // 尾節點無後繼
        prev.next = null;
    size--;
    modCount++;
    // 返回尾節點保存的元素值
    return element;
}

7、修改元素

修改元素比較簡單,先找到index對應節點,然後對值進行修改。

public E set(int index, E element) {
    checkElementIndex(index);
    // 獲取到需要修改元素的節點
    Node<E> x = node(index);
    // 保存之前的值
    E oldVal = x.item;
    // 執行修改
    x.item = element;
    // 返回舊值
    return oldVal;
}

LinkedList優點:不需要擴容和預留空間,空間效率高。

三、ArrayList與LinkedList插入和查找消耗時間測試對比

參考鏈接:https://blog.csdn.net/dearKundy/article/details/84663512

在ArrayList和LinkedList的頭部、尾部和中間三個位置插入與查找100000個元素所消耗的時間來進行對比測試,下面是測試結果:(時間單位ms)

插入 查找
ArrayList尾部 26 12
ArrayList頭部 859 7
ArrayList中間 1848 13
LinkedList尾部 28 9
LinkedList頭部 15 11
LinkedList中間 15981 34928

測試結論:
ArrayList的查找性能絕對是一流的,無論查詢的是哪個位置的元素
ArrayList除了尾部插入的性能較好外(位置越靠後性能越好),其他位置性能就不如人意了
LinkedList在頭尾查找、插入性能都是很棒的,但是在中間位置進行操作的話,性能就差很遠了,而且跟ArrayList完全不是一個量級的

根據源碼分析所得結論:

  • 對於LinkedList來說,頭部插入和尾部插入時間復雜度都是O(1)
  • 但是對於ArrayList來說,頭部的每一次插入都需要移動size-1個元素,效率可想而知
  • 但是如果都是在最中間的位置插入的話,ArrayList速度比LinkedList的速度快將近10倍

Java集合之ArrayList與LinkList