1. 程式人生 > 其它 >第十二章 ArrayList&LinkedList原始碼解析

第十二章 ArrayList&LinkedList原始碼解析

一、對於ArrayList需要掌握的七點內容

  • ArrayList的建立:即構造器
  • 往ArrayList中新增物件:即add(E)方法
  • 獲取ArrayList中的單個物件:即get(int index)方法
  • 刪除ArrayList中的物件:即remove(E)方法
  • 遍歷ArrayList中的物件:即iterator,在實際中更常用的是增強型的for迴圈去做遍歷
  • 判斷物件是否存在於ArrayList中:contain(E)
  • ArrayList中物件的排序:主要取決於所採取的排序演算法(以後講)

二、原始碼分析

2.1、ArrayList的建立(常見的兩種方式)

        List<String> strList = new ArrayList<String>();
        List<String> strList2 = new ArrayList<String>(2);

ArrayList原始碼:

基本屬性:

    //物件陣列:ArrayList的底層資料結構
    private transient Object[] elementData;
    //elementData中已存放的元素的個數,注意:不是elementData的容量
    private int size;

注意:

  • transient關鍵字的作用:在採用Java預設的序列化機制的時候,被該關鍵字修飾的屬性不會被序列化。
  • ArrayList類實現了java.io.Serializable介面,即採用了Java預設的序列化機制
  • 上面的elementData屬性採用了transient來修飾,表明其不使用Java預設的序列化機制來例項化,但是該屬性是ArrayList的底層資料結構,在網路傳輸中一定需要將其序列化,之後使用的時候還需要反序列化,那不採用Java預設的序列化機制,那採用什麼呢?直接翻到原始碼的最下邊有兩個方法,發現ArrayList自己實現了序列化和反序列化的方法
    /**
         * Save the state of the <tt>ArrayList</tt> instance to a stream (that is,
         * serialize it).
         * 
         * @serialData The length of the array backing the <tt>ArrayList</tt>
         *             instance is emitted (int), followed by all of its elements
         *             (each an <tt>Object</tt>) in the proper order.
         */
        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 array length
            s.writeInt(elementData.length);
    
            // Write out all elements in the proper order.
            for (int i = 0; i < size; i++)
                s.writeObject(elementData[i]);
    
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
    
        }
    
        /**
         * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
         * deserialize it).
         */
        private void readObject(java.io.ObjectInputStream s)
                throws java.io.IOException, ClassNotFoundException {
            // Read in size, and any hidden stuff
            s.defaultReadObject();
    
            // Read in array length and allocate array
            int arrayLength = s.readInt();
            Object[] a = elementData = new Object[arrayLength];
    
            // Read in all elements in the proper order.
            for (int i = 0; i < size; i++)
                a[i] = s.readObject();
        }

構造器:

    /**
     * 建立一個容量為initialCapacity的空的(size==0)物件陣列
     */
    public ArrayList(int initialCapacity) {
        super();//即父類protected AbstractList() {}
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity:" + initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    /**
     * 預設初始化一個容量為10的物件陣列
     */
    public ArrayList() {
        this(10);//即上邊的public ArrayList(int initialCapacity){}構造器
    }

在我們執行new ArrayList<String>()時,會呼叫上邊的無參構造器,創造一個容量為10的物件陣列。

在我們執行new ArrayList<String>(2)時,會呼叫上邊的public ArrayList(int initialCapacity),創造一個容量為2的物件陣列。

注意:

  • 上邊有參構造器的super()方法是ArrayList父類AbstractList的構造方法,這個構造方法如下,是一個空構造方法:
        protected AbstractList() {
        }
  • 在實際使用中,如果我們能對所需的ArrayList的大小進行判斷,有兩個好處:
    • 節省記憶體空間(eg.我們只需要放置兩個元素到陣列,new ArrayList<String>(2))
    • 避免陣列擴容(下邊會講)引起的效率下降(eg.我們只需要放置大約37個元素到陣列,new ArrayList<String>(40))

2.2、往ArrayList中新增物件(常見的兩個方法add(E)和addAll(Collection<? extends E> c))

2.2.1、add(E)

strList2.add("hello");

ArrayList原始碼:

    /**
     * 向elementData中新增元素
     */
    public boolean add(E e) {
        ensureCapacity(size + 1);//確保物件陣列elementData有足夠的容量,可以將新加入的元素e加進去
        elementData[size++] = e;//加入新元素e,size加1
        return true;
    }
    /**
     * 確保陣列的容量足夠存放新加入的元素,若不夠,要擴容
     */
    public void ensureCapacity(int minCapacity) {
        modCount++;
        int oldCapacity = elementData.length;//獲取陣列大小(即陣列的容量)
        //當陣列滿了,又有新元素加入的時候,執行擴容邏輯
        if (minCapacity > oldCapacity) {
            Object oldData[] = elementData;
            int newCapacity = (oldCapacity * 3) / 2 + 1;//新容量為舊容量的1.5倍+1
            if (newCapacity < minCapacity)//如果擴容後的新容量還是沒有傳入的所需的最小容量大或等於(主要發生在addAll(Collection<? extends E> c)中)
                newCapacity = minCapacity;//新容量設為最小容量
            elementData = Arrays.copyOf(elementData, newCapacity);//複製新容量
        }
    }

在上述程式碼的擴容結束後,呼叫了Arrays.copyOf(elementData, newCapacity)方法,這個方法中:對於我們這裡而言,先建立了一個新的容量為newCapacity的物件陣列,然後使用System.arraycopy()方法將舊的物件陣列複製到新的物件陣列中去了。

注意:

  • modCount變數用於在遍歷集合(iterator())時,檢測是否發生了add、remove操作。

2.2.2、addAll(Collection<? extends E> c)

使用方式:

        List<String> strList = new ArrayList<String>();
        strList.add("jigang");
        strList.add("nana");
        strList.add("nana2");
        
        List<String> strList2 = new ArrayList<String>(2);
        strList2.addAll(strList);

原始碼:

    /**
     * 將c全部加入elementData
     */
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();//將c集合轉化為物件陣列a
        int numNew = a.length;//獲取a物件陣列的容量
        ensureCapacity(size + numNew);//確保物件陣列elementData有足夠的容量,可以將新加入的a物件陣列加進去
        System.arraycopy(a, 0, elementData, size, numNew);//將物件陣列a拷貝到elementData中去
        size += numNew;//重新設定elementData中已加入的元素的個數
        return numNew != 0;//若加入的是空集合則返回false
    }

