List集合源碼解析

java集合之List集合源碼解析

1.爲什麼會有集合的概念

​ 數組在聲明的時候就需要固定大小,集合不是可以擴容。Java集合就像一個容器,可以存儲任何類型的數據,也可以結合泛型來存儲具體的類型對象。在程序運行時,Java集合可以動態的進行擴展,隨着元素的增加而擴大。

​ List集合可以存儲有序且可重複的數據。有序指的是:集合保存了元素的插入順序,可重複表示的是:兩個元素的hashCode值是否相同。List集合存儲元素的時候並不是根據元素的hashCode值來確定位置的(Set是)。

2.ArrayList的數據結構

​ ArrayList是基於數組實現的集合。

1.屬性

​ ArrayList是基於數組實現的集合。

//默認的數組容量大小 如果沒有指定大小的話就使用默認大小
private static final int DEFAULT_CAPACITY = 10;
//當ArrayList的構造方法中顯示指出ArrayList的數組長度爲0時,類內部將EMPTY_ELEMENTDATA 這個空對象數組賦給elemetData數組。
private static final Object[] EMPTY_ELEMENTDATA = {};
//當ArrayList的構造方法中沒有顯示指出ArrayList的數組長度時,類內部使用默認缺省時對象數組爲DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 
//實際存儲數據的數組
transient Object[] elementData; 
//ArrayList實際存放元素的個數
private int size;

2.構造方法

//指定大小
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
            //如果指定大小爲0 將EMPTY_ELEMENTDATA賦值給elementData
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
//不指定大小 將DEFAULTCAPACITY_EMPTY_ELEMENTDATA賦值給elementData
 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

3.add方法

//首先判斷是否需要擴容 元素存放的位置應該是當前容量大小+1的位置 也就是說數組的最小容量應該是size+1
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
//計算容量大小 如果elementData是默認的空對象,那麼就返回默認數組容量大小和最小容量大小中的最大值否則就返回最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
//確認容量大小  如果計算出來的最小容量比當前數組的容量要大,就需要進行擴容。
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
//擴容   首先將數組的容量賦值給oldCapacity  newCapacity是oldCapacity的1.5倍。 如果newCapacity比最小容量小  就將最小容量賦值給newCapacity,如果newCapacity大於數組最大容量MAX_ARRAY_SIZE,newCapacity = hugeCapacity(minCapacity);
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);
    }
//數組存放的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//如果最小容量大於數組存放的最大容量返回Integer.MAX_VALUE 否則返回 MAX_ARRAY_SIZE;
private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

在這裏插入圖片描述

4.Integer.MAX_VALUE - 8

​ Inter.Max_Value標識的int類型的最大值2^31-1,爲什麼要減去8呢?數組在java中是一種特殊類型,數組對象中有一個額外的元數據(存放了一些數組的信息,如size、flag、lock)有的回答說這個8字節是用來存儲Inter.Max_Value的,因爲本身Inter.Max_Value需要8字節的容量。但是這麼說似乎不對,如果只用來存儲size,那麼其他元數據存儲在何處?是否也需要空間來存儲呢。

源碼中這樣寫道:

/**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */

​ 所以個人覺得,這個-8只是爲了減少出錯的機率 避免OOM

3.LinkedList的數據結構

​ 在JDK1.6及其之前LinkedList是基於雙向循環鏈表實現的。在JDK1.7及其之後LinkedList是基於雙向鏈表實現的

1 JDK1.6

​ LinkedList是通過headerEntry實現的一個雙向循環鏈表的。先初始化一個空的Entry,用來做header,然後首尾相連,形成一個循環鏈表:

private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0;

private static class Entry<E> {
       E element;  // 當前存儲元素
       Entry<E> next;  // 下一個元素節點
       Entry<E> previous;  // 上一個元素節點
 
       Entry(E element, Entry<E> next, Entry<E> previous) {
           this.element = element;
           this.next = next;
           this.previous = previous;
       }
}

//構造函數
    public LinkedList() {
        //將header節點的前一節點和後一節點都設置爲自身
        header.next = header. previous = header ;
    }
 

​ LinkedList中提供了上面兩個屬性,其中size和ArrayList中一樣用來計數,表示list的元素數量,而header則是鏈表的頭結點,Entry則是鏈表的節點對象。

增加方法

 //添加首節點,將元素加到header節點後面
public void addFirst(E e) {
       addBefore(e, header.next);
 }
//添加尾節點
 public void addLast(E e){
    addbefore(e,header);
 }

private Entry<E> addBefore(E e,Entry<E> entry){
    Entry<E> newEntry =new Entry<E>(e,entrey,entry.previous);
    newEntry.previous.next=newEntry;
    newEntry.next.previous=newEntry;
    size++;
    modCount++;
}

​ 循環鏈表中沒有null指針(但是頭節點的內容始終是null)。涉及遍歷操作時,其終止條件就不再是像非循環鏈表那樣判別p或p->next是否爲空,而是判別它們是否等於某一指定指針,如頭指針或尾指針等

1 JDK1.7

​ 在jdk1.7中創建了兩個節點對象代替了 header,first頭節點的引用,last尾節點的引用

transient  int size=0;
transient Node<E> first;
transient Node<E> last;

//初始化不做任何操作
public LinkedList() {}

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

LinkedList的構造函數沒有做任何操作 那麼first、last對象是如何初始化的?爲什麼要這樣做?

​ 因爲LinkedList底層是通過鏈表實現的,每當有元素添加進來的時候,都是通過鏈接新的節點實現的(first、last也在此時初始化),也就是說他的容量是隨着節點的增加而變化的。和ArrayList不同它底層是通過數組實現的,會默認設置一個初始容量。

