首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java ArrayList 深度解析:動態數組源碼、擴容機制、遍歷效率與 toArray 避坑指南

     編輯:Java 相關知識

java.util.ArrayList 是 Java 集合框架中使用頻率最高的動態數組實現。本文從源碼級剖析其底層結構、擴容策略、隨機訪問優勢、序列化機制,並通過實測數據對比三種遍歷效率,最後重點講解 toArray() 類型轉換陷阱與正確姿勢。

一、類定位與核心特性

public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, Serializable { }
  • 底層:transient Object[] elementData + int size。
  • 實現 RandomAccess → 標記支持快速隨機訪問(O(1) 下標直達)。
  • 實現 Cloneable、Serializable → 支持淺克隆與標準序列化。
  • 非線程安全,單線程高性能首選;多線程見並發包替代方案。

二、三種構造函數與默認容量

構造函數行為
ArrayList()默認容量 10,elementData = new Object[10]
ArrayList(int initialCapacity)指定初始容量,負數拋異常
ArrayList(Collection<? extends E> c)複製構造,容量 = 集合大小,數組類型校正為 Object[]

三、核心源碼邏輯精選

3.1 擴容機制:ensureCapacity 與 1.5 倍增長

public void ensureCapacity(int minCapacity) {
    modCount++;
    int oldCapacity = elementData.length;
    if (minCapacity > oldCapacity) {
        Object[] oldData = elementData;
        int newCapacity = (oldCapacity * 3) / 2 + 1;  // 1.5倍 + 1
        if (newCapacity < minCapacity) newCapacity = minCapacity;
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
}
  • add(E e) 內部調用 ensureCapacity(size + 1)。
  • 擴容涉及 Arrays.copyOf → 原生數組複製,性能較好但頻繁擴容仍有開銷。
  • 最佳實踐:預估數據量時構造函數指定 initialCapacity 避免擴容。

3.2 隨機訪問:get/set 極致 O(1)

public E get(int index) {
    rangeCheck(index);
    return (E) elementData[index];  // 直接下標訪問
}

public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = (E) elementData[index];
    elementData[index] = element;
    return oldValue;
}

3.3 插入/刪除:數組搬移 O(n)

// 指定位置插入
public void add(int index, E element) {
    rangeCheckForAdd(index);
    ensureCapacity(size + 1);
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    elementData[index] = element;
    size++;
}

// 指定位置刪除
public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = (E) elementData[index];
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    elementData[--size] = null;  // 釋放引用助 GC
    return oldValue;
}

結論:尾部追加 O(1)(均攤),頭部/中間插刪 O(n) 需搬移後續元素。

3.4 序列化:自定義寫入/讀取

private void writeObject(ObjectOutputStream s) throws IOException {
    int expectedModCount = modCount;
    s.defaultWriteObject();
    s.writeInt(elementData.length);  // 寫入數組容量
    for (int i = 0; i < size; i++)   // 寫入前 size 個元素
        s.writeObject(elementData[i]);
    if (modCount != expectedModCount) throw new ConcurrentModificationException();
}

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();
    int arrayLength = s.readInt();
    Object[] a = elementData = new Object[arrayLength];
    for (int i = 0; i < size; i++)
        a[i] = s.readObject();
}

只序列化 size 個有效元素,不序列化數組尾部空槽,節省空間。

3.5 克隆:淺拷貝數組

public Object clone() {
    try {
        ArrayList<E> v = (ArrayList<E>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        throw new InternalError();
    }
}

四、三種遍歷方式效率實測(10 萬元素,JDK 8)

iteratorThroughRandomAccess:3 ms   // for (int i=0; i
結論:實現 RandomAccess 的 ArrayList,**下標隨機訪問最快**,for-each 次之,顯式 Iterator 最慢。實際開發優先 for (int i=0; i 或 for (E e : list)。

五、toArray() 類型轉換陷阱與正確姿勢

5.1 兩個方法簽名

Object[] toArray();                    // 返回 Object[],無法直接轉型
<T> T[] toArray(T[] a);                // 返回 T[],類型安全

5.2 為何 toArray() 會拋 ClassCastException

List<Integer> list = new ArrayList<>();
list.add(1);
Integer[] arr = (Integer[]) list.toArray();  // 拋 java.lang.ClassCastException
// 原因:toArray() 返回 new Object[size],Java 不支持數組向下轉型 (Object[] → Integer[])

5.3 三種正確調用姿勢

// 姿勢一:預分配精確大小數組
public static Integer[] toArray1(List<Integer> list) {
    Integer[] arr = new Integer[list.size()];
    list.toArray(arr);
    return arr;
}

// 姿勢二:傳入零長度數組(最常用、推薦,JDK 內部優化會精確分配)
public static Integer[] toArray2(List<Integer> list) {
    return list.toArray(new Integer[0]);
}

// 姿勢三:預分配後再轉型(冗餘,不推薦)
public static Integer[] toArray3(List<Integer> list) {
    Integer[] arr = new Integer[list.size()];
    return (Integer[]) list.toArray(arr);
}
最佳實踐:統一使用 list.toArray(new T[0])。JDK 6+ 會優化零長度數組分配,性能與預分配持平,代碼最簡潔。

六、實戰示例

import java.util.*;

public class ArrayListDemo {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1"); list.add("2"); list.add("3"); list.add("4");

        // 指定位置插入
        list.add(0, "5");  // [5, 1, 2, 3, 4]
        System.out.println("首元素: " + list.get(0));  // 5

        // 刪除元素
        list.remove("3");  // 刪除值為 "3" 的首個元素
        System.out.println("size: " + list.size());    // 4

        // 判斷包含
        System.out.println("contains 3: " + list.contains("3"));  // false

        // 修改元素
        list.set(1, "10");  // 索引 1 設為 "10"
        System.out.println("list: " + list);  // [5, 10, 2, 4]

        // Iterator 遍歷
        System.out.println("\nIterator 遍歷:");
        for (Iterator<String> it = list.iterator(); it.hasNext(); )
            System.out.println("next: " + it.next());

        // 安全轉數組
        String[] arr = list.toArray(new String[0]);
        System.out.println("\ntoArray 結果:");
        for (String s : arr) System.out.println("str: " + s);

        // 清空
        list.clear();
        System.out.println("isEmpty: " + list.isEmpty());  // true
    }
}

七、選型與最佳實踐總結

場景推薦理由
單線程、高頻隨機訪問、尾部追加ArrayListO(1) 訪問、緊湊內存、無同步開銷
高頻頭部/中間插刪LinkedList / ArrayDeque避免數組搬移
多線程讀多寫少CopyOnWriteArrayList遍歷無鎖、寫時復制
多線程讀寫均衡Collections.synchronizedList / 手動加鎖細粒度控制
預估大數據量new ArrayList<>(estimatedSize)避免多次擴容複製
  • 遍歷優先 for (int i=0; i 或 for (E e : list)。
  • 轉數組統一 list.toArray(new T[0])。
  • 遍歷中需刪除 → 僅用 Iterator.remove()。
  • 多線程環境 → 遷移至 java.util.concurrent 包對應類。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved