Java集合(2)一 ArrayList 與 LinkList

引言

ArrayList<E>和LinkList<E>在繼承關係上都繼承自List<E>接口,上篇文章咱們分析了List<E>接口的特色:有序,能夠重複,而且能夠經過整數索引來訪問。 他們在自身特色上有不少類似之處,在具體實現上ArrayList<E>和LinkList<E>又有很大不一樣,ArrayList<E>經過數組實現,LinkList<E>則使用了雙向鏈表。將他們放到一塊兒學習能夠更清楚的理解他們的區別。java

框架結構

從上面的結構圖能夠看出ArrayList<E>和LinkList<E>在繼承結構上基本相同,值得注意的是LinkList<E>在繼承了List<E>接口的同時還繼承了Deque<E>接口。 Deque<E>是一個雙端隊列的接口,LinkList<E>因爲在實現上採用了雙向鏈表,因此能夠很天然的實現雙端隊列頭尾進出的特色。node

數據結構

上一篇文章中咱們說過,爲何一個Collection<E>接口會衍生出這麼多實現類,其中最大的緣由就是每一種實如今數據結構上都有差異,而不一樣的數據結構又致使了每種集合在使用場景上又各有不一樣。 ArrayList<E>和LinkList<E>的根本區別就在數據結構上,只有瞭解了他們各自的數據結構,才能更加深刻的明白他們各自的使用場景。 在ArrayList<E>的源代碼中有一個elementData變量,這個變量就表明了ArrayList<E>所使用的數據結構:數組。編程

//The array buffer into which the elements of the ArrayList are stored.
transient Object[] elementData;
複製代碼

elementData變量是ArrayList<E>操做的基礎,他全部的操做都是基於elementData這個Object類型的數組來實現的。 數組有如下幾個特色:數組

  • 數組大小一旦初始化以後,長度固定。
  • 數組中元素之間的內存地址是連續的。
  • 只能存儲一種類數據類型的元素。

在這裏面有個transient關鍵字值得注意,他的做用是標誌當前對象不須要序列化。 若是你們瞭解序列化,請跳過下面的介紹: 序列化是什麼? 序列化簡單說就是將一個對象持久化的過程。將對象轉換成字節流的過程就叫序列化,一個對象要在網絡中傳播就必須被轉換成字節流。對應的,一個對象從字節流轉換成對象的過程就叫反序列化。 在Java中,標誌一個對象能夠被序列化只須要繼承Serializable接口便可,Serializable接口是一個空接口。 明白了什麼是序列化的概念,再來看transient關鍵字,java中規定被聲明爲transient的關鍵在被序列化的時候會被忽略,但是爲何要忽略這個對象呢?若是被忽略了那反序列化的時候這個對象怎樣恢復呢? 咱們先來想一想什麼樣的對象在序列化時須要被忽略?序列化是一個耗時也耗費空間的過程,通常在一個對象中除了必須持久化的變量,還會存在不少中間變量或臨時變量,聲明這些變量的做用是方便咱們操做這個類,舉個例子:bash

import java.io.IOException;
import java.io.ObjectInputStream;

public class SerializableDateTime implements java.io.Serializable {

	private static final long serialVersionUID = -8291235042612920489L;

	private String date = "2011-11-11";

	private String time = "11:11";

    //不須要序列化的對象
	private transient String dateTime;

	public void initDateTime() {
		dateTime = date + time;
	}

    //反序列化的時候調用,給dateTime賦值
	private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
		inputStream.defaultReadObject();
		initDateTime();
	}
}
複製代碼

SerializableDateTime對象中的dateTime對象若是在外界調用的時候會賦值,可是這個對象並非基礎數據,不須要序列化,在反序列化的時候能夠經過調用initDateTime返回獲取他的值,因此只須要序列化date和time對象便可。將dateTime對象標記爲transient,則能夠達到按需序列化的目的。 那在ArrayList<E>中爲何要忽略elementData這個對象呢? 主要是由於elementData對象不只包含了全部有用的元素,還存在許多沒有未使用的空間,而這些空間是不須要所有序列化的,爲了節約空間,因此只序列化了elementData中存有對象的那一部分,在反序列化的時候又恢復elementData對象的空間,這樣能夠達到節約序列化空間和時間的目的。網絡

//序列化時調用
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    //序列化size大小的元素,size的大小是實際存儲元素的大小,不是elementData元素的大小
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

//反序列化時調用
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        //恢復elementData對象的空間
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            //填充elementData元素的內容
            a[i] = s.readObject();
        }
    }
}
複製代碼

