目錄java
1、前言
2、ArrayList 的繼承與實現關係
2.1 ArrayList.java
2.2 抽象類AbstractList.java
2.3 接口List.java
2.4 接口RandomAccess.java
2.5 接口Cloneable
2.6 接口Serializable
3、ArrayList 關於數組和集合的討論
3.1 ArrayList 是數組仍是集合問題說明
3.2 從構造方法分析ArrayList
3.1 確認ArrayList 是集合
4、ArrayList 初始容量是0 仍是10 問題的確認
4.1 從構造方法看初始容量
4.2 從add() 方法看初始容量
4.3 肯定ArrayList 的初始容量
5、ArrayList 的擴容問題探索
5.1 擴容問題說明
5.2 經過add() 方法探索擴容問題
5.3 擴容算法
5.4 模擬擴容演示
6、ArrayList 的序列化問題補充算法
這裏主要研究到如下問題,經過源碼閱讀分析探索如下問題的答案。本文不牽涉到更多問題,因此源碼只貼出與這些問題直接聯繫的關鍵代碼塊。固然源碼中必要的全局常量、方法會貼出。數組
- ArrayList 的繼承與實現關係;
- ArrayList 關於數組和集合的討論;
- ArrayList 初始容量是0仍是10問題的確認;
- ArrayList 的擴容問題探索;
- ArrayList 的序列化問題補充;
ArrayList類經過extends關鍵字繼承AbstractList抽象類,經過關鍵字implements實現List集合接口、RandomAccess標記接口、Cloneable克隆接口、Serializable序列化接口。bash
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}複製代碼
圖2-一、ArrayList 繼承與實現關係圖:
dom
抽象類AbstractList繼承一個AbstractCollection集合一樣實現了集合List接口。性能
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {}複製代碼
List直接繼承於底層集合Collection,List是一個集合,誰同意?誰反對?ui
public interface List<E> extends Collection<E> {}複製代碼
此接口的主要目的是容許通常的算法更改其行爲,從而在將其應用到隨機或連續訪問列表時能提供良好的性能。
接口RandomAccess 是一個標記接口,實現該接口的集合List 支持快速隨機訪問。List 集合儘可能要實現RandomAccess 接口,若是集合類是RandomAccess 的實現,則儘可能用for(int i = 0; i < size; i++) 來遍歷效率高,而不要用Iterator迭代器來遍歷(若是List是Sequence List,則最好用迭代器來進行迭代)。this
關於深拷貝與淺拷貝應寫一篇博客去說明。想深刻了解能夠參考知乎問答深拷貝與淺拷貝
實現接口的目的是重寫java.lang.Object的clone()的方法,實現淺拷貝。深拷貝和淺拷貝針對像 Object, Array 這樣的複雜對象的。淺拷貝只複製一層對象的屬性,而深拷貝則遞歸複製了全部層級。spa
- 淺拷貝
被複制(拷貝)對象的全部變量都含有與原來的對象相同的值,而全部的對其餘對象的引用仍然指向原來的對象。淺拷貝即新建一個對象,複製原對象的基本屬性,一級屬性到新的存儲空間,不拷貝原對象的對象引用元素,新對象的對象引用指向原來的存儲空間,修改對象引用的元素,那麼拷貝對象和原對象都會變化。- 深拷貝
深拷貝是一個整個獨立的對象拷貝,深拷貝會拷貝全部的屬性,並拷貝屬性指向的動態分配的內存。當對象和它所引用的對象一塊兒拷貝時即發生深拷貝。深拷貝相比於淺拷貝速度較慢而且花銷較大。深拷貝新建一個對象,不只拷貝對象,還拷貝對象引用;深拷貝就是咱們日常理解的複製,將對象的所有屬性及對象引用複製到新的存儲空間,不會指向原來的對象,修改新對象的任意元素都不會影響原對象。
該接口無繼承實現關係,實現該接口的類支持序列化。所以ArrayList 支持序列化。3d
- 序列化:能夠將一個對象的狀態寫入一個Byte 流裏;
- 反序列化:能夠從其它地方把該Byte 流裏的數據讀出來。
ArrayList 是數組仍是集合,這也算問題?
咱們都知道List 是集合啊,ArrayList 繼承於List 也是集合。不過你或許會在某些文章上見過ArrayList 是數組或者說ArrayList 是基於數組的說法。
那咱們首先看一下ArrayList 的構造方法;
// 默認空數組,final 關鍵字修飾
private static final Object[] EMPTY_ELEMENTDATA = {};
// 空數據的共享空數組實例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 存儲ArrayList元素的數組緩衝區,即ArrayList 存放數據的地方
* ArrayList的容量是這個數組緩衝區的長度
* transient 關鍵字修飾,elementData 不支持序列化
*/
transient Object[] elementData;
/**
* 默認無參構造方法
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 帶整型參數的構造方法
* @param initialCapacity 初始化容量大小
*/
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);
}
}
/**
* 泛型集合參數構造方法
* @param c 集合類型參數
*/
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;
}
}
複製代碼
無參構造方法中直接初始化Object[] elementData = {}; 即ArrayList 的存儲數據的容器是一個數組。帶參構造方法也是new Object() 或者經過Arrays 的方法轉換爲數組對象。
ArrayList 實現了List 接口,重寫了集合的add(),size(),get(),remove(),toArray()等方法,多個方法的內部代碼塊是基於數組來處理數據的。
所以ArrayList 是實現List 接口的集合,是基於數組的集合,數據的存儲容器是數組,集合方法是經過數組實現的(好比泛型參數構造方法是將傳入的集合c 先轉化爲數組在進行處理的)。包括其內部類Itr implements Iterator 中從新Iterator 的方法也是基於數組計算的。
搞這個問題有意義嗎?有意義^_^
從第三部分中的構造方法能夠看出
無參構造一個ArrayList 時存儲數據的容器elementData = {};此時存儲容器大小爲0 ;
帶整型參數的構造方法經過傳入的整型數據的大小來確認初始化存儲容器elementData 的大小,當initialCapacity == 0 時,仍是賦值elementData = {};
泛型集合參數構造方法,根據集合的大小來初始化elementData 的大小,將集合轉化爲數組,數組的大小爲0 的狀況下,仍然賦值elementData = {};
這個初始化和10 又有什麼關係???
在ArrayList 中定義了一個默認的存儲容器的大小DEFAULT_CAPACITY 爲10,用關鍵字final 修飾,註釋是默認初始容器大小,經過構造方法建立ArrayList 對象並無使用到這個常量,咱們看看這個初始容器大小是怎麼初始化容器大小的。
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
// 集合的邏輯大小,即存儲真實數據的數量
private int size;
public int size() {return size;}
public boolean isEmpty() {return size == 0;}
/**
* 添加元素
* @param e
* @return
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
/**
* 確認集合內部容量大小
* @param minCapacity
*/
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
/**
* 計算集合的容量
* @param elementData
* @param minCapacity
* @return
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
複製代碼
在集合add() 添加元素時,會將當前的size + 1 傳入ensureCapacityInternal() 方法確認當前elementData 數組大小是否足夠
足夠的話size自增一,size = size + 1直接添加的元素賦值給elementData[size];
不足夠的話進行擴容,擴容問題下面涉及,這裏說擴容中特殊狀況,對空集合的擴容,好比咱們經過無參構造方法建立了集合對象,此時容器大小爲0,而後調用add() 方法添加一個元素,此時elementData == {}即此時elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,知足該條件在計算集合的容量方法calculateCapacity 中會進行」容器初始化」,實際上是擴容而已;
這裏的」=」 是等於不是賦值
此時return Math.max(DEFAULT_CAPACITY, minCapacity);
minCapacity = size + 1 = 0 + 1 = 1
DEFAULT_CAPACITY = 10
minCapacity < DEFAULT_CAPACITY = 1 < 10
結果return 10;
此時容器elementData 擴容爲Object[10]
從以上兩方面分析,因此ArrayList 的初始容量根據傳參肯定,默認無參構造方法下新對象的容器初始大小爲0。而10 是在空集合添加第一個元素時擴容時的默認容器大小。
集合擴容就是集合容量大小不能知足須要存儲數據的數量,而須要將elementData 容器大小增大,以存儲更多的元素。
集合存儲容器elementData 的容量大小不小於真實存儲元素數量size
elementData.length > size 爲真true
elementData.length = size 爲真true
elementData.length < size 爲假false
集合在添加元素時會首先判斷當前容器是否能裝下第size + 1 個元素。不能的狀況下會進行擴容,上面初始容量問題中談到當空集合擴容時會給該集合對象一個默認的容器大小10,即擴容到elementData.length == 10
這是一種特殊狀況,給了一個默認值,並無真正涉及擴容核心算法。
下面看看ArrayList 是如何擴容的。
// 集合最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 添加元素
* @param e
* @return
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
/**
* 確認集合內部容量大小
* @param minCapacity 添加元素後容器的最小容量
*/
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
/**
* 計算集合的容量
* @param elementData 存儲數據的容器
* @param minCapacity 添加元素後容器的最小容量
* @return
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
/**
* 確認明確的容量大小
* @param minCapacity 添加元素後容器的最小容量
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* 擴容方法
* @param minCapacity 添加元素後容器的最小容量
*/
private void grow(int minCapacity) {
// 擴容前容器大小
int oldCapacity = elementData.length;
// 擴容關鍵算法,newCapacity 擴容後容器大小
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity);
// 將擴容後的容器賦值給存儲容器
elementData = Arrays.copyOf(elementData, newCapacity);
}
/**
* 溢出處理
*/
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) throw new OutOfMemoryError();
// 超過最大值不合法,直接將容量大小定義爲Intager 的最大值
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
複製代碼
在添加元素的方法中依此調用容器大小判斷相關的方法,當容器大小不夠時,會進行擴容,調用grow() 方法進行擴容。擴容方法很簡單,拿到擴容前容器大小oldCapacity,進行擴容,判斷擴容後容量是否合法,是否溢出,而後進行處理爲合理的大小。
擴容算法是首先獲取到擴容前容器的大小。而後經過oldCapacity + (oldCapacity >> 1) 來計算擴容後的容器大小newCapacity。
這裏的擴容算法用到了>> 右移運算。即將十進制轉換爲二進制,每一位右移後獲得的結果。oldCapacity >> 1即oldCapacity 對2 求摩,oldCapacity/2;
oldCapacity + (oldCapacity >> 1)即oldCapacity + (oldCapacity / 2)
因此關鍵擴容算法就是當容量不夠存儲元素時,在原容器大小size 基礎上再擴充size 的接近一半,即大約擴充原容器的一半。
相對直白的嚴謹的擴容算法以下:
擴容後容器大小newCapacity = size + size / 2
舉個栗子:原容器是10,elementData 已經存儲10 個元素了,再次調用add() 方法會走grow() 方法進行擴容。運行中截圖以下圖
10 / 2 =5
新的容器大小爲 10 + 5 = 15
另:運算 「/」 的結果是整數,15/2 =7;9/2 = 4; 8/2 = 4;
圖5-一、擴容前容量大小圖:
圖5-二、擴容後容量大小圖:
圖5-三、擴容後elementData 容量大小圖:
集合的存儲容器elementData 使用transient 關鍵字修飾不支持序列化,可是咱們知道ArrayList 是支持序列化的,那咱們是怎麼序列化集合中的數據呢,這裏不直接序列化elementData,而是遍歷每一個數據分別進行IO 流處理來實現存儲容器中對象的序列化的。
// ArrayList 列表結構被修改的次數。
protected transient int modCount = 0;
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// ArrayList 列表結構被修改的次數。
int expectedModCount = modCount;
s.defaultWriteObject();
s.writeInt(size);
// 對每個對象進行IO 流的寫處理
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
複製代碼
這裏對存儲容器Object[] elementData 用transient 關鍵字修飾,考慮到容器的存儲空間在擴容後會產生很大閒置空間,擴容前容量越大這個問題越明顯;序列化時會將空的對象空間也進行序列化,而真實存儲的元素的數量爲size,那樣處理的話效率很低,因此這裏不支持存儲容器直接序列化,而寫一個新的方法來只序列化size 個真實元素便可。