帶你走進Java集合之ArrayList

木木匠發表於2018-09-24

帶你走進Java集合之ArrayList

一、前言

Java 集合類提供了一套設計良好的支援對一組物件進行操作的介面和類,JAVA常用的集合介面有4類,分別是:

  • Collection:代表一組物件,每一個物件都是它的子元素
  • Set:不包含重複元素的 Collection
  • List:有順序的 collection,並且可以包含重複元素
  • Map:可以把鍵(key)對映到值(value)的物件,鍵不能重複。

JAVA集合的類關係可以用圖表示如下:

帶你走進Java集合之ArrayList
類圖說明:

  • 實線邊框是實現類,比如:ArrayList,LinkedList,HashMap等。
  • 折線邊框是抽象類,比如:AbstractCollection,AbstractList,AbstractMap等。
  • 點線邊框的是介面,比如:Collection,Iterator,List等
  • 帶顏色框的是工具類,比如:Collections,Arrays。

通過類圖我們知道,所有的集合都繼承了Iterator介面,也就是說,所有的集合都具有迭代器,可以通過迭代器去迴圈,事實上,很多集合的功能都是依託於迭代器去實現的。

二、ArrayList常用方法

方法名 功能
size() 返回當前集合的元素個數
isEmpty() 判斷當前集合是否是空元素
contains(Object o) 判斷當前集合是否包含某個物件
indexOf(Object o) 獲取某個物件位於集合的索引位置
lastIndexOf(Object o) 獲取最後一個位於集合的索引位置
get(int index) 獲取指定位置的集合物件
set(int index, E element) 覆蓋集合某個位置的物件
add(E e) 新增物件進入集合
add(int index, E element) 新增物件進入集合指定位置
remove(int index) 移除索引位置的元素
remove(Object o) 移除某個元素

我們一般使用ArrayList最常用的方法無非就是新增,查詢和刪除。我們接下來從原始碼層面上分析下ArrayList是如何進行新增,查詢和刪除的。

ArrayList原始碼屬性

//預設容量長度
private static final int DEFAULT_CAPACITY = 10;
//空元素陣列
private static final Object[] EMPTY_ELEMENTDATA = {};
//預設容量的空元素陣列
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//儲存物件的陣列
transient Object[] elementData;
//集合的大小
private int size;
複製程式碼

ArrayList構造方法

//指定容量構造方法
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);
        }
    }
//預設無引數構造方法
 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

//指定集合構造方法
 public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            //官方的一個bug,c.toArray()可能不是一個object陣列,所以需要通過Arrays.copyOf建立1個Object[]陣列,這樣陣列中就可以存放任意物件了
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }    
複製程式碼

通過上面ArrayList的構造方法我們知道,ArrayList可以建立指定長度的list,也可以指定一個集合建立list,而預設的建立list是一個長度為10 的空陣列。

ArrayList的add()方法


 public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
// 確認能否裝得下size+1的物件
 private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
//計算容量
 private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //如果是預設長度,就比較預設長度和size+1,取最大值
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

 private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        //如果容量大於陣列的長度
        if (minCapacity - elementData.length > 0)
            //擴容
            grow(minCapacity);
    }
private void grow(int minCapacity) {
        //取陣列的長度
        int oldCapacity = elementData.length;
        //計算新長度,新長度=舊長度+舊長度/2
        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);
    }
複製程式碼

上面原始碼邏輯包括了,ArrayList的新增以及擴容,根據上面原始碼,我們知道,原來ArrayList的實際預設容量直到呼叫add()方法才會真正擴容到10,這裡通過new ArrayList()在記憶體分配的是一個空陣列,並沒有直接new Object[10],這樣設計是很巧妙的,可以節省很多空間。

ArrayList的add(int index, E element)方法

 public void add(int index, E element) {
    //判斷是否越界
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 重新複製陣列,把index+1位置往後的物件全部後移
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //覆蓋index位置的物件                 
        elementData[index] = element;
        size++;
    }
複製程式碼

ArrayList的指定位置新增物件方法,需要把指定位置後面的全部物件後移,所以這樣也是ArrayList相對於linkList新增耗時的地方。

ArrayList的get(int index)方法

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

        return elementData(index);
    }

複製程式碼