注意:

  • 從上述程式碼可以看出,若加入的c是空集合,則返回false
  • ensureCapacity(size + numNew);這個方法在上邊講
  • System.arraycopy()方法定義如下:
    public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos,  int length);

    將陣列src從下標為srcPos開始拷貝,一直拷貝length個元素到dest陣列中,在dest陣列中從destPos開始加入先的srcPos陣列元素。

除了以上兩種常用的add方法外,還有如下兩種:

2.2.3、add(int index, E element)

    /**
     * 在特定位置(只能是已有元素的陣列的特定位置)index插入元素E
     */
    public void add(int index, E element) {
        //檢查index是否在已有的陣列中
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException("Index:"+index+",Size:"+size);
        ensureCapacity(size + 1);//確保物件陣列elementData有足夠的容量,可以將新加入的元素e加進去
        System.arraycopy(elementData, index, elementData, index+1, size-index);//將index及其後邊的所有的元素整塊後移,空出index位置
        elementData[index] = element;//插入元素
        size++;//已有陣列元素個數+1
    }

注意:

  • index<=size才行,並不是index<elementData.length

2.2.4、set(int index, E element)

    /**
     * 更換特定位置index上的元素為element,返回該位置上的舊值
     */
    public E set(int index, E element) {
        RangeCheck(index);//檢查索引範圍
        E oldValue = (E) elementData[index];//舊值
        elementData[index] = element;//該位置替換為新值
        return oldValue;//返回舊值
    }

2.3、獲取ArrayList中的單個物件(get(int index))

實現方式:

        ArrayList<String> strList2 = new ArrayList<String>(2);
        strList2.add("hello");
        strList2.add("nana");
        strList2.add("nana2");
        System.out.println(strList2.get(0));

原始碼:

    /**
     * 按照索引查詢物件E
     */
    public E get(int index) {
        RangeCheck(index);//檢查索引範圍
        return (E) elementData[index];//返回元素,並將Object轉型為E
    }
    /**
     * 檢查索引index是否超出size-1
     */
    private void RangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException("Index:"+index+",Size:"+size);
    }

注:這裡對index進行了索引檢查,是為了將異常內容寫的詳細一些並且將檢查的內容縮小(index<0||index>=size,注意這裡的size是已儲存元素的個數);

事實上不檢查也可以,因為對於陣列而言,如果index不滿足要求(index<0||index>=length,注意這裡的length是陣列的容量),都會直接丟擲陣列越界異常,而假設陣列的length為10,當前的size是2,你去計算array[9],這時候得出是null,這也是上邊get為什麼減小檢查範圍的原因。

2.4、刪除ArrayList中的物件

2.4.1、remove(Object o)

使用方式:

strList2.remove("hello");

