Java 的ArrayList集合底層實現(xiàn)與最佳實踐
1. 核心概念與底層實現(xiàn)
1.1 ArrayList 的本質(zhì)
ArrayList 是基于 動態(tài)數(shù)組 的 List 實現(xiàn)類,其底層數(shù)據(jù)結(jié)構(gòu)是 Object[] elementData。它通過動態(tài)擴容機制(自動擴展數(shù)組長度)實現(xiàn)靈活的存儲需求,但犧牲了線程安全性。
- 定義:
ArrayList是Java集合框架中的一個動態(tài)數(shù)組實現(xiàn)類,繼承自AbstractList,實現(xiàn)了List接口。它允許存儲重復(fù)元素和null值,并支持通過索引快速訪問元素。 - 核心特性:
- 動態(tài)數(shù)組:容量可自動擴展,無需手動管理。
- 線程不安全:多線程環(huán)境下需手動同步或使用
Vector/CopyOnWriteArrayList。 - 隨機訪問高效:通過索引訪問元素的時間復(fù)雜度為O(1)。
- 增刪操作較慢:中間位置的插入/刪除需要移動元素,時間復(fù)雜度為O(n)。
1.1.1 底層數(shù)據(jù)結(jié)構(gòu)
ArrayList的底層基于對象數(shù)組(Object[] elementData)實現(xiàn):
- 初始容量:默認為10(通過
DEFAULT_CAPACITY定義)。 - 動態(tài)擴容:當數(shù)組空間不足時,自動擴容為原容量的1.5倍(
oldCapacity + (oldCapacity >> 1))。 - 關(guān)鍵成員變量:
// 底層數(shù)組,存儲所有元素 transient Object[] elementData; // 當前元素個數(shù) private int size;
JDK 1.7 vs JDK 1.8的初始化差異
| 特性 | JDK 1.7 | JDK 1.8+ |
|---|---|---|
無參構(gòu)造的elementData.length | 10 | 0(DEFAULTCAPACITY_EMPTY_ELEMENTDATA) |
| 首次添加元素時的擴容 | 不觸發(fā)(已預(yù)分配) | 觸發(fā)擴容到10 |
| 內(nèi)存占用 | 立即分配10個元素的內(nèi)存空間 | 延遲分配,節(jié)省初始內(nèi)存 |
1.1.2 動態(tài)擴容機制
擴容觸發(fā)條件:
當調(diào)用add()方法時,若當前數(shù)組容量(elementData.length)小于size + 1,則觸發(fā)擴容。
擴容規(guī)則:
- 新容量計算:
newCapacity = oldCapacity + (oldCapacity >> 1)(即原容量的1.5倍)。 - 特殊情況處理:
- 若計算后的容量仍小于所需最小容量(
minCapacity),則直接使用minCapacity。 - 若擴容過程中出現(xiàn)整數(shù)溢出(極端大容量),拋出
OutOfMemoryError。
- 若計算后的容量仍小于所需最小容量(
1.2 關(guān)鍵成員變量
// 核心成員變量 transient Object[] elementData; // 底層數(shù)組,存儲元素 private int size; // 當前元素個數(shù) // 靜態(tài)常量 private static final Object[] EMPTY_ELEMENTDATA = new Object[0]; // 空數(shù)組 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0]; // 無參構(gòu)造時使用 private static final int DEFAULT_CAPACITY = 10; // 默認初始容量 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 最大數(shù)組長度
2. 初始化機制:從0到10的蛻變
2.1 無參構(gòu)造的陷阱
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 初始化為空數(shù)組(長度0)
}
- 關(guān)鍵點:
elementData.length初始為0,但DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一個靜態(tài)空數(shù)組(長度0)。- 首次添加元素時,會觸發(fā) 強制擴容到10,而非直接使用
elementData的原始長度。
2.2 首次擴容的觸發(fā)過程
當調(diào)用 add() 方法時:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 此處觸發(fā)擴容
elementData[size++] = e;
return true;
}
2.2.1ensureCapacityInternal的核心邏輯
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 強制設(shè)置為10
}
ensureExplicitCapacity(minCapacity);
}
- 關(guān)鍵步驟:
- 檢查
elementData是否為DEFAULTCAPACITY_EMPTY_ELEMENTDATA(即無參構(gòu)造的空數(shù)組)。 - 若是,則將
minCapacity設(shè)為max(10, minCapacity),確保首次擴容至少到10。 - 調(diào)用
ensureExplicitCapacity繼續(xù)檢查。
- 檢查
2.2.2ensureExplicitCapacity的邏輯
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果當前容量不足
if (minCapacity - elementData.length > 0) {
grow(minCapacity);
}
}
- 觸發(fā)擴容條件:
minCapacity > elementData.length。
2.2.3grow方法的擴容計算
private void grow(int minCapacity) {
int oldCapacity = elementData.length; // 當前容量
int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量 = 原容量的1.5倍
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity; // 若新容量仍不足,則直接使用minCapacity
}
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity); // 處理溢出
}
elementData = Arrays.copyOf(elementData, newCapacity); // 復(fù)制數(shù)據(jù)到新數(shù)組
}
- 首次擴容時:
oldCapacity = 0→newCapacity = 0 + 0 = 0,但經(jīng)過ensureCapacityInternal的修正后,minCapacity = 10。- 最終
newCapacity = 10,觸發(fā)Arrays.copyOf創(chuàng)建新數(shù)組。
3. 容量演變的詳細過程
初始容量的定義
- 默認初始容量:
ArrayList的默認初始容量是10,但這僅在首次添加元素時生效。 - 底層數(shù)組的初始狀態(tài):
- JDK 1.7:無參構(gòu)造時直接創(chuàng)建長度為10的數(shù)組(預(yù)分配策略)。
- JDK 1.8+:無參構(gòu)造時底層數(shù)組初始化為空數(shù)組(長度為0),采用懶漢式初始化,直到首次調(diào)用
add()方法時才會分配容量為10的數(shù)組。
關(guān)鍵成員變量
// 源碼片段(JDK 1.8+)
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 默認初始容量為10
private static final int DEFAULT_CAPACITY = 10;
// 空數(shù)組,用于區(qū)分不同狀態(tài)的空ArrayList
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 無參構(gòu)造時使用
// 底層數(shù)組,存儲元素
transient Object[] elementData;
// 當前元素個數(shù)
private int size;
}無參構(gòu)造方法的初始化
// 無參構(gòu)造方法(JDK 1.8+)
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 初始化為空數(shù)組
}
elementData的初始狀態(tài):elementData被初始化為DEFAULTCAPACITY_EMPTY_ELEMENTDATA,其長度為0。- 此時,
ArrayList的容量(elementData.length)為0,但默認容量(DEFAULT_CAPACITY)是10。
首次添加元素時的初始化
當調(diào)用add()方法添加第一個元素時,會觸發(fā)以下流程:
// add(E e)方法(JDK 1.8+)
public boolean add(E e) {
// 確保容量足夠
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 如果當前elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(即無參構(gòu)造的情況)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 將minCapacity設(shè)為max(DEFAULT_CAPACITY, minCapacity)
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果當前容量不足
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}- 關(guān)鍵步驟:
- 當
elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA時(即無參構(gòu)造的空列表),minCapacity被強制設(shè)為Math.max(DEFAULT_CAPACITY, minCapacity)。 - 此時,
minCapacity為10(假設(shè)首次添加一個元素,minCapacity = 1,但會被替換為10)。 - 調(diào)用
grow(minCapacity)擴容到10。
- 當
初始容量的動態(tài)變化
- JDK 1.8+的初始容量流程:
- 無參構(gòu)造時:
elementData.length = 0,size = 0。 - 首次添加元素時:擴容到10,
elementData.length = 10,size = 1。 - 后續(xù)添加元素時:當
size達到10時,觸發(fā)下一次擴容(10 → 15)。
- 無參構(gòu)造時:
3.1 不同場景的容量變化
3.1.1 無參構(gòu)造的初始狀態(tài)
ArrayList<String> list = new ArrayList<>(); System.out.println(list.size()); // 0 System.out.println(list.elementData.length); // 0(JDK 1.8+)
3.1.2 首次添加元素
list.add("Hello");
// 此時:
System.out.println(list.size()); // 1
System.out.println(list.elementData.length); // 10(擴容到10)
3.1.3 添加第11個元素
for (int i = 1; i < 10; i++) {
list.add("World");
}
list.add("World"); // 第11個元素
// 此時:
System.out.println(list.size()); // 11
System.out.println(list.elementData.length); // 15(10 → 15)
3.1.4 添加第16個元素
for (int i = 0; i < 5; i++) {
list.add("Java");
}
// 此時:
System.out.println(list.elementData.length); // 22(15 → 22)
4. 擴容的數(shù)學模型與性能分析
4.1 擴容的數(shù)學公式
- 擴容公式:
newCapacity = oldCapacity + (oldCapacity >> 1)
等價于newCapacity = oldCapacity * 1.5(向下取整)。
4.2 擴容的漸進特性
| 當前容量 | 新容量計算 | 新容量實際值 |
|---|---|---|
| 0 | 0 + 0 | 10(強制修正) |
| 10 | 10 + 5 | 15 |
| 15 | 15 + 7 | 22 |
| 22 | 22 + 11 | 33 |
4.3 擴容的性能代價
- 時間復(fù)雜度:
每次擴容需復(fù)制所有元素,時間復(fù)雜度為 O(n),但通過 指數(shù)增長策略,總擴容時間復(fù)雜度為 O(n)(攤還分析)。 - 空間復(fù)雜度:
最終容量可能超過實際需求,但通過trimToSize()(JDK 11+)可回收冗余空間。
5. 核心方法的源碼解析
5.1 尾部添加add(E e)
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 確保容量足夠
elementData[size++] = e; // 直接寫入數(shù)組末尾
return true;
}
- 時間復(fù)雜度:O(1)(不考慮擴容開銷)。
5.2 中間插入add(int index, E element)
public void add(int index, E element) {
rangeCheckForAdd(index); // 索引檢查
ensureCapacityInternal(size + 1);
// 將[index, size) 的元素后移一位
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
- 時間復(fù)雜度:O(n)(需移動元素)。
5.3 刪除元素remove(int index)
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; // 釋放引用
return oldValue;
}
- 時間復(fù)雜度:O(n)(需移動元素)。
6. 特殊場景與常見問題
6.1 初始容量為0的誤解
- 常見誤區(qū):認為
ArrayList的初始容量是0,但實際:- JDK 1.8+:無參構(gòu)造時
elementData.length = 0,但首次添加元素時強制擴容到10。 size的初始值始終為0,直到元素被添加。
- JDK 1.8+:無參構(gòu)造時
6.2 擴容溢出的處理
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) { // 無法處理負數(shù)
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
- 作用:當擴容請求超過
MAX_ARRAY_SIZE(2^31-9)時,使用Integer.MAX_VALUE。
6.3 并發(fā)修改異常(ConcurrentModificationException)
- 觸發(fā)條件:在迭代過程中修改集合(非通過迭代器)。
- 解決方案:
List<String> list = new ArrayList<>();
// 錯誤示例:
for (String s : list) {
if (s.equals("remove")) {
list.remove(s); // 拋出異常
}
}
// 正確示例:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("remove")) {
it.remove(); // 安全刪除
}
}7. 性能優(yōu)化與最佳實踐
7.1 預(yù)分配容量
// 優(yōu)化示例:已知數(shù)據(jù)量時預(yù)分配容量
ArrayList<String> list = new ArrayList<>(1000000); // 初始容量100萬
for (int i = 0; i < 1000000; i++) {
list.add("Data" + i);
}
7.2 避免頻繁擴容
- 場景:添加大量元素時,預(yù)分配容量可減少擴容次數(shù)。
- 數(shù)學證明:
- 假設(shè)初始容量為
C,每次擴容增長1.5倍,添加N個元素時,擴容次數(shù)為:log_1.5(N / C)
- 預(yù)分配
C = N可完全避免擴容。
- 假設(shè)初始容量為
7.3 使用trimToSize()
// JDK 11+:回收冗余容量 list.trimToSize(); // 將數(shù)組長度調(diào)整為當前size
8. 與 LinkedList 的深度對比
8.1 底層結(jié)構(gòu)對比
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 存儲結(jié)構(gòu) | 連續(xù)內(nèi)存數(shù)組 | 雙向鏈表(每個節(jié)點存儲前后指針) |
| 訪問速度 | 快(O(1)) | 慢(O(n)需遍歷) |
| 插入/刪除速度 | 慢(中間操作需移動元素,O(n)) | 快(修改指針,O(1)) |
| 內(nèi)存占用 | 低(連續(xù)內(nèi)存) | 高(每個節(jié)點存儲額外指針) |
8.2 適用場景
- ArrayList:
- 頻繁查詢、少量增刪。
- 數(shù)據(jù)量較大但訪問模式固定。
- LinkedList:
- 頻繁增刪、少量查詢。
- 需要雙向遍歷或鏈表特性(如隊列、棧)。
9. 源碼級優(yōu)化技巧
9.1 避免toArray()的性能陷阱
// 錯誤示例:頻繁調(diào)用toArray()導(dǎo)致額外開銷
for (Object obj : list.toArray()) {
// ...
}
// 優(yōu)化示例:直接使用elementData(需謹慎)
Object[] arr = list.toArray();
for (Object obj : arr) {
// ...
}9.2 使用subList()的注意事項
// 避免直接修改子列表的引用 List<String> sublist = list.subList(0, 10); sublist.clear(); // 會修改原列表
10. 總結(jié):ArrayList 的設(shè)計哲學
- 核心思想:通過 動態(tài)數(shù)組 實現(xiàn)高效隨機訪問,以 1.5倍擴容 平衡內(nèi)存與性能。
- 適用場景:優(yōu)先選擇
ArrayList,除非需要頻繁的中間增刪操作。 - 最佳實踐:預(yù)分配容量、避免并發(fā)修改、合理使用
trimToSize()。
到此這篇關(guān)于Java 的ArrayList集合的文章就介紹到這了,更多相關(guān)Java ArrayList集合內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring根據(jù)XML配置文件 p名稱空間注入屬性的實例
下面小編就為大家分享一篇Spring根據(jù)XML配置文件 p名稱空間注入屬性的實例,具有很好的參考價值。希望對大家有所幫助2017-11-11
Springboot Maven打包跳過測試的五種方式小結(jié)
本文主要介紹了Springboot Maven打包跳過測試的五種方式小結(jié),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-04-04
MyBatis3源碼解析之如何獲取數(shù)據(jù)源詳解
用myBatis3與spring整合的時候,我們可以通過多種方式獲取數(shù)據(jù)源,下面這篇文章主要給大家介紹了關(guān)于MyBatis3源碼解析之如何獲取數(shù)據(jù)源的相關(guān)資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下2022-06-06
java httpclient設(shè)置超時時間和代理的方法
這篇文章主要介紹了java httpclient設(shè)置超時時間和代理的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-02-02
Springcould多模塊搭建Eureka服務(wù)器端口過程詳解
這篇文章主要介紹了Springcould多模塊搭建Eureka服務(wù)器端口過程詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2019-11-11
SpringBoot2.6.x升級后循環(huán)依賴及Swagger無法使用問題
這篇文章主要為大家介紹了SpringBoot2.6.x升級后循環(huán)依賴及Swagger無法使用問題,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06