ArrayList的get(int index) 方法比較簡單,只有兩步,第一,檢查是否越界,第二,返回陣列索引位置的資料。

ArrayList的remove(int index)方法

  public E remove(int index) {
        rangeCheck(index);
        
        //父類的屬性,用來記錄list修改的次數,後續迭代器中會用到
        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
        //把index位置後面的元素左移
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
複製程式碼

ArrayList 的remove(int index)方法主要分為 3步,第一步,判斷下標是否越界,第二步,記錄修改次數,並左移index位置後面的元素,第三,把最後位置賦值為null,用於快速垃圾回收。

ArrayList在迴圈中使用remove方法需要注意的問題

  • for迴圈
   List<Integer> integers = new ArrayList<>(5);
        integers.add(1);
        integers.add(2);
        integers.add(3);
        integers.add(4);
        integers.add(5);

        for (int i = 0; i < integers.size(); i++) {
            integers.remove(i);
        }
        System.out.println(integers.size());


複製程式碼

這裡首先申明一個長度為5的ArrayList的集合,然後新增五個元素,最後通過迴圈遍歷刪除,理論結果輸出0,但是輸出的結果卻是2,為什麼呢?之前分析remove原始碼我們知道,ArrayList每刪除一次就會把後面的全部元素左移,以這5個元素為例,第一個正常刪除沒問題,刪除後,元素就只剩下[2,3,4,5],這個時候remove(1),還剩[2,4,5],再remove(2),剩下[2,4],後面再remove沒有元素了,所以最後size為2。

  • foreach迴圈
  List<Integer> integers = new ArrayList<>(5);
        integers.add(1);
        integers.add(2);
        integers.add(3);
        integers.add(4);
        integers.add(5);

        for (Integer integer : integers) {
            integers.remove(integer);
        }
        System.out.println(integers.size());
複製程式碼

這段程式碼只是在上面的程式碼上面把for迴圈改成了foreach迴圈,這裡理論結果也是輸出0,但是最後卻報錯了,報錯資訊:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
複製程式碼

這裡我們發現是ArrayList的迭代器方法,ArrayList$Itr說明是ArrayList的內部類Itr中checkForComodification出問題了,我檢視下原始碼,

//這是Itr內部的屬性,初始化等於ArrayList中的modCount
int expectedModCount = modCount;

final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
複製程式碼

看到這裡我們應該清楚了,我們呼叫ArrayList的remove方法,modCount的值修改了,但是迭代器中expectedModCount值沒有修改,所以就丟擲異常了。這時候肯定有人說,你這個是騙人的,我寫的foreach刪除就不會報錯!恩,對!有一種情況是不會報錯的,就是list中只有兩個元素時,比如這樣:

   List<Integer> integers = new ArrayList<>(5);
        integers.add(1);
        integers.add(2);

        for (Integer integer : integers) {
            integers.remove(integer);
        }
        System.out.println(integers.size());
    }
複製程式碼

這時候輸出結果為1,沒有報錯,為什麼呢?我們知道foreach是for迴圈的增強,內部是通過迭代器實現的,看到剛剛報錯的程式碼也證實了我們的猜想,所以,迭代器刪除,過程是這樣的,先判斷iterator.hasNext(),迭代器有沒有下一個元素,如果有就遍歷,遍歷就會呼叫iterator.next(),該原始碼如下:

 public boolean hasNext() {
            return cursor != size;
        }

  public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
複製程式碼

我們檢視原始碼發現,以上過程只有呼叫next()會進行 checkForComodification(),當我們刪除了第一個元素時候,進入迴圈判斷,hasNext這個時候為false,不會呼叫next(),所以也就不會執行checkForComodification(),所以就能輸出1。

三、總結

  • ArrayList可以指定容量例項化,也可以指定一個集合內容初始化,預設初始化長度是10(在執行add方法後才會給真正的空間),
  • ArrayList指定位置新增和刪除,都會改變該位置之後的元素位置。
  • ArrayList在迴圈中進行remove時候需要注意報錯和下標的問題,建議用迭代器刪除是最好的

推薦閱讀

Java鎖之ReentrantLock(一)

Java鎖之ReentrantLock(二)

Java鎖之ReentrantReadWriteLock

JAVA NIO程式設計入門(一)

JAVA NIO 程式設計入門(二)

JAVA NIO 程式設計入門(三)

相關文章