add增加方法

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

public Node<E> linkLast(E e){
    //新建一個節點l將尾節點儲存起來,
    Node<E> l=last;
    //新建一個newNode節點,該節點是last節點 因此newNode.next=null, newNode.previous=last(未增加之前)
    Node<E> newNode=new Node<E>(l,e,null);
    //更新last節點的引用
    last=newNode;
    //若l爲null表示該鏈表爲空,故更新first節點的引用爲newNode節點
    if(l==null)
        first=newNode;
    else
        //若l不爲null表示該鏈表有數據 將l.next的引用更新爲newNode
        l.next=newNode;
    size++;
    modCount++;
    return newNode;
}

4.ArrayList和LinkedList比較

1.隨機訪問

​ 對於ArrayList和LinkedList兩種類型的集合的主要區別就是他們的數據結構不同,一個基於數組實現一個基於循環鏈表實現。首先比較數組和鏈表兩種數據結構的實現方式。

數組:數組是一種線性表數據結構。它用一組連續的內存空間,來存儲一組具有相同類型的數據。(數組是具體的存儲數據的方式,線性表是一種抽象概念。兩者不是從屬關係)

雙向鏈表:鏈表的一種。和單鏈表一樣,雙鏈表也是由節點組成,它的每個數據結點中都有兩個指針,分別指向直接後繼和直接前驅。

​ 這裏的連續空間指的是數組在內存中的地址是連續的,而鏈表是通過指針來指向下一個節點的地址,因此鏈表在內存中節點的地址不一定是連續的。通過了解cpu讀取數據的方式可以得出數組的效率比鏈表的高。

​ CPU有個預讀機制,當CPU在內存讀取數據的時候並不是只讀取特定某個地址空間的數據,而是會將相鄰地址的數據也一同讀取出來並保存到CPU緩存中。等下次讀取的時候會先從緩存中讀取數據,若緩存中沒有再從內存中讀取。這樣當你讀取數組中的某個值的時候很大概率會將整個數組都保存在緩存中,這樣數組的讀取效率會大大提高。

源碼實現

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

​ ArrayList實現了RandomAccess接口,該接口中沒有任何方法,通過官方的API發現這是一個標誌接口

public interface RandomAccess

Marker interface used by List implementations to indicate that they support fast (generally constant time) random access.

​ 這段話大概的意思就是說 RandomAccess 是一個標誌接口,表明實現這個這個接口的 List 集合是支持快速隨機訪問的。也就是說,實現了這個接口的集合是支持 快速隨機訪問 策略的。

​ 同時源碼中有這段話:

 * for typical instances of the class, this loop:
 * <pre>
 *     for (int i=0, n=list.size(); i &lt; n; i++)
 *         list.get(i);
 * </pre>
 * runs faster than this loop:
 * <pre>
 *     for (Iterator i=list.iterator(); i.hasNext(); )
 *         i.next();
 * </pre>

​ 表示使用for循環的方式比使用迭代器的方式更快。

2.插入和刪除

​ 對於數組來說,由於數組的數據在內存的地址是連續的,因此在數組中間插入一個數據,需要以後的數據都要移動地址。而鏈表不同只需要改變一下該位置的前一個節點的指針,將其指向插入數據的內存地址。因此對於插入和刪除操作來說:數組的效率沒有鏈表高。

時間複雜度 數組 鏈表
插入和刪除 O(n) O(1)
隨機訪問 O(1) O(n)

綜上:對於隨機訪問ArrayList優於LinkedList,插入和刪除LinkedList優於ArrayList

5.遍歷List集合

1.Iterator

​ 通過集合的繼承關係知道List集合是繼承自Collection接口,而Collection接口繼承自Iterator接口。Iterator是一個接口,它是集合的迭代器。集合可以通過Iterator去遍歷集合中的元素。

public interface Iterator<E> {
    boolean hasNext();//如果迭代器中有元素則返回true
    E next();//返回當前位置的元素並將遊標移動到下一個位置
    default void remove() {//刪除遊標左邊的元素,在執行玩next之後改操作只能執行一次
        throw new UnsupportedOperationException("remove");
    }
}

​ Collection有一個抽象方法iterator(),所有的Collection子類都實現了這個方法,返回一個Iterator對象。然後通過Iterator接口的三個方法來遍歷集合。

List<T> list=new ArrayList<T>();
Iterator it=list.iterator();
while(it.hasNext()){
    //注意:不能再while循環中多次使用next()方法,每使用一次指針就會指向下一個元素的位置,會導致while(true)但是指針確無法在移動了。
    sout(it.next())
}

2.ListIterator

​ List接口中有另外一個方法listIterator(),該方法返回一個ListIterator對象。ListIterator是一個功能更加強大的, 它繼承於Iterator接口,只能用於各種List類型的訪問。可以通過調用listIterator()方法產生一個指向List開始處的ListIterator, 還可以調用listIterator(n)方法創建一個一開始就指向列表索引爲n的元素處的ListIterator。

public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();//向前遍歷列表是否還有元素
    E next();//返回當前位置的元素並將遊標移動到下一個位置
    boolean hasPrevious();//反向遍歷列表是否還有元素
    E previous();//返回當前位置的元素並將遊標移動到上一個位置
    int nextIndex();//返回下
    int previousIndex();
    void remove();
    void set(E e);
    void add(E e);
}
public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();//向前遍歷列表是否還有元素
    E next();//返回當前位置的元素並將遊標移動到下一個位置
    boolean hasPrevious();//反向遍歷列表是否還有元素
    E previous();//返回當前位置的元素並將遊標移動到上一個位置
    int nextIndex();//返回下
    int previousIndex();
    void remove();
    void set(E e);
    void add(E e);
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章