java集合【12】——— ArrayList,LinkedList,Vector的相同點與區別是什麼?

第十六封發表於2021-03-26


要想回答這個問題,可以先把各種都講特性,然後再從底層儲存結構,執行緒安全,預設大小,擴容機制,迭代器,增刪改查效率這幾個方向入手。

特性列舉

  • ArrayList:動態陣列,使用的時候,只需要操作即可,內部已經實現擴容機制。
    • 執行緒不安全
    • 有順序,會按照新增進去的順序排好
    • 基於陣列實現,隨機訪問速度快,插入和刪除較慢一點
    • 可以插入null元素,且可以重複
  • Vector和前面說的ArrayList很是類似,這裡說的也是1.8版本,它是一個佇列,但是本質上底層也是陣列實現的。同樣繼承AbstractList,實現了List,RandomAcess,Cloneable, java.io.Serializable介面。具有以下特點:
    • 提供隨機訪問的功能:實現RandomAcess介面,這個介面主要是為List提供快速訪問的功能,也就是通過元素的索引,可以快速訪問到。
    • 可克隆:實現了Cloneable介面
    • 是一個支援新增,刪除,修改,查詢,遍歷等功能。
    • 可序列化和反序列化
    • 容量不夠,可以觸發自動擴容
    • **最大的特點是:執行緒安全的*,相當於執行緒安全的ArrayList
  • LinkedList:連結串列結構,繼承了AbstractSequentialList,實現了List,Queue,Cloneable,Serializable,既可以當成列表使用,也可以當成佇列,堆疊使用。主要特點有:
    • 執行緒不安全,不同步,如果需要同步需要使用List list = Collections.synchronizedList(new LinkedList());
    • 實現List介面,可以對它進行佇列操作
    • 實現Queue介面,可以當成堆疊或者雙向佇列使用
    • 實現Cloneable介面,可以被克隆,淺拷貝
    • 實現Serializable,可以被序列化和反序列化

底層儲存結構不同

ArrayListVector底層都是陣列結構,而LinkedList在底層是雙向連結串列結構。

執行緒安全性不同

ArrayList和LinkedList都不是執行緒安全的,但是Vector是執行緒安全的,其底層是用了大量的synchronized關鍵字,效率不是很高。

如果需要ArrayList和LinkedList是執行緒安全的,可以使用Collections類中的靜態方法synchronizedList(),獲取執行緒安全的容器。

預設的大小不同

ArrayList如果我們建立的時候不指定大小,那麼就會初始化一個預設大小為10,DEFAULT_CAPACITY就是預設大小。

private static final int DEFAULT_CAPACITY = 10;

Vector也一樣,如果我們初始化,不傳遞容量大小,什麼都不指定,預設給的容量是10:

    public Vector() {
        this(10);
    }

而LinkedList底層是連結串列結構,是不連續的儲存空間,沒有預設的大小的說法。

擴容機制

ArrayList和Vector底層都是使用陣列Object[]來儲存,當向集合中新增元素的時候,容量不夠了,會觸發擴容機制,ArrayList擴容後的容量是按照1.5倍擴容,而Vector預設是擴容2倍。兩種擴容都是申請新的陣列空間,然後呼叫陣列複製的native函式,將陣列複製過去。

Vector可以設定每次擴容的增加容量,但是ArrayList不可以。Vector有一個引數capacityIncrement,如果capacityIncrement大於0,那麼擴容後的容量,是以前的容量加上擴充套件係數,如果擴充套件係數小於等於0,那麼,就是以前的容量的兩倍。

迭代器

LinkedList原始碼中一共定義了三個迭代器:

  • Itr:實現了Iterator介面,是AbstractList.Itr的優化版本。
  • ListItr:繼承了Itr,實現了ListIterator,是AbstractList.ListItr優化版本。
  • ArrayListSpliterator:繼承於Spliterator,Java 8 新增的迭代器,基於索引,二分的,懶載入器。

VectorArrayList基本差不多,都是定義了三個迭代器:

  • Itr:實現介面Iterator,有簡單的功能:判斷是否有下一個元素,獲取下一個元素,刪除,遍歷剩下的元素
  • ListItr:繼承Itr,實現ListIterator,在Itr的基礎上有了更加豐富的功能。
  • VectorSpliterator:可以分割的迭代器,主要是為了分割以適應並行處理。和ArrayList裡面的ArrayListSpliterator類似。

LinkedList裡面定義了三種迭代器,都是以內部類的方式實現,分別是:

  • ListItr:列表的經典迭代器
  • DescendingIterator:倒序迭代器
  • LLSpliterator:可分割迭代器

增刪改查的效率

理論上ArrayListVector檢索元素,由於是陣列,時間複雜度是O(1),在集合的尾部插入或者刪除是O(1),但是其他的地方增加,刪除,都是O(n),因為涉及到了陣列元素的移動。但是LinkedList不一樣,LinkedList不管在任何位置,插入,刪除都是O(1)的時間複雜度,但是LinkedList在查詢的時候,是O(n)的複雜度,即使底層做了優化,可以從頭部/尾部開始索引(根據下標在前一半還是後面一半)。

如果插入刪除比較多,那麼建議使用LinkedList,但是它並不是執行緒安全的,如果查詢比較多,那麼建議使用ArrayList,如果需要執行緒安全,先考慮使用Collectionsapi獲取執行緒安全的容器,再考慮使用Vector

測試三種結構在頭部不斷新增元素的結果:


import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;

public class Test {
    public static void main(String[] args) {
        addArrayList();
        addLinkedList();
        addVector();
    }
    public static void addArrayList(){
        List list = new ArrayList();
        long startTime = System.nanoTime();
        for(int i=0;i<100000;i++){
            list.add(0,i);
        }
        long endTime = System.nanoTime();
        System.out.println((endTime-startTime)/1000/60);
    }

