天天看點

java中的資料集合--List源碼分析CollectionList再看一下LinkList和ArrayList的不同與多數部落格的錯誤認知

java中的資料集合--List源碼分析

  • Collection
  • List
    • ArrayList
      • Iterator方法
      • add方法與ArrayList的擴容
    • LinkList分析
      • 構造方法
      • add方法
  • 再看一下LinkList和ArrayList的不同與多數部落格的錯誤認知

Collection

先看一下collection的解釋說明部分:

/ *
 * @param <E> the type of elements in this collection
 *
 * @author  Josh Bloch
 * @author  Neal Gafter
 * @see     Set
 * @see     List
 * @see     Map
 * @see     SortedSet
 * @see     SortedMap
 * @see     HashSet
 * @see     TreeSet
 * @see     ArrayList
 * @see     LinkedList
 * @see     Vector
 * @see     Collections
 * @see     Arrays
 * @see     AbstractCollection
 * @since 1.2
 */
           

這個解釋實際上基本涵蓋了所有我們編碼過程中使用的資料集合類,可以分析一下這些類的源碼,基本上java中的集合類對你再無什麼秘密。

public interface Collection<E> extends Iterable<E> {}
           

collection本身是一個接口,繼承自疊代器Iterable。

如上所述,确實隻能implement一個接口,但是作為interface同樣可以被extends調用,并且一個子類可以extends多個interface父類,這句話有些拗口,不過看看上面collection同樣是一個interface類,但是他用extends了一個類Iterable仍然是interface類型的。

java中的資料集合--List源碼分析CollectionList再看一下LinkList和ArrayList的不同與多數部落格的錯誤認知

上圖是collection類中的所有方法,同僚涵蓋了Iterable中的方法,還有自己的方法,比如toArray,增删等等。

java中的資料集合--List源碼分析CollectionList再看一下LinkList和ArrayList的不同與多數部落格的錯誤認知

上圖是collection的子類,從圖中可以看出,他的子類包含ArrayList,ArrayDueque,ArraySet,concurrentlist、concurrentSet之類的子類,實際上集合類的三大件,set 、list、 map三個中,除了map其餘的兩個都繼承自Collection。

List

List同樣是一個接口類,看一下源碼:

public interface List<E> extends Collection<E> {
           
java中的資料集合--List源碼分析CollectionList再看一下LinkList和ArrayList的不同與多數部落格的錯誤認知

類中的接口如上圖,我們通過分析ArrayList中的增删改查來看list接口方法和Iterator中的方法。

ArrayList

ArrayList繼承自SeriaLizable,表明它是一個可序列化的類

public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{}
           

Arraylist中存儲資料的資料結構是

transient Object[] elementData; // non-private to simplify nested class access
           

有上面一行代碼可以看出,ArrayList的底層結構實際上是一個數組,并且數組的類型是Object,并且ArrayList定義的時候是以泛型定義,這樣也就解釋了,為什麼ArrayList既可以存放基本類型的資料,也可以存放自定義類型的資料。

/**
 * Constructs an empty list with the specified initial capacity.
 *
 * @param  initialCapacity  the initial capacity of the list
 * @throws IllegalArgumentException if the specified initial capacity
 *         is negative
 */
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);
    }
}

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

/**
 * Constructs a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 *
 * @param c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 */
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;
    }
}
           

ArrayList有三個構造方法,分别是直接new一個不帶參數的ArrayList,一個帶有初始容量的ArrayList,或者參數是Collection的ArrayList,其中我們最常用的不帶參的ArrayList構造方法,實際上等于在new的時候建立了一個DEFAULTCAPACITY_EMPTY_ELEMENTDATA,這個是一個空的數組,這樣做的目的,筆者認為是減小不必要的記憶體損耗。

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
           

既然是數組那麼定義的時候就會定義一個初始容量,後面我們會講到一個ArrayList的另一個機制–擴容

Iterator方法

首先看ArrayList中的iterator方法:

/**
 * Returns an iterator over the elements in this list in proper sequence.
 *
 * <p>The returned iterator is <a href="#fail-fast" target="_blank" rel="external nofollow" ><i>fail-fast</i></a>.
 *
 * @return an iterator over the elements in this list in proper sequence
 */
public Iterator<E> iterator() {
    return new Itr();
}
           

注釋的意思是按照一定順序傳回清單中的元素,疊代的實作是通過一個内部類實作,每次疊代都會new一個Itr()

Itr()源碼如下:

/**
 * An optimized version of AbstractList.Itr
 */
