Java基礎系列:瞭解ArrayList

俗世遊子 發表於 2020-10-18

來,進來的小夥伴們,我們認識一下。

我是俗世遊子,在外流浪多年的Java程式猿

認識陣列

Java中,存在兩種儲存資料的容器:

  • 陣列
  • 集合

我們首先來了解下陣列

陣列

認識陣列

首先,我們要明白:陣列是相同型別資料的有序集合

我猜一定有人會說,Object的陣列可以存字串,數字等等,你說的不對

Object: 在我面前,你們都是弟弟

其中,我們將儲存在陣列中的資料稱之為:元素

元素在陣列中儲存的位置稱之為下標

我們可以通過下標來得到所對應的元素,反過來也一樣

記憶體空間

我們都知道,宣告物件就是在記憶體中開闢一塊空間,而宣告一個陣列就是在記憶體中開闢一串連續的空間:

陣列結構

如何使用陣列

宣告陣列

Java中,任何的資料型別都可以定義物件,比如:

String[] arrs = new String[8];
arrs[0] = "元素1";
System.out.println(arrs[0]);	// 元素1

int[] intArrs = {1,2,3,4,5,6,7};
System.out.println(intArrs[0]);	// 元素1

記憶體結構如上圖所示

簡單一點說,使用陣列可以分為3步:

  • 宣告一個陣列,然後進行分配記憶體空間
  • 根據下標賦值
  • 通過下標處理元素

我們來看下下面這段程式碼:

String[] arrs = new String[8];
arrs[0] = "元素1";
arrs[8] = "元素8";
System.out.println(arrs[0]);
 /**
  * Java.lang.ArrayIndexOutOfBoundsException: 8
  */

需要注意的一點是:在任何的程式語言中,如果需要通過下標來得到指定元素,那麼這個下標的值一定是從0開始的,且可取值的最大範圍為:指定長度 - 1

上面程式碼中:我們最大可以操作的範圍為 7

而且,陣列在宣告的時候有分配記憶體空間的操作,那麼如果我們指定下標超過了陣列指定的長度,那麼我們的程式就會丟擲異常:

  • ArrayIndexOutOfBoundsException:陣列越界問題

陣列拷貝

上面介紹了陣列的簡單使用,下面我們再來重點看一個操作:陣列拷貝。比如:

String[] arrs = {"元素1","元素2","元素3","元素4","元素5","元素6","元素7"};
String[] newArrs = new String[7];

// 如何把arrs資料賦值給newArrs

arrs 是另一種定義方式:

  • 就是說我們知道陣列中存放的資料,可以直接通過**{}**的形式儲存資料,也就是初始值

陣列拷貝操作:

  • 常規方式就是通過for迴圈,根據下標然後對新陣列進行復制,這種方式比較簡單,我就不做過多的說明

Java.lang.System中為我們提供了一個靜態方法,可以專門用來做陣列拷貝:

public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

首先來看如何使用:

String[] arrs = {"元素1","元素2","元素3","元素4","元素5","元素6","元素7"};
String[] newArrs = new String[7];

System.arraycopy(arrs, 0, newArrs, 0, 7);
Arrays.stream(newArrs).forEach(System.out::println);
/**
元素1
元素2
元素3
元素4
元素5
元素6
元素7
*/

newArrs = new String[3];
System.arraycopy(arrs, 2, newArrs, 0, 3);
Arrays.stream(newArrs).forEach(System.out::println);
/**
元素3
元素4
元素5
*/

Arrays.stream(newArrs).forEach(System.out::println);這是jdk8之後推出的lambda的寫法,看不懂的可以忽略,就當for迴圈輸出就好

從上面的方法和輸出我們多少都能看出來這裡的引數的意義,用一句話總結下來就是:

將源陣列的第srcPos個位置到指定長度的元素copy到目標陣列從第destPos個位置開始到指定長度位置

老外很有趣,以後再看到 srcsource 這些都是代表的是源;desttarget 都是代表目標

而且一般引數的順序都是:先源後目標

切記:在copy的時候,不能超過目標陣列限定長度

而且:該方法執行效率略低,涉及到陣列整體資料的copy

陣列還有一個二維陣列,這裡就不介紹了。在實際工作中很少會用到。

關於陣列就介紹到這裡,我們還沒有結束,下面我們重點要來聊一聊Java中的集合

認識集合

可以說,集合Java中也是屬於一種容器,上面我們介紹的陣列也是一種容器。上面我們也介紹過了陣列,我們先考慮一個問題:

  • 為什麼會在存在了陣列的情況下,Java又推出了集合

我們在這裡來總結一下陣列的特點,上面說到:

  • 陣列是一個相同型別資料的集合,在陣列中只能儲存一種型別的資料

  • 陣列在定義的時候必須指定容器的長度,而且超出長度之後就無法在儲存資料。

可以說,這兩個特點讓我們在實際的開發中存在過多的限制。

所以出現了我們接下來要聊的集合

集合框架

集合在Java中存在於java.util包下,為我們提供了一套效能優良,使用方便的介面和類。

在集合框架中,我們可以感受到另外一種程式設計思想:面向介面程式設計

根據儲存資料格式進行劃分,可以分為兩大類

  • Collection
  • Map

兩者都是介面,通過介面來規範操作方式

其中,Collection儲存的資料是單一元素,而Map是已 *<Key, Value>*的格式儲存起來的。下面我們通過一張圖來看看這一系列的主角們:

集合結構圖

我們直接來聊聊ArrayList

ArrayList

ArrayList屬於List的子類,其特點:

  • 有序
  • 不唯一(可重複)

ArrayList結構

