ArrayList源碼解析:在內存只有10M的空間中申請一塊5M的數組空間,會導致OOM嗎?

面試三連

面試官:使用過集合嗎?能說說都使用過哪些嗎?

小明:當然使用過,使用比較多的就是ArrayList與HashMap,還有LinkedList、HashTable、ConcurrentHashMap等等。

面試官:用的不少啊,那來說說你對ArrayList的理解吧。

小明:ArrayList是一個基於數組實現的集合,主要特點在於隨機訪問速度較快,但是插入刪除速度較慢。

面試官:那你知道爲什麼隨機訪問速度較快,插入刪除速度較慢嗎?

小明:不知道。

面試官: 現在內存還有10M內存,現在想申請一塊5M大小的ArrayList空間,程序會拋出OOM嗎?

小明:不會。

面試官:出去的時候記得把門帶上,謝謝!

小明在面試在面試的時候被問到了ArrayList,但是他只回答到了一部分,比如剛剛的那個問題:爲什麼隨機訪問速度較快,插入刪除速度較慢?小明就矇蔽了,因爲小明背面試題的時候只是記住結論,而並沒有探索爲什麼,所以再面試的時候就gg了,這也給了我們一個警告,我們在看資料的時候一定不能只看結論,否則就只能和小明一樣回家等通知了。

初識ArrayList

ArrayList就是動態數組,用MSDN中的說法,就是Array的複雜版本,它提供了動態的增加和減少元素,實現了ICollection和IList接口,靈活的設置數組的大小等好處。

也就是說,ArrayList其實就是一個數組,一般的數組長度是不允許發生改變的,但是ArrayList實現了數組的長度改變,所以叫動態數組,那你好奇他是怎麼實現的動態數組嗎?請隨我一起剝開ArrayList的神祕面紗。

我相信很多人在開發中或多或少都會使用到ArrayList,比如接收數據庫返回的列表,前端的批量保存等等,所以ArrayList在我們開發中還是比較重要的存在,所以今天我就來講一講它的源碼解析。

ArrayList源碼解析

ArrayList成員變量