這種序列化和反序列化的方法很是巧妙,在咱們編程的過程當中也能夠借鑑這種辦法來節約序列化和反序列化的空間和時間。數據結構

LinkedList<E>在底層實現上採用了鏈表這種數據結構,並且是雙向鏈表,即每一個元素都包含他的上一個和下一個元素的引用:框架

//鏈表的第一個元素
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;
    }
}
複製代碼

鏈表的特色:dom

  • 長度不固定,能夠隨時增長和減小
  • 鏈表中的元素在內存地址上能夠是連續的,也能夠是不連續的,大部分狀況下都是不連續的。

構造函數

ArrayList<E>提供了3種構造方式,默認的構造函數會初始化一個空的數組,在以後添加元素的過程當中會對數組進行擴容,擴容操做在必定程度上會影響數組的性能。若是能提早預估最終的數組使用空間大小,能夠經過ArrayList(int 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);
    }
}

public ArrayList() {
    //初始化一個空的數組
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

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;
    }
}
複製代碼

LinkList<E>只提供了2種構造方式,默認的構造函數是一個空函數,由於鏈表這種數據結構在使用上不須要初始化空間,也不須要擴容,每次須要添加元素時直接追加就能夠,在空間的最大化利用上鍊表比數組更加合理。這並不表明鏈表使用的空間小,相反,鏈表每一個節點由於要存儲下一個節點引用(雙向鏈表會存儲上下兩個節點的引用),在相同元素空間使用上會比數組大的多。

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
複製代碼

添加元素

ArrayList<E>在添加元素的過程當中,須要考慮數組空間是否足夠,不夠的狀況下須要擴容。

//ArrayList<E>添加元素到末尾
public boolean add(E e) {
    //檢查數組容量,不夠就擴容,擴容調用grow(int minCapacity) 方法
    ensureCapacityInternal(size + 1);  
    elementData[size++] = e;
    return true;
}

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;
    //向右位移一位,至關於除以2,比除法運算要快,每次擴容在原容量的基礎上增長一半,新的容量爲原容量的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:
    //拷貝全部數據元素到新的數組中,內部調用System.arraycopy來拷貝全部數組元素
    elementData = Arrays.copyOf(elementData, newCapacity);
}
複製代碼

不擴容: 擴容:

從中能夠看出,不擴容的狀況下添加元素到末尾很是方便,時間複雜度爲O(1),擴容的狀況下每次都須要拷貝全部元素到新數組,時間複雜度上爲O(n),存在必定性能損耗。
LinkedList<E>在添加元素時因爲鏈表的特性,不須要考慮擴容的問題,但LinkedList<E>每次都須要new一個Node來存儲元素。

//LinkedList<E>添加元素到末尾
public boolean add(E e) {
    linkLast(e);
    return true;
}

void linkLast(E e) {
    final Node<E> l = last;
    //new一個新的鏈表元素並連接到末尾
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
複製代碼

ArrayList<E>在添加元素到指定索引位置的時候,除了檢查容量以外,因爲數組具備在空間連續存儲的特性,還須要對插入元素以後的全部節點作一次位移。 ```java //ArrayList添加元素到指定索引位置 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++;
複製代碼

}

<img src="http://images2017.cnblogs.com/blog/368583/201711/368583-20171130181801667-175597278.png" style="max-width: 770px">

LinkedList&lt;E>添加到指定位置時首先須要先查找元素的位置,而後添加。
```java
//LinkedList<E>添加元素到指定索引位置
public void add(int index, E element) {
    checkPositionIndex(index);
    
    if (index == size)
        //直接添加元素到末尾
        linkLast(element);
    else
        //添加到指定位置前先查找當前位置已經存在的元素
        linkBefore(element, node(index));
}

//查找指定索引的元素
Node<E> node(int index) {
    // assert isElementIndex(index);
    //指定索引小於元素數量的一半時從first開始遍歷,大於元素數量的一半時從last開始遍歷
    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;
    }
}
複製代碼

LinkedList<E>的這種查找對性能有影響嗎?相比ArrayList<E>的擴容以及位移插入位後面全部的元素性能如何?咱們來對插入到頭部、尾部以及中間位置3種特殊狀況作個簡單測試。 插入到尾部:

private static void addTailElementArrayList(int count) {

    long startTime = System.currentTimeMillis();
    List<Integer> list = new ArrayList<Integer>();
    for (int i = 0; i < count; i++) {
        list.add(i);
    }
    long endTime = System.currentTimeMillis();
    System.out.println("addTailElementArrayList time: " + (endTime - startTime));
}