很多時候不知道無從下手的時候,就先從該類的構造方法看起:

這是我們在使用ArrayList的方式

// 無參
List<String> list1 = new ArrayList<>();
// 指定初始化長度
List<String> list2 = new ArrayList<>(8);
// 初始化內容
List<String> list3 = new ArrayList<String>(Arrays.asList(arrs));

下面我們通過這三個構造方法來具體看下ArrayList是如何實現的:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_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);
    }
}

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;
    }
}

通過檢視構造方法,我們可以看出:

  • ArrayList底層是採用陣列來儲存資料的,上面我們對陣列也有了一定的瞭解
  • 因為是陣列的形式,新增元素是有序的,且允許新增null
  • 同時,當我們採用無參構造方法的時候,預設是空的Object陣列

private static final int DEFAULT_CAPACITY = 10; 我們記住這個屬性,很快我們就會說到

具體操作方法

背景: 我們後續的方式都以無參構造方法建立的ArrayList物件進行說明

預設構造方法在初始化的時候,底層預設是空的Object資料,上面陣列說到,我們在儲存資料的時候陣列必須要定義陣列的長度,那麼無參構造方法在新增元素的時候又是怎麼做到新增成功而且不會出錯呢?

下面我們來詳細瞭解一下

add()

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

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++;
}

兩者都是新增元素的方法,唯一的區別在於:

  • **add(index, element)**方法可以指定元素的新增位置

    關於該方法的執行效率,是分情況的

    • 如果指定的index越靠近頭部位置,那麼效率越低;反之亦然

add方法對比

  • **add(element)**預設是按照順序新增

而且新增方式可以分為兩步:

  • 判斷當前容量是否需要擴容?如果需要擴容就先對陣列進行擴容操作然後再進行賦值
  • 否則就直接在對應位置上進行賦值操作

下面我們來研究一下擴容的方法:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

針對底層初始化的時候出現空Object陣列的情況,這裡對該情況進行了驗證:

  • 如果是空的Object[], 那麼就會拿預設初始化值10和容量對比,取最大值(addAll也是一樣的),可以說,預設情況下第一次新增元素時,會將當前容量擴容到10個長度

然後得到上面通過驗證得到的長度進行擴容判斷,如果容量不足,才會進行擴容的操作:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

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

我們具體檢視**grow()**方法,可以得到以下結果:

  • 如果elementData是空陣列的情況下,會擴容到10個長度。

因為如果新擴容後的長度小於**calculateCapacity()**得到的值的話,**calculateCapacity()**得到的值就是最終要擴容的值

**calculateCapacity()**得到的值:使我們當前List的size() + 我們需要新增List中元素的數量

  • 而且下次還需要擴容的話,會擴大到當前容量的1.5倍長度
  • List擴容操作其實是通過上面我們講到的 System.arraycopy()方法來進行操作的,我們也說到過,這種方式對效能有影響。所以我們在使用的時候,最好預先設定初始容量,這樣可以減少擴容的次數

迭代器:Iterator

ArrayList底層是採用資料結構來實現的,所以我們隨機訪問資料的方式和直接通過陣列來訪問是一樣的,不過在ArrayList中專門提供了一個方法:

List<String> list = new ArrayList<>(5);
list.add("11");
list.add("12");
list.add("13");
list.add("14");
list.add("15");

System.out.println(list.get(0));	// 11

下來可以自己看下實現原始碼,肯定是陣列根據下標獲取元素的方式

這塊主要的內容是 Iterator,是Java對設計模式中迭代器模式的實現,下面我們來看操作程式碼

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String next = iterator.next();
    System.out.println(next);
}

List提供了iterator(),通過該方法我們就可以拿到Iterator進行迭代操作,我們先來看一看它的執行流程是什麼樣,其中最重要的兩個屬性:

  • cursor: 遊標,得到的是下一個元素
  • lastRet:返回當前元素
流程
  • 預設情況下,cursor=0,lastRet=-1
  • 在使用的時候,while一直在判斷是否還存在元素:
public boolean hasNext() {
    return cursor != size;
}
  • 如果存在的話,呼叫next()得到當前元素,同時改變cursor和lastRet的值,可以進行下一次的操作
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];
}
  • 接下來就是不斷迴圈直到hasNext()為false

看圖更詳細

Iterator執行流程

和ListIterator的對比
ListIterator<String> listIterator = list.listIterator();

準確來講,ListIteratorIterator的子類,擁有和Iterator同樣的功能,但是又有不同:

  • ListIterator中存在add()set(),支援在迭代過程中向集合中新增和修改元素
  • ListIterator中存在hasPrevious()previous(),支援逆向迭代
  • 還有其他如得到當前定位索引等特性,下來自己看看原始碼

執行緒安全性

其實,ArrayList如果只是區域性變數的話,是不牽扯執行緒安全不安全的,只有作為共享資源存在的時候,才會出現執行緒不安全的問題:

  • ArrayList的方法沒有加鎖
  • 變數沒有用volidate修飾

這部分設計到多執行緒的知識,後面會對多執行緒的基礎知識進行介紹

同時還會專門開一個專欄,深入瞭解多執行緒的內容。大家在這裡記住就可以了

那麼, 這種情況下,我們如何操作來保證執行緒安全呢?

  • 自己對操作的方法進行加鎖
  • 採用Java提供的方法:Collections.synchronizedList(list);

自己檢視原始碼,其實就是對方法進行加鎖操作

  • 使用CopyOnWriteArrayList,該類屬於java.util.concurrent下,也就是我們所說的JUC

文件

更多關於ArrayList使用方法推薦檢視其文件:

ArrayList API文件