1. 程式人生 > >精通ArrayList,關於ArrayList你想知道的一切

精通ArrayList,關於ArrayList你想知道的一切

over which 1.5 lar into intern 執行 發現 數據

精通ArrayList,關於ArrayList你想知道的一切

@(ArrayList)[數據結構|擴容|序列化]


[TOC]

前言

在做Java開發中,ArrayList是最常用的數據結構之一,我們用它來存儲一個數據列表。初始化一個ArrayList對象之後,我們可以使用它提供的諸多的方法:插入,指定位置插入,批量插入,獲取,刪除,非空判斷,存量獲取等。

雖然我們都熟練使用,但是否有過這樣的疑問:ArrayList是怎麽保存我add()進去的數據的呢?當我new 一個ArrayList對象的時候,他有多大容量?我初始化了一個容量為10的ArrayList,卻能插入11個元素,它是怎麽擴容的呢?……下面將會從ArrayList源碼來看這些問題。

ArrayList 內部結構,和常用方法實現

ArrayList是基於數組存儲。打開ArrayList源碼發現其中有個變量表明:

    /**
     * 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;

變量的註釋是說,這個數組是用來存儲ArrayList元素的,數組的長度即是ArrayList的容量。一個空ArrayList中的elementData是一個空數組,當第一次添加數據的時候,容量會擴充到DEFAULT_CAPACITY(也就是10)。

實例化方法

ArrayList有兩個實例化方法,也稱構造函數。無參實例化方法代碼如下:

    private
static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }

這個實例化方法很簡單,其實就是給存儲元素的數組elementData賦值——一個空的Object數組。

有參的實例化方法如下:

private static final Object[] EMPTY_ELEMENTDATA = {};    
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做為初始化elementData的長度,如果這個數小於0拋異常,如果等於0 結果和無參構造函數一樣。

添加元素 add()方法

添加方法有兩個,一個是普通插入,一個是指定位置插入。普通方法代碼如下:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

方法第一行是和容量相關的,下面詳細分析。這裏主要看第二行,添加元素其實就是將目標元素e放入數組elementData的size下標處。同時讓size加1。下面在看指定位置插入:

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--插入位置,element--目標元素。第一行檢查插入位置是否合理(0 < index <size),第二行統一是容量方面的。我們需要註意第三行,調用System.arraycopy方法來做“元素移位”,位移後再賦值elementData[index] = element。

假設list裏邊存儲了A,B,C,D,E5個字母,現在調用add(3,"F"),將F插入。則調用System.arraycopy位移後示意圖如下:

A B C D E
A B C D E

然後執行elementData[3]=“F”;整體過程
| 原始 | A | B | C | D | E | |
| :--: | ---- | ---- | ---- | ---- | ---- | ---- |
| 位移 | A | B | C | | D | E |
| 插入 | A | B | C | F | D | E |

get()方法

    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }
    E elementData(int index) {
        return (E) elementData[index];
    }

get方法第一行檢查index是否合法,例如你肯定不能get(-1)。然後取出elementData數組中的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;
    }

remove方法移除elementData中index處的元素,並將這個元素返回。numMoved為需要發生位移的元素的個數。然後調用位移。然後移除最後一個元素。

怎麽擴容的

上面我們看到add()方法中有個ensureCapacityInternal()方法,這個方法實際上完成了擴容操作。擴容操作分為兩部分,1、確定最小容量的值(為了插入當前元素,容量所要達到的值)

這個最小容量值就是minCapacity變量。如果當前elementData數組為空,minCapacity=10。否則minCapacity=size+1;

如果minCapacity小於10,則取10。如果minCapacity大於elementData的長度,則調用grow()方法擴容。

2、調用grow()方法,grow()方法如下:

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍
        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);
    }

這個方法裏確定elementData數組的新容量,並調用Arrays.copyOf完成擴容。確定新容量基於這三個數值:

  • 最小容量(size+1) minCapacity
  • 當前容量的1.5倍 elementData.lengt 1.5倍
  • 允許的最大容量

當調用new ArrayList();初始化的時候,elementData為空。第一次調用add()方法的時候,擴容至10。添加第11個元素的時候就需要擴容了,擴容後的值是15。10+(10>>1)=15。當添加第16個元素的時候,擴容至15+(15>>1)=22。

所以,可以粗略的理解成每次需要擴容時會擴大至原來的1.5倍,最大不超過Integer.MAX_VALUE。

序列化的問題

前面我們提到,ArrayList是基於數組的,它存儲數據就是放在其數組類型成員變量上,也就是這個:

    transient Object[] elementData; 

這個變量是用transient修飾的。transient關鍵字的作用是這麽定義的:

如果用transient聲明一個實例變量,當對象存儲時,它的值不需要維持。換句話來說就是,用transient關鍵字標記的成員變量不參與序列化過程。

莫非數組元素不參與序列化?那我辛辛苦苦add添加的數據豈不是沒了。——然而,實際的情況不是這樣。

ArrayList實現了writeObject()和readObject()方法,相當於定制了序列化和反序列化。盡管elementData變量是用transient修飾的。但是實際上elementData中的元素在序列化的時候被寫入了。方法如下:

    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 size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // 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();
        }
    }

為什麽要這麽做呢?我們前面提到,ArrayList是動態擴容的,所以,當一個arrayList具有10個容量的時候,實際上可能只存放了一個元素。即size=1,而elementData.length=10。這個時候序列化elementData幹啥呢,後面都是空值。

線程安全問題

ArrayList是線程不安全的。所以,多線程環境下容易出錯。下面例子中啟動20個線程,每個線程向共享的ArrayList中插入20個元素,最終輸出ArrayList的長度。如果ArrayList是線程安全的,那麽最終的結果應該是200.

public class Test {

    public static void main(String[] args)throws Exception {
        ArrayList<Integer> list = new ArrayList<>();
        final CyclicBarrier cb=new CyclicBarrier(20);
        final CountDownLatch latch=new CountDownLatch(20);
        for(int i=0;i<20;i++){
            Thread t1 = new Thread(()->{
                try{
                    long cur = System.nanoTime();
                    Thread.sleep(100);
                    System.out.println(cur+"準備好了");
                    cb.await();
                    for(int j=0;j<20;j++){
                        list.add(j);
                    }
                    System.out.println(cur+"執行完了");
                    latch.countDown();
                }catch (InterruptedException |BrokenBarrierException e){
                    e.printStackTrace();
                }
            });
            t1.start();
        }
        latch.await();
        System.out.println("數組大小:"+list.size());
    }
}

我隨便執行一次,得到如下結果:

33927850979931準備好了
33927850899613準備好了
33927850816171準備好了
33927850686322準備好了
33927850595294準備好了
33927850751470準備好了
33927851168680準備好了
33927851239628準備好了
33927851821046準備好了
33927851959373準備好了
33927851351182準備好了
33927851887532準備好了
33927851070067準備好了
33927852038799準備好了
33927851433286準備好了
33927852370336準備好了
33927852448424準備好了
33927852126703準備好了
33927852215500準備好了
33927852281986準備好了
33927852281986執行完了
33927850979931執行完了
33927850899613執行完了
33927850816171執行完了
33927850686322執行完了
33927850595294執行完了
33927850751470執行完了
33927851168680執行完了
33927851239628執行完了
33927851821046執行完了
33927852370336執行完了
33927851433286執行完了
33927852038799執行完了
33927851959373執行完了
33927851070067執行完了
33927851887532執行完了
33927852215500執行完了
33927851351182執行完了
33927852126703執行完了
33927852448424執行完了
數組大小:397

和明顯結果不對,再執行幾次,還會發現,報數組越界。所以,ArrayList是線程不安全的。

精通ArrayList,關於ArrayList你想知道的一切