    public static void addLinkedList(){
        List list = new LinkedList();
        long startTime = System.nanoTime();
        for(int i=0;i<100000;i++){
            list.add(0,i);
        }
        long endTime = System.nanoTime();
        System.out.println((endTime-startTime)/1000/60);
    }
    public static void addVector(){
        List list = new Vector();
        long startTime = System.nanoTime();
        for(int i=0;i<100000;i++){
            list.add(0,i);
        }
        long endTime = System.nanoTime();
        System.out.println((endTime-startTime)/1000/60);
    }
}

測出來的結果,LinkedList最小,Vector費時最多,基本驗證了結果:

ArrayList:7715
LinkedList:111
Vector:8106

測試get的時間效能,往每一個裡面初始化10w個資料,然後每次get出來:


import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;

public class Test {
    public static void main(String[] args) {
        getArrayList();
        getLinkedList();
        getVector();
    }
    public static void getArrayList(){
        List list = new ArrayList();
        for(int i=0;i<100000;i++){
            list.add(0,i);
        }
        long startTime = System.nanoTime();
        for(int i=0;i<100000;i++){
            list.get(i);
        }
        long endTime = System.nanoTime();
        System.out.println((endTime-startTime)/1000/60);
    }

    public static void getLinkedList(){
        List list = new LinkedList();
        for(int i=0;i<100000;i++){
            list.add(0,i);
        }
        long startTime = System.nanoTime();
        for(int i=0;i<100000;i++){
            list.get(i);
        }
        long endTime = System.nanoTime();
        System.out.println((endTime-startTime)/1000/60);
    }
    public static void getVector(){
        List list = new Vector();
        for(int i=0;i<100000;i++){
            list.add(0,i);
        }
        long startTime = System.nanoTime();
        for(int i=0;i<100000;i++){
            list.get(i);
        }
        long endTime = System.nanoTime();
        System.out.println((endTime-startTime)/1000/60);
    }
}

測出來的時間如下,LinkedList 執行get操作確實耗時巨大,VectorArrayList在單執行緒環境其實差不多,多執行緒環境會比較明顯,這裡就不測試了:

ArrayList : 18
LinkedList : 61480
Vector : 21

測試刪除操作的程式碼如下,刪除的時候我們是不斷刪除第0個元素:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;

public class Test {
    public static void main(String[] args) {
        removeArrayList();
        removeLinkedList();
        removeVector();
    }
    public static void removeArrayList(){
        List list = new ArrayList();
        for(int i=0;i<100000;i++){
            list.add(0,i);
        }
        long startTime = System.nanoTime();
        for(int i=0;i<100000;i++){
            list.remove(0);
        }
        long endTime = System.nanoTime();
        System.out.println((endTime-startTime)/1000/60);
    }

    public static void removeLinkedList(){
        List list = new LinkedList();
        for(int i=0;i<100000;i++){
            list.add(0,i);
        }
        long startTime = System.nanoTime();
        for(int i=0;i<100000;i++){
            list.remove(0);
        }
        long endTime = System.nanoTime();
        System.out.println((endTime-startTime)/1000/60);
    }
    public static void removeVector(){
        List list = new Vector();
        for(int i=0;i<100000;i++){
            list.add(i);
        }
        long startTime = System.nanoTime();
        for(int i=0;i<100000;i++){
            list.remove(0);
        }
        long endTime = System.nanoTime();
        System.out.println((endTime-startTime)/1000/60);
    }
}

測試結果,LinkedList確實效率最高,但是VectorArrayList效率還要高。因為是單執行緒的環境,沒有觸發競爭的關係。

ArrayList: 7177
LinkedList: 34
Vector: 6713

下面來測試一下,vector多執行緒的環境,首先兩個執行緒,每個刪除5w元素:

package com.aphysia.offer;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;

public class Test {
    public static void main(String[] args) {
        removeVector();
    }

    public static void removeVector() {
        List list = new Vector();
        for (int i = 0; i < 100000; i++) {
            list.add(i);
        }
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    list.remove(0);
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    list.remove(0);
                }
            }
        });
        long startTime = System.nanoTime();
        thread1.start();
        thread2.start();
        while (!list.isEmpty()) {

        }
        long endTime = System.nanoTime();
        System.out.println((endTime - startTime) / 1000 / 60);
    }
}

測試時間為:12668

如果只使用一個執行緒,測試的時間是:8216,這也從結果說明了確實Vector在多執行緒的環境下,會競爭鎖,導致執行時間變長。

總結一下

  • ArrayList
    • 底層是陣列,擴容就是申請新的陣列空間,複製
    • 執行緒不安全
    • 預設初始化容量是10,擴容是變成之前的1.5倍
    • 查詢比較快
  • LinkedList
    • 底層是雙向連結串列,可以往前或者往後遍歷
    • 沒有擴容的說法,可以當成雙向佇列使用
    • 增刪比較快
    • 查詢做了優化,index如果在前面一半,從前面開始遍歷,index在後面一半,從後往前遍歷。
  • Vector
    • 底層是陣列,幾乎所有方法都加了Synchronize
    • 執行緒安全
    • 有個擴容增長係數,如果不設定,預設是增加原來長度的一倍,設定則增長的大小為增長係數的大小。

【刷題筆記】
Github倉庫地址:https://github.com/Damaer/codeSolution
筆記地址:https://damaer.github.io/codeSolution/

【作者簡介】
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java原始碼解析,JDBC,Mybatis,Spring,redis,分散式,劍指Offer,LeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。

2020年我寫了什麼?

開源刷題筆記

平日時間寶貴,只能使用晚上以及週末時間學習寫作,關注我,我們一起成長吧~

相關文章