1. 程式人生 > 實用技巧 >ArrayList 與 LinkedList

ArrayList 與 LinkedList

前言

ArrayList 與 LinkedList 估計在java面試彙總肯定會問到,一般需要掌握的點有.
比如還應該瞭解的有:

  1. 都是List,都java集合Collection介面的實現類.
  2. ArrayList基於陣列實現,長度固定,但可以擴容.
  3. ArrayList查詢快,增刪慢,因為需要移動資料
  4. LinkedList增刪快,不用移動資料
  5. LinkedList 提供了更多的方法來操作其中的資料,但是這並不意味著要優先使用LinkedList ,因為這兩種容器底層實現的資料結構截然不同。
  6. 二者執行緒多不安全.為什麼?

所以本文將試圖去原始碼上找答案.

ArrayList

ArrayList是可變長度陣列-當元素個數超過陣列的長度時,會產生一個新的陣列,將原陣列的資料複製到新陣列,再將新的元素新增到新陣列中。

方法及原始碼

  • add
    //底冊Object型別的陣列
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 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);  // 擴容
        elementData[size++] = e;  //擴容後直接index賦值
        return true;
    }

先看是否越界,越界就擴容,
擴容原始碼如下

    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); //通過拷貝放入新陣列進行擴容
    }

這裡Arrays.copyOf 底層使用System.arraycopy拷貝到新陣列(len+1)裡.

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

        return elementData(index);
    }

get直接通過index獲取值,so快.

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

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

沒啥好講的,直接覆蓋.

  • 刪除
    public E remove(int index) {
        rangeCheck(index); //檢查邊界

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index, //拷貝new陣列
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

刪除後把index+1前移.然後末尾置為null,這裡註釋交給GC不太懂. 有一次拷貝和移動陣列.

執行緒不安全

  • 為什麼會執行緒不安全
  1. 因為size++屬於非原子操作,這裡在多執行緒下可能會被其他線拿到,又沒有做擴容處理 可能會值覆蓋或者越界即ArrayIndexOutOfBoundsException.
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true; 
    }
  1. 如果兩個執行緒擦操作同一個下標資料,可能會覆蓋. 原因是a執行緒已經擴容了,b執行緒沒有get到size的變化,直接坐了覆蓋操作.
  • 如何保證執行緒安全

首先理解執行緒安全,就是保證多個執行緒下資料

  1. Collections.synchronizedList
    public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }


        public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);} //同步
        }

但是原始碼註釋有寫到,遍歷集合的時候需要手動同步

     * It is imperative that the user manually synchronize on the returned
     * list when iterating over it:
     * <pre>
     *  List list = Collections.synchronizedList(new ArrayList());
     *      ...
     *  synchronized (list) {
     *      Iterator i = list.iterator(); // Must be in synchronized block
     *      while (i.hasNext())
     *          foo(i.next());
     *  }
     * </pre>

所以為了防止忘記,建議第二種.

  1. 使用CopyOnWriteArrayList
    public boolean add(E e) {
        final ReentrantLock lock = this.lock; //你懂的
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

LinkedList

LinkedList 底層的資料結構是基於雙向迴圈連結串列的,且頭結點中不存放資料,如下:

  • add
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    
    
    
    /**
     * 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++;
    }

  • remove
    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

    /**
     * Unlinks non-null node x.
     */
    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;  // 將前一節點的next引用賦值為x的下一節點
            x.prev = null; //解除了x節點對前節點的引用
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev; // 將x的下一節點的previous賦值為x的上一節點
            x.next = null; //解除了x節點對後節點的引用
        }

        x.item = null; // 將被移除的節點的內容設為null
        size--;
        modCount++;
        return element;
    }

刪除過程基本上就是:

  1. 前節點指向後節點
  2. 當前pre為空
  3. 後節點的pre指向前節點
  4. 當前節點next為空
  5. 當前節點為空 交給gc處理
  6. size--

與ArrayList比較而言,LinkedList的刪除動作不需要“移動”很多資料,從而效率更高。

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

    Node<E> node(int index) {
        // assert isElementIndex(index);
       // 獲取index處的節點。    
        // 若index < 雙向連結串列長度的1/2,則從前先後查詢;    
        // 否則,從後向前查詢。  
        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;
        }
    }

index與長度size的一半比較,要麼從頭開始,要麼從尾部開始. 但是都需要挨個查詢,所以這裡體驗出LinkedList比ArrayList查詢效率慢了

多執行緒下

LinkedList原始碼中發現,同樣會出現非原子性操作 size++/size--問題,所以是執行緒不安全的.

  1. Collections.synchronizedList 通ArrayList同步方法
  2. ConcurrentLinkedQueue 這個原始碼下詳解

總結

儲存結構

ArrayList是基於陣列實現的;按照順序將元素儲存(從下表為0開始),刪除元素時,刪除操作完成後,需要使部分元素移位,預設的初始容量都是10.

LinkedList是基於雙向連結串列實現的(含有頭結點)。

執行緒安全性

ArrayList 與 LinkedList 因為都不是原子操作,都不能保證執行緒安全.
Collections.synchronizedList,但是迭代list需要自己手動加鎖

ps:
Vector實現執行緒安全的,即它大部分的方法都包含關鍵字synchronized,但是Vector的效率沒有ArraykList和LinkedList高。

效率

ArrayList 從指定的位置檢索一個物件,或在集合的末尾插入、刪除一個元素的時間是一樣的,時間複雜度都是O(1)

LinkedList中,在插入、刪除任何位置的元素所花費的時間都是一樣的,時間複雜度都為O(1),但是他在檢索一個元素的時間複雜度為O(n).

ps: 事實上lindlinst並不會節省空間,只是相比增刪節省時間而已.