ArrayList底層

1、ArrayList集合底層數據結構

1.ArrayList集合介紹

List集合的可調整大小數組實現。java

2.數組結構介紹

  • 增刪快:每次增長刪除元素,都須要更改數組長度、拷貝以及移除元素位置。
  • 查詢快:因爲數組在內存中是一塊連續空間,所以能夠根據地址+索引的方式快速獲取對應位置上的元素。

2、ArrayList繼承關係

首先咱們來看一下ArrayList的繼承關係圖,以下:面試

ArrayList底層

由上圖可知,ArrayList分別實現了RandomAccess、List、Cloneable、Serializable四個接口,那麼咱們分別來看一下他們分別的做用吧算法

2.1 Serializable標記性接口

介紹: 類的序列化由實現java.io.Serializable接口的類啓動。不實現此接口的類將不會使任何狀態序列化或反序列化。可序列化類的全部子類都是可序列化的。序列化接口沒有方法和字段,僅用於標識可串行化的語義。數組

序列化:將對象的數據寫入到文件(寫對象)安全

反序列化:將文件中對象的數據讀取出來(讀對象)數據結構

ArrayList底層

2.2Cloneable標記性接口

介紹:一個類實現Cloneable接口來指示Object.clone()方法,該方法對於該類的實列進行字段的複製是合法的。在不實現Cloneable接口的實例上調用對象的克隆方法會致使異常CloneNotSupportedException被拋出。簡言之:克隆就是依據已經有的數據,創造一份新的徹底同樣的數據拷貝多線程

ArrayList底層

克隆的前提條件併發

  • 被克隆對象所在的類必須實現Cloneable接口
  • 必須重寫clone方法

ArrayList底層ArrayList底層

2.3RandomAccess標記接口

1.介紹標記接口由list實現使用,以代表他們支持快速(一般爲恆定時間)隨機訪問。app

此接口的主要目的是容許算法更改其行爲,以便在應用與隨機訪問列表或順序訪問列表時提供良好的性能。dom

用於操縱隨機訪問列表的最佳算法能夠在應用與順序訪問列表時產生二次行爲(如LinkedList)。鼓勵通用列表算法在應用若是將其應用於順序訪問列表以前提供交差性能的算法時,檢查給定義列表是否爲instanceof,而且必要時更改其行爲以保證可接受的性能。

人們認識到,隨機訪問和順序訪問之間的區別一般是模糊的。例如,一些LIst實現提供漸進的線性訪問時間,若是它們在實踐中得到巨大可是恆定的訪問時間。這樣的一個List實現一般應該實現這個接口。根據經驗,List應實現此接口,若是對於類的經典實列,次循環;

for(int i = 0,n = list.size() ; i < n ; i++)
            list.get(i);

比這個循環運行得更快;

for(Iterator i = list.iterator;i.hasllext();)
            i.next();

2.4AbstractList抽象類

3、ArrayList源碼分析

3.1構造方法

ArrayList底層

從上圖能夠看見,ArrayList是有三個構造方法(一個無參,倆個有參)

Constructor Constructor描述
ArrayList() 構造一個初始容量爲10的空容器
ArrayList(int initialCapacity) 構造具備指定初始容量的空列表
ArrayList(Collection<? extends E> c) 構造一個包含指定集合的元素的列表,按照他們由集合的迭代器返回的順序

3.2 案例演示

案例一:

1.空參構造ArrayList()

public static void main(String[] args){
    //new一個ArrayList真的能夠構造一個初始容量爲10的空列表嗎?
    ArrayList<String> list = new ArrayList<String>();
}

那咱們就來看看源碼是怎麼走的吧!

//首先空參構造
public ArrayList() {
    //賦值
     this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    //這個時候能夠看出來在賦值
    //那麼咱們就去查看倆個屬性
}

//看完之後發現是一個空容量的數組,長度爲0
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//集合真正存儲數據的容器
transient Object[] elementData;

2.ArrayList(int initialCapacity)

public static void main(String[] args){
    //這行代碼ArrayList底層作了什麼
    ArrayList<String> list = new ArrayList<String>(5);
}

ArrayList底層

3.ArrayList(Collection<? extends E> c)

//ArrayList(Collection<? extends E> c)構造一個包含指定集合的元素列表,按照他們由集合的迭代器返回的順序
        ArrayList<String> list = new ArrayList<String>(5);
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");

        //這行代碼作了什麼
        ArrayList<String> list1 = new ArrayList<>(list);

        for (String s : list1) {
            System.out.println(s);
        }