private static void addTailElementLinkedList(int count) {

    long startTime = System.currentTimeMillis();
    List<Integer> list = new LinkedList<Integer>();
    for (int i = 0; i < count; i++) {
        list.add(i);
    }
    long endTime = System.currentTimeMillis();
    System.out.println("addTailElementLinkedList time: " + (endTime - startTime));
}
複製代碼
100 1000 10000 100000
ArrayList 0 0 1 160
LinkList 0 0 1 110

插入到頭部:

private static void addHeadElementArrayList(int count) {

    long startTime = System.currentTimeMillis();
    List<Integer> list = new ArrayList<Integer>();
    for (int i = 0; i < count; i++) {
        list.add(0, i);
    }
    long endTime = System.currentTimeMillis();
    System.out.println("addHeadElementArrayList time: " + (endTime - startTime));
}

private static void addHeadElementLinkedList(int count) {

    long startTime = System.currentTimeMillis();
    List<Integer> list = new LinkedList<Integer>();
    for (int i = 0; i < count; i++) {
        list.add(0, i);
    }
    long endTime = System.currentTimeMillis();
    System.out.println("addHeadElementLinkedList time: " + (endTime - startTime));
}
複製代碼
100 1000 10000 100000
ArrayList 0 1 10 900
LinkList 0 1 1 6

插入到中間:

private static void addCenterIndexElementArrayList(int count) {

    long startTime = System.currentTimeMillis();
    List<Integer> list = new ArrayList<Integer>();
    for (int i = 0; i < count; i++) {
        list.add(list.size()>>1, i);
    }
    long endTime = System.currentTimeMillis();
    System.out.println("addCenterIndexElementArrayList time: " + (endTime - startTime));
}

private static void addCenterIndexElementLinkedList(int count) {

    long startTime = System.currentTimeMillis();
    List<Integer> list = new LinkedList<Integer>();
    for (int i = 0; i < count; i++) {
        list.add(list.size()>>1, i);
    }
    long endTime = System.currentTimeMillis();
    System.out.println("addCenterIndexElementLinkedList time: " + (endTime - startTime));
}
複製代碼
100 1000 10000 100000
ArrayList 0 1 6 400
LinkList 0 3 80 10000

從中能夠得處幾個簡單結論:

  • 在添加到末尾時,ArrayList<E>和LinkedList<E>在性能上差距不明顯,儘管ArrayList<E>須要擴容,但LinkedList<E>也須要new一個Node對象。
  • 在插入到頭部時,LinkedList<E>性能明顯好於ArrayList<E>,由於ArrayList<E>每次都須要將全部元素向後移動一個位置,而LinkedList<E>因爲是雙向鏈表每次只須要改變first元素就能夠了。
  • 在插入到中間位置的時候,ArrayList<E>性能優明顯好於LinkedList<E>,這是由於ArrayList<E>此時只須要移動一半的元素,而LinkedList<E>由於其雙向鏈表查找元素的特殊性,只能從頭或者尾部開始遍歷,每次都須要遍歷一半的元素,這個操做耗費了大量時間,而ArrayList<E>在擴容以及移動元素上的性能消耗比想象的要小。

咱們在ArrayList<E>和LinkedList<E>的選擇上,須要充分考慮使用時的場景,LinkedList<E>在插入數據上並非必定比ArrayList<E>性能好,相反的在不少狀況下ArrayList<E>性能反而要好的多。不能由於插入操做多,就必定選用LinkedList<E>,還須要考慮插入元素的位置等其餘因素來最終決定。

刪除元素

ArrayList<E>刪除元素經過遍歷元素查找到相等的元素而後使用索引刪除,刪除以後還要將被刪除元素後的元素前移。

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            //查找到equals的元素的索引而後刪除
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

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
}
複製代碼

LinkedList<E>經過向後遍歷鏈表的方式查找到equals的元素直接刪除便可。

public boolean remove(Object o) {
    if (o == null) {
        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;
}
複製代碼

遍歷元素

在遍歷元素上ArrayList<E>存在更有效的方式,他實現了RandomAccess接口,表明ArrayList<E>支持快速訪問。 RandomAccess自己是一個空接口,這種接口通常用來表明一類特徵,RandomAccess表明實現類具備快速訪問的特徵。ArrayList<E>實現快速訪問的方式是經過索引。這表明ArrayList<E>在遍歷時經過for循環方式要比經過Iterator或ListIterator迭代器方式要快。LinkedList<E>沒有實現這個藉口,因此通常仍是經過Iterator迭代器來訪問。

相關文章
相關標籤/搜索