JAVA集合之ArrayList

1、前言

Java 集合類提供了一套設計良好的支持對一組對象進行操做的接口和類,JAVA經常使用的集合接口有4類,分別是:java

  • Collection:表明一組對象,每個對象都是它的子元素
  • Set:不包含重複元素的 Collection
  • List:有順序的 collection,而且能夠包含重複元素
  • Map:能夠把鍵(key)映射到值(value)的對象,鍵不能重複。

JAVA集合的類關係能夠用圖表示以下:編程

類圖說明:數組

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

經過類圖咱們知道,全部的集合都繼承了Iterator接口,也就是說,全部的集合都具備迭代器,能夠經過迭代器去循環,事實上,不少集合的功能都是依託於迭代器去實現的。工具

2、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是如何進行添加,查詢和刪除的。post

ArrayList源碼屬性this

//默認容量長度
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 的空數組。code

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],這樣設計是很巧妙的,能夠節省不少空間。blog

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。

3、總結

  • ArrayList能夠指定容量實例化,也能夠指定一個集合內容初始化,默認初始化長度是10(在執行add方法後纔會給真正的空間),
  • ArrayList指定位置添加和刪除,都會改變該位置以後的元素位置。
  • ArrayList在循環中進行remove時候須要注意報錯和下標的問題,建議用迭代器刪除是最好的

推薦閱讀

Java鎖之ReentrantLock(一)

Java鎖之ReentrantLock(二)

Java鎖之ReentrantReadWriteLock

JAVA NIO編程入門(一)

JAVA NIO 編程入門(二)

JAVA NIO 編程入門(三)

相關文章
相關標籤/搜索