因爲這個源碼比較複雜,因此將源碼拷貝過來而且進行解釋

public ArrayList(Collection<? extends E> c) {
        //首先將list集合轉換爲數組,使用的是父接口Collection的方法
        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 {
            // 若是長度爲0,就把空數組的地址賦值給集合存元素的數組
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

ArrayList底層

3.3add添加方法

方法名 描述
public boolean add(E e) 將指定的元素添加到此列表的尾部。
public void add(int index, E element) 將指定的元素插入此列表中的指定位置。
public boolean addAll(Collection<? extends E> c) 按照指定 collection 的迭代器所返回的元素順序,將該 collection 中的全部元素添加到此列表的尾部。
public boolean addAll(int index, Collection<? extends E> c) 從指定的位置開始,將指定 collection 中的全部元素插入到此列表中。
  • public boolean add(E e)添加單個元素
@Test
    public void test(){
        ArrayList<String> list = new ArrayList<>();
        list.add("悅悅");
    }

ArrayList底層

ArrayList底層

ArrayList底層

若是倆個數組相等,則返回容量最大的那個

ArrayList底層

ArrayList初始默認的值是10

ArrayList底層

ArrayList底層

若是minCapacity參數大於數組長度,則進行擴容

ArrayList底層

注意:這裏進行右移(>>)右移幾位至關於除以2的幾回冪;左移幾位至關於乘以2的幾回冪;
擴容的核心算法:原容量的1.5倍

進行判斷賦值給一個新的數組容量

  • public void add(int index, E element)在指定的索引上添加元素
@Test
    public void test(){
        ArrayList<String> list = new ArrayList<>();
        list.add("悅悅");
        list.add("123");
        list.add("456");
        list.add(1,"789");
        System.out.println(list);
    }

ArrayList底層

ArrayList底層

首先判斷索引是否大於集合的長度或者索引是否小於0,若是爲true則會拋出一個異常,返回;

ArrayList底層

後面的邏輯就和添加單個元素是同樣的

當ensureCapacityInternal方法走完之後,調用拷貝方法

ArrayList底層

ArrayList底層

  • public boolean addAll(Collection<? extends E> c)將集合中的全部元素一次性添加到集合中
@Test
    public void test(){
        ArrayList<String> list = new ArrayList<>();
        list.add("悅悅");
        list.add("123");
        list.add("456");
        ArrayList<String> list1 = new ArrayList<>();
        list1.addAll(list);
        System.out.println(list);
        System.out.println(list1);
    }

ArrayList底層

首先將有數據的集合轉換爲數組

再將有數據的數組長度賦值給numNew變量

再建立一個新的數組

將有數據的數組進行拷貝到新的數組

給新數組重新定義長度

ArrayList底層

  • public boolean addAll(int index, Collection<? extends E> c)將指定集合中的全部元素插入到此列表中,從指定的位置開始。
@Test
    public void test(){
        ArrayList<String> list = new ArrayList<>();
        list.add("悅悅");
        list.add("123");
        list.add("456");
        ArrayList<String> list1 = new ArrayList<>();
        list1.add("大胖子");
        list1.add("想變帥");
        list1.addAll(1,list);
        System.out.println(list);
        System.out.println(list1);
    }

ArrayList底層

首先進行校驗索引

將有參數的集合(數據源)轉換爲數組

記錄數據源的長度賦值給numNew

再給存儲數據的數組進行擴容

numMoved:表明要移動元素的個數--》移動一個;數據目的(集合list1)的長度-調用addAll的第一個參數(索引1)

再進行判斷移動的個數是否大於0;根據不一樣的結果調用不一樣的方法

重新給集合大小進行賦值

ArrayList底層

圖解詳細過程

ArrayList底層

arraycopy的時候Ox777發生變化,首先進行佔位,而後將內容拷貝進去

ArrayList底層

ArrayList底層

add元素移動位置的代碼復原

ArrayList底層

3.4轉換方法

  • public String toString()把集合全部數據轉換成字符串
@Test
    public void test(){
        ArrayList<String> list = new ArrayList<>();
        list.add("123");
        list.add("456");
        list.add("789");
        String s = list.toString();
        System.out.println(s);
    }

ArrayList底層

注意看:

1.這裏並無直接進入ArrayList裏面,而是找到了ArrayList的父類AbstractCollection的toString方法中,緣由是ArrayList裏面並無重寫toString方法,因此找到了父類的toString方法

2.這裏的循環查看,使用的是迭代器,並無使用for循環!!!!

3.5迭代器

@Test
    public void test(){
        ArrayList<String> list = new ArrayList<>();
        list.add("123");
        list.add("456");
        list.add("789");
        Iterator<String> it = list.iterator();
        while (it.hasNext()){
            System.out.println(it.next());
        }
    }

ArrayList底層

建立一個內部類的對象

ArrayList底層

注意:Object[] elementData = ArrayList.this.elementData;這裏是將ArrayList的數組重新賦值使用

迭代器中進行集合元素的刪除

ArrayList底層

寫的代碼看似沒有問題,可是卻拋出了java.util.ConcurrentModificationException的異常!

那這個異常又是什麼呢?那就是併發修改異常

那這個異常緣由又是什麼呢?

ArrayList底層

在add的時候會有一個標記,添加一個元素會自增1;

ArrayList底層

在迭代器裏面,將標記值賦給一個新的變量

ArrayList底層

在next的方法中進行判斷實際的修改次數是否是不等於預期修改的次數。可是刪除一個元素之後,預期修改值就不等了,因此拋出異常!

注意:若是要刪除的元素在倒數第二個位置的時候,不會拋出異常

ArrayList底層

爲何呢?

由於在調用hasNext方法的時候,光標的值和集合的長度同樣,那麼就會返回false,所以就不會再次調用next的方法獲取集合的元素,既然不會調用next方法,那麼底層就不會產生併發修改異常

迭代器刪除元素

ArrayList底層

咱們能夠很清晰的看到,使用迭代器的remove方法刪除元素是不會產生異常的

ArrayList底層

迭代器的remove方法刪除元素,其實底層仍是調用集合的remove方法,可是調用迭代器的remove方法,每次都會給預期的修改次數的方法進行重新賦值,所以無論怎麼修改,都不會產生併發修改異常。

4、面試題

4.1 ArrayList是如何擴容的?

看add方法

第一次擴容10

之後每次都是原容量的1.5倍

4.2ArrayList頻繁擴容致使添加性能急速降低,如何處理?

需求:在已有集合的基礎上還須要添加10w條數據

@Test
    public void test(){
        ArrayList<String> list = new ArrayList<>();
        list.add("123");
        list.add("456");
        list.add("789");

        long startTime = System.currentTimeMillis();
        for(int i = 0 ; i < 100000 ; i++ ){
            list.add(i + "");
        }
        long endTime = System.currentTimeMillis();
        System.out.println(endTime - startTime);
    }
//用時30毫秒
//分析:看起來程序沒有任何問題,可是若是從深層次挖掘存在很大問題
//第一:擴容n次
//第二:性能低

解決辦法:在new ArrayList集合的時候直接固定好容量

@Test
    public void test(){
        ArrayList<String> list1 = new ArrayList<>();
        long startTime1 = System.currentTimeMillis();
        //需求:還須要添加10w條數據
        for(int i = 0 ; i < 100000 ; i++ ){
            list1.add(i+"");
        }
        long endTime1 = System.currentTimeMillis();
        System.out.println(endTime1 - startTime1);
        System.out.println("-------------------");
        ArrayList<String> list = new ArrayList<>(100000);
        long startTime = System.currentTimeMillis();
        //需求:還須要添加10w條數據
        for(int i = 0 ; i < 100000 ; i++ ){
            list.add(i+"");
        }
        long endTime = System.currentTimeMillis();
        System.out.println(endTime - startTime);
    }
結果:
35
-------------------
10

4.3ArrayList插入或者刪除元素必定比LinkedList慢嗎?

  • 根據索引刪除

    案例:ArrayList和LinkedList對比

    //建立ArrayList集合對象
          ArrayList<String> list = new ArrayList<>();
        //添加500w個元素
          for (int i = 0; i < 5000000; i++) {
              list.add(i+"悅悅");
          }
          //獲取開始時間
          long startTime = System.currentTimeMillis();
          //根據索引刪除ArrayList集合元素
          //刪除索引50000對應的元素
          String remove = list.remove(50000);
          System.out.println(remove);
          //獲取結束時間
          long endTime = System.currentTimeMillis();
          System.out.println(endTime-startTime);
    
          LinkedList<String> strings = new LinkedList<>();
          //添加500w個元素
          for (int i = 0; i < 5000000; i++) {
              strings.add(i+"悅悅");
          }
          //獲取開始時間
          startTime = System.currentTimeMillis();
          //刪除索引50000對應的元素
          String remove1 = strings.remove(50000);
          System.out.println(remove1);
          //獲取結束時間
          endTime = System.currentTimeMillis();
          System.out.println(endTime-startTime);

    結果爲:

    50000悅悅
    3
    50000悅悅
    2

咱們能夠看到基本都差很少,有的時候會是同樣的,那咱們就一塊兒來看看LinkedList的刪除操做吧,查找緣由

ArrayList底層

ArrayList底層

ArrayList底層

這裏進行索引校驗,若是知足就返回true。

ArrayList底層

進行查找要刪除的元素,

首先進行判斷索引是否小於集合長度的一半

若是小於,那麼就第一個節點賦值給x

進行遍歷查找

返回節點

若是索引大於集合長度的一半

把最後一個節點賦值給x

從最後一位往前找

獲取前一個節點

返回找到的節點

ArrayList底層

而後進行解綁,因此說,LinkedList不必定比ArrayList刪除一個元素要快

4.4ArrayList是線程安全的嗎?

  • ArrayList不是線程安全的

首先建立一個CollectionTask類實現Runnable接口

//經過構造方法共享一個集合
    private List<String> list;

    public CollectionTask(List<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //把當前線程名字加入到集合當中
        list.add(Thread.currentThread().getName());
    }

在建立一個測試類

@Test
        public void test() throws InterruptedException {
            //建立集合
            ArrayList<String> list = new ArrayList<>();
            //建立線程任務
            CollectionTask collectionTask = new CollectionTask(list);
            //開啓50條線程
            for (int i = 0; i < 50; i++) {
                new Thread(collectionTask).start();
            }

            //確保子線程執行完畢
            Thread.sleep(3000);

            //遍歷集合
            for (int i = 0; i < list.size(); i++) {
                System.out.println(list.get(i));
            }

            System.out.println("集合長度:"+list.size());
        }

結果爲:

null
Thread-1
Thread-12
Thread-15
Thread-14
Thread-11
Thread-19
Thread-13
Thread-16
Thread-10
null
null
Thread-8
Thread-3
Thread-2
null
Thread-4
Thread-22
Thread-17
Thread-21
Thread-23
Thread-20
Thread-24
Thread-27
Thread-26
Thread-30
Thread-28
Thread-25
Thread-29
Thread-32
Thread-39
Thread-35
Thread-31
Thread-33
Thread-34
Thread-42
Thread-43
Thread-36
Thread-37
Thread-46
Thread-41
Thread-47
Thread-44
Thread-45
Thread-40
Thread-48
Thread-49
集合長度:47

上面結果能夠看出來,有的元素爲null,而且最後的長度也不對,所以能夠證實ArrayList集合是線程不安全的。

解決方法,在run方法裏面加鎖(或者使用Collections.synchronizedList()方法)

//經過構造方法共享一個集合
    private List<String> list;

    public CollectionTask(List<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        synchronized (this) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //把當前線程名字加入到集合當中
            list.add(Thread.currentThread().getName());
        }
    }

再次運行的結果爲:

Thread-0
Thread-49
Thread-48
Thread-47
Thread-46
Thread-45
Thread-44
Thread-43
Thread-42
Thread-41
Thread-40
Thread-39
Thread-38
Thread-37
Thread-36
Thread-35
Thread-33
Thread-34
Thread-32
Thread-31
Thread-30
Thread-29
Thread-28
Thread-27
Thread-26
Thread-25
Thread-24
Thread-23
Thread-22
Thread-21
Thread-20
Thread-19
Thread-17
Thread-18
Thread-16
Thread-15
Thread-14
Thread-13
Thread-12
Thread-11
Thread-10
Thread-9
Thread-8
Thread-7
Thread-6
Thread-5
Thread-4
Thread-3
Thread-2
Thread-1
集合長度:50

集合長度與預期一致,而且中間沒有null

4.5如何複製某個ArrayList到另一個ArrayLiist中去

  • 使用clone()方法
  • 使用ArrayList構造方法
  • 使用addAll方法

4.6已知成員變量集合存儲N多用戶名稱,在多線程的環境下,使用迭代器在讀取集合數據的同時如何保證還能夠正常的寫入數據到集合?

普通集合ArrayList

建立CollectionTask類

private static  ArrayList<String> list = new ArrayList<>();
    static {
        list.add("123");
        list.add("456");
        list.add("789");
    }

    @Override
    public void run() {
        for (String s : list) {
            System.out.println(s);
            //在讀取數據的同時又向集合寫入數據
            list.add("coco");
        }
    }

建立測試類

@Test
        public void test() throws InterruptedException {
            //建立線程任務
            CollectionTask collectionTask = new CollectionTask();

            //建立10條線程
            for (int i = 0; i < 10; i++) {
                new Thread(collectionTask).start();
            }
        }

運行之後發現拋出異常

ArrayList底層

解決方法:使用CopyOnWriteArrayList集合

ArrayList底層

ArrayList底層

這個時候就不存在併發異常了。

4.7ArrayList和LinkList區別?

  • ArrayList

    • 基於動態數組的數據結構
    • 對於隨機訪問的set和get,ArrayList要優於LinkedList
    • 對於隨機操做的add和remove,ArrayList不必定比LinkedList慢(ArrayList底層是由動態數組,所以並非每次add和remove的時候都須要建立新數組)
  • LinkedList
    • 基於鏈表的數據結構
    • 對於順序操做,LinkedList不必定比ArrayList慢
    • 對於隨機操做,LinkedList效率明顯較低

5、自定義ArrayList

@SuppressWarnings("all")
public class MyArrayList<E> {
    //定義數組,用於存儲集合的元素
    private Object[] elementData;
    //定義變量,用於記錄數組的個數
    private int size;
    //定義空數組,用於在建立對象的時候給elementData初始化
    private  Object[] emptyArray = {};
    //定義常量,用於記錄集合的容量
    private final int DEFAULT_CAPACITY = 10;

    //構造方法
    public MyArrayList() {
        //給elementData初始化
        elementData = emptyArray;
    }

    //定義add方法
    public boolean add(E e){
        //調用的時候判斷是否須要擴容
        grow();
        //將元素添加到集合
        elementData[size++] = e;
        return true;
    }

    //簡單擴容
    private void grow(){
        //判斷集合存儲元素的數組是否等於emptyArray
        if (elementData == emptyArray){
            //第一次擴容
            elementData = new Object[DEFAULT_CAPACITY];
        }
        //核心算法 1.5倍
        //若是size==集合存元素數組的長度,就須要擴容
        if (size == elementData.length){
            //先定義變量記錄老容量
            int oldCapacity = elementData.length;
            //核心算法 1.5倍
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            //建立一個新的數組,長度就newCapacity
            Object[] obj = new Object[newCapacity];
            //拷貝元素
            System.arraycopy(elementData,0,obj,0,elementData.length);
            //把新數組的地址賦值給elementData
            elementData = obj;
        }
    }

    //轉換方法
    public String toString(){
        //建議對集合進行判斷,若是沒有內容直接返回"[]"
        if (size == 0){
            return "[]";
        }

        //建立StringBuilder
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        //循環遍歷數組
        for (int i = 0; i < size; i++) {
            //判斷i是否等於size-1
            if (i == size-1){
                //追加元素,還須要追加]
                sb.append(elementData[i]).append("]");
            }else {
                sb.append(elementData[i]).append(", ");
            }
        }
        //把sb中的全部數據轉換爲一個字符串,且返回
        return sb.toString();
    }

    //修改方法
    public E set(int index, E element){
        //建議先對方法的參數索引進行預判
        checkIndex(index);
        //把index索引對應的元素取出來
        E value =(E) elementData[index];
        //替換元素
        elementData[index] = element;
        return value;
    }

    private void checkIndex(int index) {
        if (index >= size || index < 0){
            //製造一個異常
            throw new IndexOutOfBoundsException("索引越界了!");
        }
    }

    //刪除方法
    public E remove(int index){
        //索引檢驗
        checkIndex(index);
        //取出元素
        E value = (E) elementData[index];
        //計算出要移動元素的個數
        int numMoved = size - index - 1;
        //判斷要移動的個數是否大於0
        if (numMoved > 0){
            System.arraycopy(elementData,index+1,elementData,index,numMoved);
        }
        //把最後一個位置上的元素置爲null;
        elementData[--size] = null;
        return value;
    }

    //根據索引獲取元素
    public E get(int index){
        //索引校驗
        checkIndex(index);
        //查詢元素
        return (E) elementData[index];
    }

    //獲取集合的長度
    public int size(){
        return size;
    }
}
相關文章
相關標籤/搜索