private class Itr implements Iterator<E> {
    // Android-changed: Add "limit" field to detect end of iteration.
    // The "limit" of this iterator. This is the size of the list at the time the
    // iterator was created. Adding & removing elements will invalidate the iteration
    // anyway (and cause next() to throw) so saving this value will guarantee that the
    // value of hasNext() remains stable and won't flap between true and false when elements
    // are added and removed from the list.
    protected int limit = ArrayList.this.size;

    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor < limit;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        int i = cursor;
        if (i >= limit)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
            limit--;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;

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

下面這句話是在内部類中的next()方法完成對list中元素的拷貝,是以next的操作不會對list中的元素有所改變:

Object[] elementData = ArrayList.this.elementData;
           

下面這句話的意思是在多線程操作list的時候的異常檢查,也側面證明了ArrayList并不是一個線程安全的集合類。

throw new ConcurrentModificationException();
           

remove()中的實作過程核心是這句話,表明他是直接對ArrayList進行的操作。

ArrayList.this.remove(lastRet);
           

add方法與ArrayList的擴容

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

/**
 * Inserts the specified element at the specified position in this
 * list. Shifts the element currently at that position (if any) and
 * any subsequent elements to the right (adds one to their indices).
 *
 * @param index index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

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

add方法有兩個,一個是直接添加,一個是帶有index,可以指定位置添加,此處的添加不做過多讨論,看一下,add方法中的ensureCapacityInternal方法,此方法牽扯到ArrayList的另一個機制–擴容。

首先我們看一下ensureCapacityInternal方法源碼:

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

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

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

上面第一個方法中,實作了對new一個參數為空的ArrayList的容量初始化過程,初始化容量也正是DEFAULT_CAPACITY,這個常量值為10。

下面分析一下比較關鍵的一個方法,也就是最常分析的ArrayList的擴容機制。

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    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);
}
           

下面這句話的意思就是如果目前容量在不夠用的時候,會先擴容到目前數組長度的1.5倍,如果你一次添加的資料超過原來容量的1.5倍,那麼擴容的容量擴容為當下添加數量和原來數量的容量和,如果添加的資料量超過數組最大的容量,那麼就會報一個outofMemory錯誤:(初始容量的定義是,如果你第一次添加的資料量大于10,那麼你的初始容量就是你添加的長度,如果小于10,那麼數組的初時容量就是10),此處很多部落格上直接說arraylist擴容直接說擴容1.5倍是不準确的。

int newCapacity = oldCapacity + (oldCapacity >> 1);
           

至此,ArrayList的擴容機制講解完畢。

ArrayList中還有替換對應位置元素的方法,set方法,可以根據上面的分析方法檢視,比較簡單。

LinkList分析

連結清單結構印象中一直有些複雜,貌似牽扯到頭指針尾指針的問題,不過比較複雜的結構,也代表着更強大的功能,今天來從源碼分析一下。

構造方法

LinkList比ArrayList要複雜一些,首先看一下類的定義和構造

public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{}
           

從定義中可以看出,他內建在list,Deque,cloneable,Serializable,這些說明他可以序列化,可以實作雙端隊列,同時又是一個list。

接下來看一下類中的變量

transient int size = 0;

/**
 * Pointer to first node.
 * Invariant: (first == null && last == null) ||
 *            (first.prev == null && first.item != null)
 */
transient Node<E> first;

/**
 * Pointer to last node.
 * Invariant: (first == null && last == null) ||
 *            (last.next == null && last.item != null)
 */
transient Node<E> last;
           

變量隻有三個,一個是size表示連結清單的長度,然後分别是Node類型的頭first和尾部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;
    }
}
           

Node是LinkList的靜态内部類,分别維護了一個next和prev兩個節點,分别定義為節點前一個節點和後一個節點,而item則代表目前節點存放的内容。

下面看一下構造。

/**
 * Constructs an empty list.
 */
public LinkedList() {
}

/**
 * Constructs a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 *
 * @param  c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
           

構造分為兩個,我們先看一下帶參構造,稍後分析一下add方法。

帶參構造主要的實作過程是addAll方法

/**
 * Inserts all of the elements in the specified collection into this
 * list, starting at the specified position.  Shifts the element
 * currently at that position (if any) and any subsequent elements to
 * the right (increases their indices).  The new elements will appear
 * in the list in the order that they are returned by the
 * specified collection's iterator.
 *
 * @param index index at which to insert the first element
 *              from the specified collection
 * @param c collection containing elements to be added to this list
 * @return {@code true} if this list changed as a result of the call
 * @throws IndexOutOfBoundsException {@inheritDoc}
 * @throws NullPointerException if the specified collection is null
 */