/**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

DEFAULT_CAPACITY:數組初始默認大小,大小等於10
EMPTY_ELEMENTDATA:使用有參構造時,但是數組大小爲0或者數組爲空的時候使用。
DEFAULTCAPACITY_EMPTY_ELEMENTDATA :使用無參構造的默認數組值,也就是elementData
elementData:動態數組
size:數組大小

實例化ArrayList

ArrayList的實例化一共有三種方式

1.無參構造

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

注意這裏的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,是不是我們剛剛說的,無參構造的時候elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA。

使用方法

ArrayList<String> list = new ArrayList<String>();
System.out.println("集合:"+list);
集合:[]

Process finished with exit code 0

2.有參構造

有參構造分兩種情況,第一種是給定數組的初始化大小,第二種是拷貝其他集合

指定數組大小

public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

initialCapacity:初始化數組的大小,如果initialCapacity大於o,那麼就會創建一個長度爲initialCapacity的數組,等於0,就會將EMPTY_ELEMENTDATA賦值給elementData,否則怕拋出異常。

使用方法

//有參構造
        ArrayList<String> list2 = new ArrayList<String>(50);
        System.out.println("集合:" + list2);
集合:[]

Process finished with exit code 0

這裏就有疑問了,上面兩種創建方式返回的結果都是一樣的,爲什麼ArrayList還要給出一個指定大小的構造呢?肯定是有原因的,這個我們在後面講。

數組拷貝

public ArrayList(Collection<? extends E> c) {
        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 {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

我們來看看,首先講需要拷貝的集合轉成數組,然後判斷需要拷貝的數組大小是否等於0,等於0直接給一個空數組:EMPTY_ELEMENTDATA,需如果需要拷貝到餓數組大於0並且和當前的數組不是同一個對象,那麼就執行拷貝,請注意這裏的拷貝屬於淺拷貝,爲什麼這麼說呢?請看下面代碼

List<User> list3 = new ArrayList<User>();
        //初始化User對象
        User user = new User();
        user.setUserName("小明");
        user.setSex(1);
        user.setAge(18);
        list3.add(user);
        System.out.println("list3:"+list3);
        //集合拷貝
        ArrayList<User> list4 = new ArrayList<User>(list3);
        System.out.println("拷貝完之後的list4:"+list4);
        //集合拷貝完成之後修改User對象的值
        user.setAge(20);
        System.out.println("修改User對象年齡之後的lsit4:"+list4);

明白我爲什麼這麼寫嗎?因爲我剛剛說了這裏的集合拷貝指的是淺拷貝,所以我打印了還沒有背拷貝的list3、拷貝完之後的list4以及修改User對象年齡之後的lsit4,你能猜到他們對應的輸出結果嗎?自己可以在腦海中想象一下,然後請看下面輸出結果

list3:[User(userName=小明, age=18, sex=1)]
拷貝完之後的list4:[User(userName=小明, age=18, sex=1)]
修改User對象年齡之後的lsit4:[User(userName=小明, age=20, sex=1)]

Process finished with exit code 0

我們可以看到list3和拷貝完之後的list4是一模一樣的,但是修改User對象年齡之後的lsit4卻發生了改變,那就是年齡變成了20,我們並沒有對list4的對象做修改,他爲啥改變了呢?這就是java淺拷貝和深拷貝的知識了,如果對這方面不熟悉的可以參考原型模式:如何快速的克隆出一個對象?

ArrayList的構造函數基本上講的差不多了,但是這裏還是沒有引出動態數組的概念啊,他還是一個死的,那是什麼時候他會變成動態的呢?客官不要心急,我們接着往下看。

ArrayList方法

add()

ArrayList一共給我們提供了兩個add(),我們一起來看一下吧。

第一個:add(E e)

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
//每次添加的時候都需要判斷一下數組的長度還夠不夠,如果不夠就需要另外處理
 private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

//數組初始化的大小與需要插入位置的大小比較,返回大的那一個
public static int max(int a, int b) {
        return (a >= b) ? a : b;
    }

//判斷是否需要擴容,如果插入的位置已經大於數組的大小,那麼進行擴容操作
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

//擴容,將數組擴大原來的1.5倍,並且將原來的數組拷貝到新數組,再將新數組複製給原數組
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);
    }

add()的源碼大致就是這樣的,每次添加的時候都會判斷插入的位置是否大於了數組的大小,如果大於就進行擴容處理,將數組擴大原來的1.5倍( oldCapacity + (oldCapacity >> 1)),但是這裏有一點需要特別注意一下,如果擴容的大小已經超過了ArrayList指定的最大數值,那會發生什麼呢?

@Native public static final int   MAX_VALUE = 0x7fffffff;

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

如果擴容的大小已經超過了ArrayList指定的最大數值,他會先判斷插入的位置是否已經大於了ArrayList允許的最大數值,如果大於,直接返回:MAX_VALUE,否者返回MAX_ARRAY_SIZE,這裏一定要注意一個是擴容後的大小,一個是插入位置,一定不要搞錯,這裏就是ArrayList爲什麼被稱爲動態數組。

使用方法

//無參構造
        ArrayList<String> list = new ArrayList<String>();
        list.add("小明");
        list.add("賣托兒索的小火柴");
        System.out.println("list:" + list);
list:[小明, 賣托兒索的小火柴]

Process finished with exit code 0

我這裏初始化的時候創建了一個無參構造,所以數組的初始大小爲:10,添加兩個元素的時候並不會觸發擴容機制。

第二種:add(int index, E element)

public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

故名思意,看參數就應該能大致的猜出這個方法是幹什麼的,沒錯,他就是插入指定位置元素,他的插入和第一個差不多,唯一的區別就是第一個是往後添加,這裏是按index添加到這指定下表位置,然後將其他的元素往後移,也就是System.arraycopy(elementData, index, elementData, index + 1, size - index)。

 ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("賣托兒索的小火柴");
        list.add("海闊天空");
        list.add(5,"逆天而行");
        System.out.println("list:" + list);

你們可以猜到執行的結果嗎?執行結果就是報錯,爲什麼呢?源碼裏面有這麼一個方法

 private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

判斷插入的位置是否大於elementData的數組長度或者是否小於0,由於我這裏只添加了兩個元素,所以size應該是3,我們添加的下標卻是5,所以就會拋出異常

Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 5, Size: 2
	at java.util.ArrayList.rangeCheckForAdd(ArrayList.java:661)
	at java.util.ArrayList.add(ArrayList.java:473)
	at com.ymy.list.MyArrayList.main(MyArrayList.java:18)

Process finished with exit code 1

我們修改一下代碼,將下表修改成1

ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("賣托兒索的小火柴");
        list.add("海闊天空");
        list.add(1,"逆天而行");
        System.out.println("list:" + list);

這個時候我們再來看運行結果

list:[小明, 逆天而行, 賣托兒索的小火柴, 海闊天空]

Process finished with exit code 0

我們發現逆天而行被添加到了下標爲1的位置,而賣托兒索的小火柴,和海闊天空相應的往後移了一位。

trimToSize()

之前沒有說清楚size與elementData的關係,size表示的是elementData數組中已經存放了多少元素,而elementData.length表示ArrayList的初始數組大小,請不要搞混.

public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

這個方法就是判斷已經存在數組中的元素個數(size)和數組初始化的大小(elementData.length)做對比,如果小於初始化值就去掉多餘的,返回一個elementData大小等於size,實現的方式就是通過拷貝的形式。

  ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("賣托兒索的小火柴");
        list.trimToSize();
        System.out.println("list:" + list);

爲了能看到我說的,我們斷點調試走一波
在這裏插入圖片描述
在這裏插入圖片描述
我們發現走到斷點的那一行size=2,elementData.length= 10,下面就是判斷了,很明顯2<10,所以這裏會執行數據拷貝,拷貝完成之後我們在看結果
在這裏插入圖片描述
清除了多餘沒用的元素下標,但是這個方法大家在使用的時候還是慎重比較好,如果你清除完成之後又想添加數據,這個時候ArrayList就會執行擴容操作了,這是需要進行數據拷貝的,慎重哦。

ensureCapacity(int minCapacity)

源碼如下

public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

大致意思:當你初始化了一個大小爲10的初始數組之後,並添加了5條數據,這個時候你發現10可能不夠,要是數組大小在大一點就好了,ensureCapacity就是解決這個問題的,他會擴大你指定的大小,但是擴大之後數組的大小是不是你指定的大小這個是不確定的,因爲ensureExplicitCapacity(minCapacity);的源碼在上面也看到了,他會現在原來的數組大小的基礎上擴大1.5倍,然後在和你傳入的數值做對比,如果大於你傳入的,那麼使用舊數組(elementData)大小的1.5倍作爲新數組的大小,如果小於你傳入的數值,這個時候就會以你傳入的大小作爲數組(elementData)的大小,這點一定要搞清楚哦,不然的話,你會發現你明明設置了值,但是最後數組的大小卻和你設置的不一樣,就會感覺是你的代碼寫的有問題。

我們先來看一個擴容小於原數組大小1.5倍的數值:12

ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("賣托兒索的小火柴");
        list.ensureCapacity(12);
        System.out.println("list:" + list);

在這裏插入圖片描述
我們發現elementData的大小並不是我們傳入的12,而是15,要注意哦

我們再來看看擴容大於原始數組大小1.5倍的數值:20

ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("賣托兒索的小火柴");
        list.ensureCapacity(20);
        System.out.println("list:" + list);

在這裏插入圖片描述
在這裏我在貼出一下導致這兩種原因的代碼在哪裏
在這裏插入圖片描述

size()

返回當前ArrayList已經添加了多少條元素,這個不用多說,相信大家都知道。

isEmpty()

public boolean isEmpty() {
        return size == 0;
    }

判斷ArrayList是否添加了數據,但是這點需要注意一下,這裏只能判斷是否存在元素,不能判斷ArrayList是否爲空,這點需要注意,如果使用這個方法判斷空的話就報錯哦。

contains(Object o)

public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

判斷ArrayList所有元素中是否存在當前元素,如果是對象,判斷的就是引用地址了,這裏需要注意,如果我們的ArrayList的泛型是對象,那麼最好重寫一下equals和hashcode方法,舉個例子

沒有重寫equals()與 hashCode()

 ArrayList<User> list = new ArrayList<User>(10);
        //用戶插入集合的數據
        User user1 = new User();
        user1.setUserName("小明");
        user1.setSex(1);
        user1.setAge(18);
        //用於對比的數據
        User user2 = new User();
        user2.setUserName("小明");
        user2.setSex(1);
        user2.setAge(18);

        list.add(user1);
        System.out.println("是否包含user1:" + list.contains(user1));
        System.out.println("是否包含user2:" + list.contains(user2));
是否包含user1:true
是否包含user2:false

Process finished with exit code 0

看到輸出結果了吧,判斷是否包含user1結果爲:true;判斷是否包含user2的結果爲:false,那是因爲往list中添加的是user1,所以比較user1的時候他們都是同一個引用地址,所以返回true,而user2是新new出來的,他們是兩個完全不相同的對象,內存地址肯定也不相同,所以這個時候肯定就返回false,因爲user1和user2裏面存放的數據都是一樣的,有時候我們只需要判斷內容是否相等,並不需要判斷內存地址是否相等的時候需要怎麼做呢?

重寫equals()與 hashCode()
改造一下我們的User對象

package com.ymy.entity;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.Objects;

@Getter
@Setter
@ToString
public class User {

    private String userName;

    private Integer age;

    private Integer sex;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(userName, user.userName) &&
                Objects.equals(age, user.age) &&
                Objects.equals(sex, user.sex);
    }

    @Override
    public int hashCode() {

        return Objects.hash(userName, age, sex);
    }
}

測試代碼還是不變,我們在運行查看結果

是否包含user1:true
是否包含user2:true

Process finished with exit code 0

總結就是一句話,ArrayList引用對象的時候如果沒有重寫equals()與 hashCode()對比的就是內存地址,如果重寫了equals()與 hashCode(),對比的就是實實在在的數據。請拿小本本記好,這個要考。

indexOf(Object o)

查找元素所在的下標,如果查找的是對象,默認比較的是內存地址這點和contains(Object o)一樣。

源碼如下

public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

如果查找的內容爲空,那麼這個就會返回第一個元素爲空的下標,否者返回數組中第一次出現查找元素的下標。

沒有重寫equals()與 hashCode()

 ArrayList<User> list = new ArrayList<User>(10);
        //用戶插入集合的數據
        User user1 = new User();
        user1.setUserName("小明");
        user1.setSex(1);
        user1.setAge(18);
        //用於對比的數據
        User user2 = new User();
        user2.setUserName("小明");
        user2.setSex(1);
        user2.setAge(18);

        list.add(user1);
        System.out.println("是否包含user1:" + list.indexOf(user1));
        System.out.println("是否包含user2:" + list.indexOf(user2));
是否包含user1:0
是否包含user2:-1

Process finished with exit code 0

很明顯查找user1的時候是同一個內存地址,所以返回了對應的下標,而user2與user1不是同一個內存地址,所以返回了-1。

重寫equals()與 hashCode()

重寫的方法和contains()一樣,我們直接看結果即可

是否包含user1:0
是否包含user2:0

Process finished with exit code 0

所以一定要區分你需要查找的是值相同還是地址相同,不然就會導致bug哦。

lastIndexOf(Object o)

與indexOf()效果一樣,都是查找元素所在的下標,但是又有一點區別,那就是lastIndexOf()返回的是最後一次出現的下標位置。

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一樣,這裏就不做demo展示了。

clone()

克隆一個ArrayList

源碼如下

public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

使用方式

 ArrayList<User> list = new ArrayList<User>(10);
        //用戶插入集合的數據
        User user1 = new User();
        user1.setUserName("小明");
        user1.setSex(1);
        user1.setAge(18);
        list.add(user1);
        System.out.println("list1:"+list);
        ArrayList<User> list2 = (ArrayList<User>) list.clone();
        System.out.println("list2:"+list2);

運行結果

list1:[User(userName=小明, age=18, sex=1)]
list2:[User(userName=小明, age=18, sex=1)]

Process finished with exit code 0

將list拷貝到list2,但是這裏需要注意一點,這裏的拷貝屬於淺拷貝,list2和list1共享一個User對象,這是需要特別注意的。

toArray()

將ArrayList轉換成數組

源碼

public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

使用方式

ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list1:" + list);
        Object[] array = list.toArray();
        System.out.println("array:" + array);
        array[2] = "海闊天空";

運行結果

list1:[小明, 逆天而行]
array:[Ljava.lang.Object;@3a71f4dd
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 2
	at com.ymy.list.MyArrayList.main(MyArrayList.java:17)

Process finished with exit code 1

ArrayList轉數組沒有問題,但是在數組賦值的時候卻報錯了,這一點需要注意,這裏的數組長度就是ArrayList的數組實際長度,ArrayList的長度是2,下標最大爲1,但是我們賦值的時候給的下標是2,所以就會拋出數組越界的錯誤。

get(int index)

根據下表獲取元素信息

源碼

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

        return elementData(index);
    }

第一步校驗下標是否越界,然後返回對應下標元素信息。

使用方法

 ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list1:" + list);
        String name = list.get(1);
        System.out.println("name:"+name);

運行結果

Connected to the target VM, address: '127.0.0.1:62855', transport: 'socket'
list1:[小明, 逆天而行]
name:逆天而行
Disconnected from the target VM, address: '127.0.0.1:62855', transport: 'socket'

Process finished with exit code 0

這裏面的下標一定不能大於elementData的size,否者就會拋出數組越界。

set(int index, E element)

在指定下標添加元素

源碼

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

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

添加的下標不能越界,他會將你的元素添加到數組的指定下標,並且返回被替換的元素,這裏是替換哦,被替換的元素不會往後移,這點需要特別注意。

使用方法

ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list:" + list);
        String name = list.set(1,"海闊天空");
        System.out.println("name:"+name);
        System.out.println("修改後的list:"+list);

結果

list:[小明, 逆天而行]
name:逆天而行
修改後的list:[小明, 海闊天空]

Process finished with exit code 0

remove(int index)

刪除指定下標的元素,並返回被刪除的元素值

源碼

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,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

他在刪除了指定下標之後,那這個下標的位置就會處於空缺,這個時候ArrayList做了一件事,那就是將數組進行重新排序,實現的方式就是數據拷貝,使用一個新的數組接受兩段數據,一段是刪除下標之前的數據,一段是刪除下表之後的數據,整合到一個新的數組,然後賦值到原數組中。這裏只需要瞭解一下即可,最後返回了被刪除的元素值。

使用方法

ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list:" + list);
        String name = list.remove(1);
        System.out.println("name:"+name);
        System.out.println("刪除後的list:"+list);

運行結果

list:[小明, 逆天而行]
name:逆天而行
刪除後的list:[小明]

Process finished with exit code 0

remove(Object o)

通過元素值刪除數組中存在的元素,這種刪除比較耗時間,爲什麼這麼說呢?請看源碼

 public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

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; // clear to let GC do its work
    }


首先,他會判斷刪除的元素是否爲空,如果是空,那麼它將刪除數組中第一個空元素,然後直接返回,如果你要刪除的元素不爲空,那這個時候就會循環數組,找到你要刪除的第一個元素進行刪除,但是刪除的時候有需要做數據拷貝,如果不做的話,數組下標就會錯亂,最後返回刪除結果。

使用方法

 ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list:" + list);
        boolean remove = list.remove("逆天而行");
        System.out.println("是否刪除成功:"+remove);
        System.out.println("刪除後的list:"+list);

運行結果

list:[小明, 逆天而行]
是否刪除成功:true
刪除後的list:[小明]

Process finished with exit code 0

clear()

這個方法比較簡單,就是將數組中所有的元素都設置爲null,然後將size設置爲0。

源碼

public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

使用方法

 ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list:" + list);
        list.clear();
        System.out.println("刪除後的list:"+list);

運行結果

list:[小明, 逆天而行]
刪除後的list:[]

Process finished with exit code 0

addAll(Collection<? extends E> c)

將其他的集合添加到當前集合,這裏添加方式是通過拷貝實現的。

源碼如下

 public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

ensureCapacityInternal(size + numNew);這行代碼是不是經常看到,不用我多說想必大家也知道了,沒錯,就是判斷當前的集合是否可以裝下這些數據,是否需要擴容,接下來就是數據添加了,添加的方式就是通過數據拷貝,這裏的拷貝同樣屬於淺拷貝。

使用方法

 ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        System.out.println("list:" + list);
        ArrayList<String> list2 = new ArrayList<String>();
        list2.add("隨風起舞");
        lis2t.add("窮兇極惡");
        list.addAll(list2);
        System.out.println("添加之後的lsit:"+list);

運行結果

list:[小明, 逆天而行]
添加之後的lsit:[小明, 逆天而行, 隨風起舞, 窮兇極惡]

Process finished with exit code 0

addAll(int index, Collection<? extends E> c)

這個方法其實和上面那個也是大同小異,就是添加集合,但是這裏的添加方式有點區別,這裏可以指定下標。

源碼如下

 public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

它會往你指定的下標添加集合元素,原本屬於當前下標的元素向後移動,移動方式也是通過數據拷貝事項的。

使用方法

  ArrayList<String> list = new ArrayList<String>(10);
        list.add("小明");
        list.add("逆天而行");
        list.add("鐵血無雙");
        System.out.println("list:" + list);
        ArrayList<String> list2 = new ArrayList<String>();
        list2.add("隨風起舞");
        list2.add("窮兇極惡");
        list.addAll(1,list2);
        System.out.println("添加之後的lsit:"+list);

運行結果

list:[小明, 逆天而行, 鐵血無雙]
添加之後的lsit:[小明, 隨風起舞, 窮兇極惡, 逆天而行, 鐵血無雙]

Process finished with exit code 0

我們插入的下標位置爲1,這個時候ArrayList就將list2這兩個元素從下標1開始往後田間,衝突的元素就往後移,直到沒有衝突爲止。

這裏就暫時先說這麼多吧,這些都是一些比較常用的方法,看了肯定會對你有所幫助。

開篇解答

再文章開頭的時候我們說到小明面試的時候被問到在一塊內存只有10M的空間中申請一塊5M的數組空間,會導致OOM嗎?

這個答案是:不確定,爲什麼這麼說呢?原因很簡單,那是因爲數組在內存中存放的地址都是連續的,比如:00xx01、00xx02、00xx03 … 00xxnn,雖然說內存還有10M,但是不能保證連續的內存空間還剩5M,如果連續空間不足5M,那麼在創建ArrayList的時候就會拋出OOM,這個時候你就會疑問了,既然數組要求內存地址是連續的,那是什麼導致內存地址不連續呢?這個就涉及到鏈表了,鏈表存儲的數據在內存中的地址是隨機的,關於鏈表這個就不展開了,否者又得講半天,所以只需要記住:數組盛情內存空間的時候要求內存地址是連續的,如果連續的內存地址空間不足,那麼在創建數組的時候就會拋出OOM。

總結

雖然我們在日常開發中經常使用ArrayList,但是我們對他的原理熟悉嗎?如果不熟悉就因爲一個細節就會讓你的程序變慢或者內存溢出。

自動擴容:如果我們創建ArrayList的時候知道了大概的長度的時候儘量指明數組長度,否者在數據添加的時候就會頻繁出發擴容,然而擴容就會導致數據拷貝,雖然數據拷貝屬於淺拷貝,但是頻繁的數據拷貝同樣會消耗我們的性能,所以在實例化的時候最好給出數組初始長度,避免頻繁擴容。

手動擴容(ensureCapacity):手動擴容的時候需要注意一點,手動擴容的最終數組大小有可能不是你指定的大小,他有一個校驗規則,第一,將元素組長度擴大1.5倍,然後在和你傳入的擴容數值做對比,誰大用誰。

刪除、修改(元素刪除):這兩個相對來說比較耗時,爲什麼這麼說呢?原因就是刪除的時候會循環整個數組,最好情況第一次就找到了你要操作的數據,但是最壞情況是你循環了一遍數組才找到你要操作的元素,所以刪除、修改的時間複雜度爲:O(n),並且操作完成之後還伴隨這一次數據拷貝,所以刪除的時候能用下標就用下標,是在找不到下標在使用元素刪除。

查詢:隨機訪問的速度較快,那是因爲根據下標能很快的找到對應的元素,時間複雜度爲:O(1)。

線程安全性問題:ArrayList不是線程安全的,這點想必大家都知道,這裏就不再囉嗦了。

總的來說就是儘量指定數組長度,避免頻繁擴容,少使用元素刪除,所以在選型的時候一定要注意使用,雖然ArrayList簡單,但是使用不當,也會給項目造成很大損失。

ArrayList的源碼解析並沒有完全寫完,還有一些,我覺得開發中可能使用的不多,所以這裏就不打算繼續講了,大家看着也累,後續有時間的話再給補上,還請見諒。

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