原始碼:

    /**
     * 從前向後移除第一個出現的元素o
     */
    public boolean remove(Object o) {
        if (o == null) {//移除物件陣列elementData中的第一個null
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {//移除物件陣列elementData中的第一個o
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    /*
     * 刪除單個位置的元素,是ArrayList的私有方法
     */
    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; //將最後一個元素設為null,在下次gc的時候就會回收掉了
    }

 2.4.2、remove(int index)

使用方式:

strList2.remove(0);

原始碼:

    /**
     * 刪除指定索引index下的元素,返回被刪除的元素
     */
    public E remove(int index) {
        RangeCheck(index);//檢查索引範圍

        E oldValue = (E) elementData[index];//被刪除的元素
        fastRemove(index);
        return oldValue;
    }

注意:

  • remove(Object o)需要遍歷陣列,remove(int index)不需要,只需要判斷索引符合範圍即可,所以,通常:後者效率更高。

 2.5、判斷物件是否存在於ArrayList中(contains(E)

原始碼:

    /**
     * 判斷動態陣列是否包含元素o
     */
    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

    /**
     * 返回第一個出現的元素o的索引位置
     */
    public int indexOf(Object o) {
        if (o == null) {//返回第一個null的索引
            for (int i = 0; i < size; i++)
                if (elementData[i] == null)
                    return i;
        } else {//返回第一個o的索引
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;//若不包含,返回-1
    }

    /**
     * 返回最後一個出現的元素o的索引位置
     */
    public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size - 1; i >= 0; i--)
                if (elementData[i] == null)
                    return i;
        } else {
            for (int i = size - 1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

注意:

  • indexOf(Object o)返回第一個出現的元素o的索引;lastIndexOf(Object o)返回最後一個o的索引

2.6、遍歷ArrayList中的物件(iterator())

使用方式:

        List<String> strList = new ArrayList<String>();
        strList.add("jigang");
        strList.add("nana");
        strList.add("nana2");
        
        Iterator<String> it = strList.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }

原始碼:iterator()方法是在AbstractList中實現的,該方法返回AbstractList的一個內部類Itr物件

    public Iterator<E> iterator() {
        return new Itr();//返回一個內部類物件
    }

Itr:

    private class Itr implements Iterator<E> {
        
        int cursor = 0;//標記位:標記遍歷到哪一個元素
        int expectedModCount = modCount;//標記位:用於判斷是否在遍歷的過程中,是否發生了add、remove操作

        //檢測物件陣列是否還有元素
        public boolean hasNext() {
            return cursor != size();//如果cursor==size,說明已經遍歷完了,上一次遍歷的是最後一個元素
        }

        //獲取元素
        public E next() {
            checkForComodification();//檢測在遍歷的過程中,是否發生了add、remove操作
            try {
                E next = get(cursor++);
                return next;
            } catch (IndexOutOfBoundsException e) {//捕獲get(cursor++)方法的IndexOutOfBoundsException
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        //檢測在遍歷的過程中,是否發生了add、remove等操作
        final void checkForComodification() {
            if (modCount != expectedModCount)//發生了add、remove操作,這個我們可以檢視add等的原始碼,發現會出現modCount++
                throw new ConcurrentModificationException();
        }
    }

遍歷的整個流程結合"使用方式"與"Itr的註釋"來看。注:上述的Itr我去掉了一個此時用不到的方法和屬性。

三、總結

  • ArrayList基於陣列方式實現,無容量的限制(會擴容)
  • 新增元素時可能要擴容(所以最好預判一下),刪除元素時不會減少容量(若希望減少容量,trimToSize()),刪除元素時,將刪除掉的位置元素置為null,下次gc就會回收這些元素所佔的記憶體空間。
  • 執行緒不安全
  • add(int index, E element):新增元素到陣列中指定位置的時候,需要將該位置及其後邊所有的元素都整塊向後複製一位
  • get(int index):獲取指定位置上的元素時,可以通過索引直接獲取(O(1))
  • remove(Object o)需要遍歷陣列
  • remove(int index)不需要遍歷陣列,只需判斷index是否符合條件即可,效率比remove(Object o)高
  • contains(E)需要遍歷陣列

做以上總結,主要是為了與後邊的LinkedList作比較。

elementData

第三章 LinkedList原始碼解析

一、對於LinkedList需要掌握的八點內容

  • LinkedList的建立:即構造器
  • 往LinkedList中新增物件:即add(E)方法
  • 獲取LinkedList中的單個物件:即get(int index)方法
  • 修改LinkedList中的指定索引的節點的資料set(int index, E element)
  • 刪除LinkedList中的物件:即remove(E),remove(int index)方法
  • 遍歷LinkedList中的物件:即iterator,在實際中更常用的是增強型的for迴圈去做遍歷
  • 判斷物件是否存在於LinkedList中:contain(E)
  • LinkedList中物件的排序:主要取決於所採取的排序演算法(以後講)

二、原始碼分析

2.1、LinkedList的建立

實現方式:

List<String> strList0 = new LinkedList<String>();

原始碼:在讀原始碼之前,首先要知道什麼是環形雙向連結串列,參考《演算法導論(第二版)》P207

    private transient Entry<E> header = new Entry<E>(null, null, null);//底層是雙向連結串列,這時先初始化一個空的header節點
    private transient int size = 0;//連結串列中的所儲存的元素個數

    /**
     * 構造環形雙向連結串列
     */
    public LinkedList() {
        header.next = header.previous = header;//形成環形雙向連結串列
    }

Entry是LinkedList的一個內部類:

    /**
     * 連結串列節點
     */
    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;
        }
    }

執行完上述的無參構造器後:形成的空環形雙向連結串列如下:

其中,左上角為previous,右下角為next

2.2、往LinkedList中新增物件(add(E e))

實現方式:

strList0.add("hello");

原始碼:

    /**
     * 在連結串列尾部增加新節點,新節點封裝的資料為e
     */
    public boolean add(E e) {
        addBefore(e, header);//在連結串列尾部增加新節點,新節點封裝的資料為e
        return true;
    }
    /*
     * 在連結串列指定節點entry後增加新節點,新節點封裝的資料為e
     */
    private Entry<E> addBefore(E e, Entry<E> entry) {
        Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
        newEntry.previous.next = newEntry;//新節點的前一個節點的下一節點為該新節點
        newEntry.next.previous = newEntry;//新節點的下一個節點的前一節點為該新節點
        size++;            //連結串列中元素個數+1
        modCount++;        //與ArrayList相同,用於在遍歷時檢視是否發生了add和remove操作
        return newEntry;
    }

在新增一個元素後的新環形雙向連結串列如下:

在上述的基礎上,再呼叫一次add(E)後,新的環形雙向連結串列如下:

這裡,結合著程式碼註釋與圖片去看add(E)的原始碼就好。

注意:在新增元素方面LinkedList不需要考慮陣列擴容和陣列複製,只需要新建一個物件,但是需要修改前後兩個物件的屬性。

2.3、獲取LinkedList中的單個物件(get(int index))

 實現方式:

strList.get(0);//注意:下標從0開始

原始碼:

    /**
     * 返回索引值為index節點的資料,index從0開始計算
     */
    public E get(int index) {
        return entry(index).element;
    }
    /**
     * 獲取指定index索引位置的節點(需要遍歷連結串列)
     */
    private Entry<E> entry(int index) {
        //index:0~size-1
        if (index < 0 || index >= size)
            throw new IndexOutOfBoundsException("Index:"+index+", Size:"+size);
        Entry<E> e = header;//頭節點:既作為頭節點也作為尾節點
        if (index < (size >> 1)) {//index<size/2,則說明index在前半個連結串列中,從前往後找
            for (int i = 0; i <= index; i++)
                e = e.next;
        } else {//index>=size/2,則說明index在後半個連結串列中,從後往前找
            for (int i = size; i > index; i--)
                e = e.previous;
        }
        return e;
    }

注意:

  • 連結串列節點的按索引查詢,需要遍歷連結串列;而陣列不需要。
  • header節點既是頭節點也是尾節點
  • 雙向連結串列的查詢,先去判斷索引值index是否小於size/2,若小於,從header節點開始,從前往後找;若大於等於,從header節點開始,從後往前找
  • size>>1,右移一位等於除以2;左移一位等於乘以2

2.4、修改LinkedList中指定索引的節點的資料:set(int index, E element)

使用方式:

strList.set(0, "world");

原始碼:

    /**
     * 修改指定索引位置index上的節點的資料為element
     */
    public E set(int index, E element) {
        Entry<E> e = entry(index);//查詢index位置的節點
        E oldVal = e.element;//獲取該節點的舊值
        e.element = element;//將新值賦給該節點的element屬性
        return oldVal;//返回舊值
    }

注意:entry(int index)檢視上邊

2.5、刪除LinkedList中的物件

2.5.1、remove(Object o)

使用方式:

strList.remove("world")

原始碼:

    /**
     * 刪除第一個出現的指定元資料為o的節點
     */
    public boolean remove(Object o) {
        if (o == null) {//從前往後刪除第一個null
            //遍歷連結串列
            for (Entry<E> e = header.next; e != header; e = e.next) {
                if (e.element == null) {
                    remove(e);
                    return true;
                }
            }
        } else {
            for (Entry<E> e = header.next; e != header; e = e.next) {
                if (o.equals(e.element)) {
                    remove(e);
                    return true;
                }
            }
        }
        return false;
    }
    /*
     * 刪除節點e
     */
    private E remove(Entry<E> e) {
        //header節點不可刪除
        if (e == header)
            throw new NoSuchElementException();

        E result = e.element;
        //調整要刪除節點的前後節點的指標指向
        e.previous.next = e.next;
        e.next.previous = e.previous;
        //將要刪除元素的三個屬性置空
        e.next = e.previous = null;
        e.element = null;
        
        size--;//size-1
        modCount++;
        return result;
    }

注意:

  • header節點不可刪除

 2.5.2、remove(int index)

使用方式:

strList.remove(0);

原始碼:

    /**
     * 刪除指定索引的節點
     */
    public E remove(int index) {
        return remove(entry(index));
    }

注意:

  • remove(entry(index))見上邊
  • remove(Object o)需要遍歷連結串列,remove(int index)也需要

 2.6、判斷物件是否存在於LinkedList中(contains(E)

原始碼:

    /**
     * 連結串列中是否包含指定資料o的節點
     */
    public boolean contains(Object o) {
        return indexOf(o) != -1;
    }
    /**
     * 從header開始,查詢第一個出現o的索引
     */
    public int indexOf(Object o) {
        int index = 0;
        if (o == null) {//從header開始,查詢第一個出現null的索引
            for (Entry e = header.next; e != header; e = e.next) {
                if (e.element == null)
                    return index;
                index++;
            }
        } else {
            for (Entry e = header.next; e != header; e = e.next) {
                if (o.equals(e.element))
                    return index;
                index++;
            }
        }
        return -1;
    }

注意:

  • indexOf(Object o)返回第一個出現的元素o的索引

2.7、遍歷LinkedList中的物件(iterator())

使用方式:

        List<String> strList = new LinkedList<String>();
        strList.add("jigang");
        strList.add("nana");
        strList.add("nana2");
        
        Iterator<String> it = strList.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }

原始碼:iterator()方法是在父類AbstractSequentialList中實現的,

    public Iterator<E> iterator() {
        return listIterator();
    }

listIterator()方法是在父類AbstractList中實現的,

    public ListIterator<E> listIterator() {
        return listIterator(0);
    }

listIterator(int index)方法是在父類AbstractList中實現的,

    public ListIterator<E> listIterator(final int index) {
        if (index < 0 || index > size())
            throw new IndexOutOfBoundsException("Index: " + index);

        return new ListItr(index);
    }

該方法返回AbstractList的一個內部類ListItr物件

ListItr:

    private class ListItr extends Itr implements ListIterator<E> {
        ListItr(int index) {
            cursor = index;
        }

上邊這個類並不完整,它繼承了內部類Itr,還擴充套件了一些其他方法(eg.向前查詢方法hasPrevious()等),至於hasNext()/next()等方法還是來自於Itr的。

Itr:

   private class Itr implements Iterator<E> {
        
        int cursor = 0;//標記位:標記遍歷到哪一個元素
        int expectedModCount = modCount;//標記位:用於判斷是否在遍歷的過程中,是否發生了add、remove操作

        //檢測物件陣列是否還有元素
        public boolean hasNext() {
            return cursor != size();//如果cursor==size,說明已經遍歷完了,上一次遍歷的是最後一個元素
        }

        //獲取元素
        public E next() {
            checkForComodification();//檢測在遍歷的過程中,是否發生了add、remove操作
            try {
                E next = get(cursor++);
                return next;
            } catch (IndexOutOfBoundsException e) {//捕獲get(cursor++)方法的IndexOutOfBoundsException
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        //檢測在遍歷的過程中,是否發生了add、remove等操作
        final void checkForComodification() {
            if (modCount != expectedModCount)//發生了add、remove操作,這個我們可以檢視add等的原始碼,發現會出現modCount++
                throw new ConcurrentModificationException();
        }
    }

注:

  • 上述的Itr我去掉了一個此時用不到的方法和屬性。
  • 這裡的get(int index)方法參照2.3所示。

三、總結

  • LinkedList基於環形雙向連結串列方式實現,無容量的限制
  • 新增元素時不用擴容(直接建立新節點,調整插入節點的前後節點的指標屬性的指向即可)
  • 執行緒不安全
  • get(int index):需要遍歷連結串列
  • remove(Object o)需要遍歷連結串列
  • remove(int index)需要遍歷連結串列
  • contains(E)需要遍歷連結串列

一、對於LinkedList需要掌握的八點內容

  • LinkedList的建立:即構造器
  • 往LinkedList中新增物件:即add(E)方法
  • 獲取LinkedList中的單個物件:即get(int index)方法
  • 修改LinkedList中的指定索引的節點的資料set(int index, E element)
  • 刪除LinkedList中的物件:即remove(E),remove(int index)方法
  • 遍歷LinkedList中的物件:即iterator,在實際中更常用的是增強型的for迴圈去做遍歷
  • 判斷物件是否存在於LinkedList中:contain(E)
  • LinkedList中物件的排序:主要取決於所採取的排序演算法(以後講)

二、原始碼分析

2.1、LinkedList的建立

實現方式:

List<String> strList0 = new LinkedList<String>();

原始碼:在讀原始碼之前,首先要知道什麼是環形雙向連結串列,參考《演算法導論(第二版)》P207

    private transient Entry<E> header = new Entry<E>(null, null, null);//底層是雙向連結串列,這時先初始化一個空的header節點
    private transient int size = 0;//連結串列中的所儲存的元素個數

    /**
     * 構造環形雙向連結串列
     */
    public LinkedList() {
        header.next = header.previous = header;//形成環形雙向連結串列
    }

Entry是LinkedList的一個內部類:

    /**
     * 連結串列節點
     */
    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;
        }
    }

執行完上述的無參構造器後:形成的空環形雙向連結串列如下:

其中,左上角為previous,右下角為next

2.2、往LinkedList中新增物件(add(E e))

實現方式:

strList0.add("hello");

原始碼:

    /**
     * 在連結串列尾部增加新節點,新節點封裝的資料為e
     */
    public boolean add(E e) {
        addBefore(e, header);//在連結串列尾部增加新節點,新節點封裝的資料為e
        return true;
    }
    /*
     * 在連結串列指定節點entry後增加新節點,新節點封裝的資料為e
     */
    private Entry<E> addBefore(E e, Entry<E> entry) {
        Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
        newEntry.previous.next = newEntry;//新節點的前一個節點的下一節點為該新節點
        newEntry.next.previous = newEntry;//新節點的下一個節點的前一節點為該新節點
        size++;            //連結串列中元素個數+1
        modCount++;        //與ArrayList相同,用於在遍歷時檢視是否發生了add和remove操作
        return newEntry;
    }

在新增一個元素後的新環形雙向連結串列如下:

在上述的基礎上,再呼叫一次add(E)後,新的環形雙向連結串列如下:

這裡,結合著程式碼註釋與圖片去看add(E)的原始碼就好。

注意:在新增元素方面LinkedList不需要考慮陣列擴容和陣列複製,只需要新建一個物件,但是需要修改前後兩個物件的屬性。

2.3、獲取LinkedList中的單個物件(get(int index))

 實現方式:

strList.get(0);//注意:下標從0開始

原始碼:

    /**
     * 返回索引值為index節點的資料,index從0開始計算
     */
    public E get(int index) {
        return entry(index).element;
    }
    /**
     * 獲取指定index索引位置的節點(需要遍歷連結串列)
     */
    private Entry<E> entry(int index) {
        //index:0~size-1
        if (index < 0 || index >= size)
            throw new IndexOutOfBoundsException("Index:"+index+", Size:"+size);
        Entry<E> e = header;//頭節點:既作為頭節點也作為尾節點
        if (index < (size >> 1)) {//index<size/2,則說明index在前半個連結串列中,從前往後找
            for (int i = 0; i <= index; i++)
                e = e.next;
        } else {//index>=size/2,則說明index在後半個連結串列中,從後往前找
            for (int i = size; i > index; i--)
                e = e.previous;
        }
        return e;
    }

注意:

  • 連結串列節點的按索引查詢,需要遍歷連結串列;而陣列不需要。
  • header節點既是頭節點也是尾節點
  • 雙向連結串列的查詢,先去判斷索引值index是否小於size/2,若小於,從header節點開始,從前往後找;若大於等於,從header節點開始,從後往前找
  • size>>1,右移一位等於除以2;左移一位等於乘以2

2.4、修改LinkedList中指定索引的節點的資料:set(int index, E element)

使用方式:

strList.set(0, "world");

原始碼:

    /**
     * 修改指定索引位置index上的節點的資料為element
     */
    public E set(int index, E element) {
        Entry<E> e = entry(index);//查詢index位置的節點
        E oldVal = e.element;//獲取該節點的舊值
        e.element = element;//將新值賦給該節點的element屬性
        return oldVal;//返回舊值
    }

注意:entry(int index)檢視上邊

2.5、刪除LinkedList中的物件

2.5.1、remove(Object o)

使用方式:

strList.remove("world")

原始碼:

    /**
     * 刪除第一個出現的指定元資料為o的節點
     */
    public boolean remove(Object o) {
        if (o == null) {//從前往後刪除第一個null
            //遍歷連結串列
            for (Entry<E> e = header.next; e != header; e = e.next) {
                if (e.element == null) {
                    remove(e);
                    return true;
                }
            }
        } else {
            for (Entry<E> e = header.next; e != header; e = e.next) {
                if (o.equals(e.element)) {
                    remove(e);
                    return true;
                }
            }
        }
        return false;
    }
    /*
     * 刪除節點e
     */
    private E remove(Entry<E> e) {
        //header節點不可刪除
        if (e == header)
            throw new NoSuchElementException();

        E result = e.element;
        //調整要刪除節點的前後節點的指標指向
        e.previous.next = e.next;
        e.next.previous = e.previous;
        //將要刪除元素的三個屬性置空
        e.next = e.previous = null;
        e.element = null;
        
        size--;//size-1
        modCount++;
        return result;
    }

注意:

  • header節點不可刪除

 2.5.2、remove(int index)

使用方式:

strList.remove(0);

原始碼:

    /**
     * 刪除指定索引的節點
     */
    public E remove(int index) {
        return remove(entry(index));
    }

注意:

  • remove(entry(index))見上邊
  • remove(Object o)需要遍歷連結串列,remove(int index)也需要

 2.6、判斷物件是否存在於LinkedList中(contains(E)

原始碼:

    /**
     * 連結串列中是否包含指定資料o的節點
     */
    public boolean contains(Object o) {
        return indexOf(o) != -1;
    }
    /**
     * 從header開始,查詢第一個出現o的索引
     */
    public int indexOf(Object o) {
        int index = 0;
        if (o == null) {//從header開始,查詢第一個出現null的索引
            for (Entry e = header.next; e != header; e = e.next) {
                if (e.element == null)
                    return index;
                index++;
            }
        } else {
            for (Entry e = header.next; e != header; e = e.next) {
                if (o.equals(e.element))
                    return index;
                index++;
            }
        }
        return -1;
    }

注意:

  • indexOf(Object o)返回第一個出現的元素o的索引

2.7、遍歷LinkedList中的物件(iterator())

使用方式:

        List<String> strList = new LinkedList<String>();
        strList.add("jigang");
        strList.add("nana");
        strList.add("nana2");
        
        Iterator<String> it = strList.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }

原始碼:iterator()方法是在父類AbstractSequentialList中實現的,

    public Iterator<E> iterator() {
        return listIterator();
    }

listIterator()方法是在父類AbstractList中實現的,

    public ListIterator<E> listIterator() {
        return listIterator(0);
    }

listIterator(int index)方法是在父類AbstractList中實現的,

    public ListIterator<E> listIterator(final int index) {
        if (index < 0 || index > size())
            throw new IndexOutOfBoundsException("Index: " + index);

        return new ListItr(index);
    }

該方法返回AbstractList的一個內部類ListItr物件

ListItr:

    private class ListItr extends Itr implements ListIterator<E> {
        ListItr(int index) {
            cursor = index;
        }

上邊這個類並不完整,它繼承了內部類Itr,還擴充套件了一些其他方法(eg.向前查詢方法hasPrevious()等),至於hasNext()/next()等方法還是來自於Itr的。

Itr:

   private class Itr implements Iterator<E> {
        
        int cursor = 0;//標記位:標記遍歷到哪一個元素
        int expectedModCount = modCount;//標記位:用於判斷是否在遍歷的過程中,是否發生了add、remove操作

        //檢測物件陣列是否還有元素
        public boolean hasNext() {
            return cursor != size();//如果cursor==size,說明已經遍歷完了,上一次遍歷的是最後一個元素
        }

        //獲取元素
        public E next() {
            checkForComodification();//檢測在遍歷的過程中,是否發生了add、remove操作
            try {
                E next = get(cursor++);
                return next;
            } catch (IndexOutOfBoundsException e) {//捕獲get(cursor++)方法的IndexOutOfBoundsException
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        //檢測在遍歷的過程中,是否發生了add、remove等操作
        final void checkForComodification() {
            if (modCount != expectedModCount)//發生了add、remove操作,這個我們可以檢視add等的原始碼,發現會出現modCount++
                throw new ConcurrentModificationException();
        }
    }

注:

  • 上述的Itr我去掉了一個此時用不到的方法和屬性。
  • 這裡的get(int index)方法參照2.3所示。

三、總結

  • LinkedList基於環形雙向連結串列方式實現,無容量的限制
  • 新增元素時不用擴容(直接建立新節點,調整插入節點的前後節點的指標屬性的指向即可)
  • 執行緒不安全
  • get(int index):需要遍歷連結串列
  • remove(Object o)需要遍歷連結串列
  • remove(int index)需要遍歷連結串列
  • contains(E)需要遍歷連結串列

四種List實現類的對比總結

1、ArrayList

  • 非執行緒安全
  • 基於物件陣列
  • get(int index)不需要遍歷陣列,速度快;
  • iterator()方法中呼叫了get(int index),所以速度也快
  • set(int index, E e)不需要遍歷陣列,速度快
  • add方法需要考慮擴容與陣列複製問題,速度慢
  • remove(Object o)需要遍歷陣列,並複製陣列元素,速度慢
  • remove(int index)不需要遍歷陣列,需要複製陣列元素,但不常用
  • contain(E)需要遍歷陣列

2、LinkedList

  • 非執行緒安全
  • 基於環形雙向連結串列
  • get(int index)需要遍歷連結串列,速度慢;
  • iterator()方法中呼叫了get(int index),所以速度也慢
  • set(int index, E e)方法中呼叫了get(int index),所以速度也慢
  • add方法不需要考慮擴容與陣列複製問題,只需建立新物件,再將新物件的前後節點的指標指向重新分配一下就好,速度快
  • remove(Object o)需要遍歷連結串列,但不需要複製元素,只需將所要刪除的物件的前後節點的指標指向重新分配一下以及將所要刪除的物件的三個屬性置空即可,速度快
  • remove(int index)需要遍歷連結串列,但不需要複製元素,只需將所要刪除的物件的前後節點的指標指向重新分配一下以及將所要刪除的物件的三個屬性置空即可,但不常用
  • contain(E)需要遍歷連結串列

3、Vector(執行緒安全的ArrayList)

  • 執行緒安全
  • 擴容機制與ArrayList不同

4、Stack(繼承於Vector)

  • 執行緒安全
  • 效率低下,可採用雙端佇列Deque或LinkedList來實現,Deque用的較多

總結:

  • 在查詢(get)、遍歷(iterator)、修改(set)使用的比較多的情況下,用ArrayList
  • 在增加(add)、刪除(remove)使用比較多的情況下,用LinkedList
  • 在需要執行緒安全而且對效率要求比較低的情況下,使用Vector,當然,實現ArrayList執行緒安全的方法也有很多,以後再說
  • 在需要使用棧結構的情況下,使用Deque,Stack廢棄就行了

1、ArrayList

  • 非執行緒安全
  • 基於物件陣列
  • get(int index)不需要遍歷陣列,速度快;
  • iterator()方法中呼叫了get(int index),所以速度也快
  • set(int index, E e)不需要遍歷陣列,速度快
  • add方法需要考慮擴容與陣列複製問題,速度慢
  • remove(Object o)需要遍歷陣列,並複製陣列元素,速度慢
  • remove(int index)不需要遍歷陣列,需要複製陣列元素,但不常用
  • contain(E)需要遍歷陣列

2、LinkedList

  • 非執行緒安全
  • 基於環形雙向連結串列
  • get(int index)需要遍歷連結串列,速度慢;
  • iterator()方法中呼叫了get(int index),所以速度也慢
  • set(int index, E e)方法中呼叫了get(int index),所以速度也慢
  • add方法不需要考慮擴容與陣列複製問題,只需建立新物件,再將新物件的前後節點的指標指向重新分配一下就好,速度快
  • remove(Object o)需要遍歷連結串列,但不需要複製元素,只需將所要刪除的物件的前後節點的指標指向重新分配一下以及將所要刪除的物件的三個屬性置空即可,速度快
  • remove(int index)需要遍歷連結串列,但不需要複製元素,只需將所要刪除的物件的前後節點的指標指向重新分配一下以及將所要刪除的物件的三個屬性置空即可,但不常用
  • contain(E)需要遍歷連結串列

3、Vector(執行緒安全的ArrayList)

  • 執行緒安全
  • 擴容機制與ArrayList不同

4、Stack(繼承於Vector)

  • 執行緒安全
  • 效率低下,可採用雙端佇列Deque或LinkedList來實現,Deque用的較多

總結:

  • 在查詢(get)、遍歷(iterator)、修改(set)使用的比較多的情況下,用ArrayList
  • 在增加(add)、刪除(remove)使用比較多的情況下,用LinkedList
  • 在需要執行緒安全而且對效率要求比較低的情況下,使用Vector,當然,實現ArrayList執行緒安全的方法也有很多,以後再說
  • 在需要使用棧結構的情況下,使用Deque,Stack廢棄就行了