public boolean addAll(int index, Collection<? extends E> c) {
//檢查時候有多線程修改導緻的異常
    checkPositionIndex(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;

    Node<E> pred, succ;
    //帶參初始化傳入的index==size
    if (index == size) {
    //此處pred首先指派為連結清單尾部
        succ = null;
        pred = last;
    } else {
    //對于插傳入連結表中某個位置的邏輯,走的是這裡,這個時候,succ代表的是插入的原始節點,而pred代表的是他前一個節點,新插入的元素插在二者中間
        succ = node(index);
        pred = succ.prev;
    }

//循環插入
    for (Object o : a) {
    //初始化插入節點的第一個元素
        @SuppressWarnings("unchecked") E e = (E) o;
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
        //對于一個空的連結清單,先把連結清單頭作為第一個插入的元素
            first = newNode;
        else
        //對于已經存在資料的連結清單,把新的節點放到pred的下一個節點位置
            pred.next = newNode;

		//這一步是把剛剛插入的節點指派給pred,保證pred始終代表最新插入的節點,也就是保證pred,是被插入位置的前一個節點
        pred = newNode;
    }

    if (succ == null) {
    //succ代表新插入的最後一個節點,循環結束,指派。
        last = pred;
    } else {
    //同樣循環結束,指派
        pred.next = succ;
        succ.prev = pred;
    }

    size += numNew;
    modCount++;
    return true;
}
           

上面指派部分已經把連結清單中的addAll講解完畢了,下圖是一個從網上盜的圖,便于大家了解連結清單結構,此圖是一個循環連結清單的結構,咱們介紹的是一個單向連結清單,是以看的時候把紅框圈住的部分忽略即可。

java中的資料集合--List源碼分析CollectionList再看一下LinkList和ArrayList的不同與多數部落格的錯誤認知

add方法

/**
 * Appends the specified element to the end of this list.
 *
 * <p>This method is equivalent to {@link #addLast}.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    linkLast(e);
    return true;
}
           

add方法的主要實作過程是linkLast,顧名思義,add方法插入元素的時候,是插入到最後一個位置的。

/**
 * Links e as last element.
 */
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;
    size++;
    modCount++;
}
           

在分析一個帶參的add方法

/**
 * Inserts the specified element at the specified position in this list.
 * Shifts the element currently at that position (if any) and any
 * subsequent elements to the right (adds one to their indices).
 *
 * @param index index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
           

此方法中如果插入的位置和size相同就插入到後面,如果不同,就插入到前面,意思是什麼呢,

通俗點講,如果你傳入的index小于連結清單的尺寸,那麼就把元素插入到你設定的那個index節點之前,有人會問那是不是存在index大于size的情況,答案是不存在的,因為上面還有一兒checkPositionIndex方法。linkBefore方法源碼如下。

/**
 * Inserts element e before non-null Node succ.
 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    //首先把插入目标節點的前一個節點指派給pred
    final Node<E> pred = succ.prev;
    //定義一個新的節點
    final Node<E> newNode = new Node<>(pred, e, succ);
    //把要插入的節點,指派給插入index位置的前一個節點
    succ.prev = newNode;
    if (pred == null)
    //如果連結清單為空,則指派第一個節點為新插入的節點
        first = newNode;
    else
    //如果不為空個,那麼把新的節點,把原始插入節點的前一個節點和新的節點關聯
        pred.next = newNode;
    size++;
    modCount++;
}
           

至此簡單的介紹了LinkList的源碼。

參考:(https://blog.csdn.net/qedgbmwyz/article/details/80108618)

再看一下LinkList和ArrayList的不同與多數部落格的錯誤認知

ArrayList和LinkList的根本上不同是存儲方式不同,一個是順序存儲結構,一個是鍊式存儲結構,二者實體表現是,一個存儲空間是連續的,一個是不連續的。

很多部落格上說ArrayList插入删除效率低,查詢效率高,LinkList反之,并且說出了複雜度,ArrayList的插入删除時間複雜度是O(N),查詢是O(1)

LinkList插入和删除是O(1)查詢則是O(N)

這種說法是十分不準确的,既然LinkList查詢的效率更低,那麼向某一個位置插入資料,所經曆的過程必定是先查詢到這個元素,之後在插入,那麼這個過程是不是時間複雜度已經提高了呢?有源碼為證

/**
 * Inserts the specified element at the specified position in this list.
 * Shifts the element currently at that position (if any) and any
 * subsequent elements to the right (adds one to their indices).
 *
 * @param index index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
           

上述代碼的意思是,如果插入到連結清單的末端,則直接插入即可,如果插入的不是末端,那麼調用linkBefore,這裡面的參數中有一個node(index),我們看一下node()的源碼:

/**
 * Returns the (non-null) Node at the specified element 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;
    }
}
           

源碼中根據插入的位置和連結清單的長度進行比較,大于二人之一,則從尾部查詢,小于二分之一則從頭部開始查詢,這個過程,恰好印證了之前的想法。是以直接說連結清單插入和删除效率高,是十分不準确的,如果連結清單的長度足夠大,那麼實際上連結清單的插入和删除時間複雜度并不低。

ArrayList 是線性表(數組)

get() 直接讀取第幾個下标,複雜度 O(1)

add(E) 添加元素,直接在後面添加,複雜度O(1)

add(index, E) 添加元素,在第幾個元素後面插入,後面的元素需要向後移動,複雜度O(n)

remove()删除元素,後面的元素需要逐個移動,複雜度O(n)

LinkedList 是連結清單的操作

get() 擷取第幾個元素,依次周遊,複雜度O(n)

add(E) 添加到末尾,複雜度O(1)

add(index, E) 添加第幾個元素後,需要先查找到第幾個元素,直接指針指向操作,複雜度O(n)

remove()删除元素,同樣需要先查詢後删除,複雜度O(n)