介紹
ArrayList是一個陣列佇列,相當於動態陣列,與Java的陣列對比,他的容量可以動態改變。
繼承關係
- ArrayList繼承AbstractList
- 實現了
List
,RandomAccess
,Cloneable
,Serializable
介面
特點
- 基於陣列實現速度快
- 實現了
RandomAccess
介面,提供了隨機訪問功能 - 實現了
Cloneable
介面,能被克隆 - 實現了
Serializable
介面,支援序列化傳輸 - 非執行緒安全(ps:執行緒安全類:CopyOnWriteArrayList)
- 適用於頻繁查詢和獲取資料
- 查詢效率在眾多List中效率還是非常不錯
建構函式以及常用的方法
建構函式
public ArrayList();//無元素預設為0,有元素初始化預設容量為10
public ArrayList(Collection<? extends E> c);//預設為c這個集合的list(淺拷貝)
public ArrayList(int initialCapacity);//設定一個初始化為initialCapacity集合
注意點
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[]的錯誤,所以採用如下方法
if (elementData.getClass() != Object[].class)
//使用陣列拷貝來進行
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
在這個方法中出現if (elementData.getClass() != Object[].class)
這樣一組判斷,查閱資料發現,這是一個bug才這麼判斷的地址,改問題已經在JDK9已經進行了修復了。
成員變數
private static final long serialVersionUID = 8683452581122892189L;
/**
* 預設初始化容量10
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 共享的空資料容器
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 共享空資料容器
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 快取資料集合
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* 容器資料集大小
*
* @serial
*/
private int size;
/**
* 該容器能夠承受的最大容量
* 為什麼是Integer.MAX_VALUE - 8;
* 因為有些VM虛擬機器會在一個陣列中儲存一些頭部資訊,所以採用這個值
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
值得注意的點:
MAX_ARRAY_SIZE
設定為Integer.MAX_VALUE - 8
是因為有些VM虛擬機器會在陣列中儲存一些頭部資訊,從而佔用一些空間,所以-8
常用方法
值得注意的方法
trimToSize():
縮小容器大小讓容器釋放多餘的空間,會觸發一次陣列的變化
/**
* 縮小容器大小
* 例如當你一開始建立了一個100個的List但是你只使用了10個,想將這個容器縮減為10個
*/
public void trimToSize() {
//這個引數和我們併發控制的時候version一個味道,用於判斷是否併發修改的標誌1
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
clone():
返回資料的淺克隆的例項
/**
* 返回資料的淺克隆的例項
*
* @return a clone of this <tt>ArrayList</tt> instance
*/
public Object clone() {
try {
//呼叫父類的克隆方法->Object的克隆方法
ArrayList<?> v = (ArrayList<?>) super.clone();
//拷貝陣列,注意是直接通過Arrays的copyOf所以為淺克隆
v.elementData = Arrays.copyOf(elementData, size);
//設定併發version
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
//克隆異常則丟擲error
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
toArray():
同樣為淺拷貝,拷貝出來的陣列還是會被改變的,該方法返回的為Object[]陣列
/**
* 同樣為淺拷貝,拷貝出來的陣列還是會被改變的
* 將List轉換成陣列
* Demo[] cloneArr = (Demo[]) demos.toArray(); //ERROR
*
* @return an array containing all of the elements in this list in
* proper sequence
*/
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
add方法
/**
* 追加一個元素在列表的最後面
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
//這裡會導致其併發版本+1,
// 因為需要先確認容器大小操作,並確定是否需要擴容。
//對資料有修改,因而其併發版本也就會+1
ensureCapacityInternal(size + 1); // Increments modCount!!
//設定值
elementData[size++] = e;
return true;
}
/**
* 在index位置後插入元素,並移動後面元素的位置
* 1. 需要對index後面的所有的元素index+1,需要拷貝工作產生
* 2. 如果你的List中有大量的這樣的插入工作建議採用
* @see LinkedList
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
//校驗index
rangeCheckForAdd(index);
//確定擴容許可權
ensureCapacityInternal(size + 1); // Increments modCount!!
//陣列拷貝,耗時工作
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//賦值
elementData[index] = element;
//長度
size++;
}
- 該方法每次都要確定容器大小,會導致併發版本的count+1
- add 方法有兩個
- add(E e):直接將資料插入到List的尾部
- add(int index,E e):將資料插入到index後面
- 如果是插入,則會導致陣列進行復制操作,由於ArrayList基於陣列,所以會導致陣列複製,而陣列複製必定是一個耗時的操作
remove()
public E remove(int index) {
//校驗
rangeCheck(index);
//併發引數+1
modCount++;
//獲取舊值
E oldValue = elementData(index);
//移動的長度為=陣列長度-需要刪除元素下標-1
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//讓gc回收這個資料的記憶體
elementData[--size] = null; // clear to let GC do its work
//返回舊值
return oldValue;
}
- 同樣刪除會導致陣列複製
subList
該方法主要用於將陣列進行分割,對於陣列分割後,其實該陣列為淺拷貝操作,如果在該SubList中操作相關資料,將會導致ArrayList中的資料改變!!
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
ArrayList擴容機制
規則
- 如果其陣列需要進行擴容,則會擴容為原陣列的1.5倍
- 如果使用者指定了容器的大小,且用於指定的數值大於容器的最小容量,則將用於容量作為該容器容量
- 如果容器容量擴容後大於
Integer.MAX_VALUE - 8
,則會嘗試擴容為Integer.MAX_VALUE
大小
解析
確定minCapacity值是否比容器中的資料容量大
如果大則擴容,否則什麼也不做
擴容
如果minCapacity比newCapacity小則直接使用minCapacity作為擴容容量
如果其陣列個數大於最大的陣列的長度,則嘗試使用Integer.MAX_VALUE作為陣列的容器大小
原始碼解析
/**
* 1.確定minCapacity值是否比容器中的資料容量大
* 2.如果大則擴容,否則什麼也不做
* @param minCapacity
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
* 擴容
* 1. 如果minCapacity比newCapacity小則直接使用minCapacity作為擴容容量
* 2. 如果其陣列個數大於最大的陣列的長度,則嘗試使用Integer.MAX_VALUE作為陣列的容器大小
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//新容器容量擴容為現在容器的容量的1.5倍
//eg 舊10個->新15個
int newCapacity = oldCapacity + (oldCapacity >> 1);
//看看誰大
if (newCapacity - minCapacity < 0)
//如果min比new小則直接複製min
newCapacity = minCapacity;
//如果新的容器比最大的陣列大小還要打
if (newCapacity - MAX_ARRAY_SIZE > 0)
//只能複製為最大容器大小,但是可能會丟擲oom
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
/**
* 大陣列容器擴容
* @param minCapacity
* @return
*/
private static int hugeCapacity(int minCapacity) {
//校驗引數合法性
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
List的序列化操作
由於Java預設序列化以及反序列化的時候回分別呼叫對應的writeObject方法以及readObject()方法,所以以下將對這兩個方法進行分析。
6.1 為什麼需要自定義序列化規則
由於在ArrayList中的elementData陣列中可能存在一些空的元素(由於ArrayList擴容機制)
6.2 原始碼分析
- 序列化操作:
- ArrayList內部儲存資料元素為
transient
不會被序列化
- ArrayList內部儲存資料元素為
/**
* 快取資料集合
*/
transient Object[] elementData; // non-private to simplify nested class access
如何支援序列化操作
1. 序列化數量 2. 獲取資料陣列,然後使用for迴圈一個一個序列化該物件到資料中。
/** * Save the state of the <tt>ArrayList</tt> instance to a stream (that * is, serialize it). * 保證其可以被序列化到物件中 * * @serialData The length of the array backing the <tt>ArrayList</tt> * instance is emitted (int), followed by all of its elements * (each an <tt>Object</tt>) in the proper order. */ private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff //資料被序列化的數量 int expectedModCount = modCount; //使用預設的模式進行序列化,只序列化非靜態化變數以及非transient修飾的資料 s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone() //寫入長度 s.writeInt(size); // Write out all elements in the proper order. for (int i=0; i<size; i++) { //寫入每一個object資料 s.writeObject(elementData[i]); } //如果發現序列化的modCount與expectedModCount可能是併發導致 if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
如何進行的反序列化
- 重新建立List,然後讀取長度以及多個物件對物件進行相關賦值操作
/** * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is, * deserialize it). * 反序列化資料 */ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { //設定預設的陣列長度為空陣列長度 elementData = EMPTY_ELEMENTDATA; // Read in size, and any hidden stuff //使用預設的模式進行序列化,只序列化非靜態化變數以及非transient修飾的資料 s.defaultReadObject(); // Read in capacity //讀取list長度 s.readInt(); // ignored if (size > 0) { // be like clone(), allocate array based upon size not capacity //開始計算並克隆 int capacity = calculateCapacity(elementData, size); SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); //確定陣列長度是否夠 ensureCapacityInternal(size); //進行資料讀取 Object[] a = elementData; // Read in all elements in the proper order. for (int i=0; i<size; i++) { a[i] = s.readObject(); } } }
JDK1.8新增的方法
forEach()
lambda迴圈方法
/**
* JDK新增方法 ForEach方法
* @param action
*/
@Override
public void forEach(Consumer<? super E> action) {
//校驗lambda不為空
Objects.requireNonNull(action);
//併發version版本統計
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
//迴圈每次判斷一下併發引數是否進行了修改,如果進行了修改則直接退出for迴圈
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
//並丟擲併發異常
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
spliterator
該方法用於返回進行併發計算時候的分割器
/**
* Creates a <em><a href="Spliterator.html#binding">late-binding</a></em>
* and <em>fail-fast</em> {@link Spliterator} over the elements in this
* list.
*
* <p>The {@code Spliterator} reports {@link Spliterator#SIZED},
* {@link Spliterator#SUBSIZED}, and {@link Spliterator#ORDERED}.
* Overriding implementations should document the reporting of additional
* characteristic values.
* 併發分割方法
* 懶載入加入,只有當資料
* @return a {@code Spliterator} over the elements in this list
* @since 1.8
*/
@Override
public Spliterator<E> spliterator() {
/**
* 1. 引數1 this
* 2. origin
* 3. fence 當使用的時候才進行初始化
* 4. 併發引數
*/
return new ArrayListSpliterator<>(this, 0, -1, 0);
}
removeIf方法
lambda 移除符合某個規則的方法
/**
* liambad方法移除元素
* @param filter
* @return
*/
@Override
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
// figure out which elements are to be removed
// any exception thrown from the filter predicate at this stage
// will leave the collection unmodified
int removeCount = 0;
//使用這玩意來統計存在的位置
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
if (filter.test(element)) {
removeSet.set(i);
removeCount++;
}
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
// shift surviving elements left over the spaces left by removed elements
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
//新陣列的大小為之前的陣列長度-需要移除元素的個數
final int newSize = size - removeCount;
//執行清除工作
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
//釋放gc
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
//併發version檢查
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
//併發值+1
modCount++;
}
return anyToRemove;
}
ArrayListSpliterator
其為併發分割器,用於我們使用併發呼叫parallelStream方法時候呼叫該方法
static final class ArrayListSpliterator<E> implements Spliterator<E> {
private final ArrayList<E> list;
private int index; // current index, modified on advance/split
private int fence; // -1 until used; then one past last index
private int expectedModCount; // initialized when fence set
/** Create new spliterator covering the given range */
ArrayListSpliterator(ArrayList<E> list, int origin, int fence,
int expectedModCount) {
this.list = list; // OK if null unless traversed
this.index = origin;
this.fence = fence;
this.expectedModCount = expectedModCount;
}
/**
* 當獲取的時候才進行fence的初始化操作
* @return
*/
private int getFence() { // initialize fence to size on first use
int hi; // (a specialized variant appears in method forEach)
ArrayList<E> lst;
if ((hi = fence) < 0) {
//之前的陣列為空則說明沒有進行分割,則從0開始
if ((lst = list) == null)
hi = fence = 0;
else {
//否則則
expectedModCount = lst.modCount;
//則將該值賦值為lst的長度
hi = fence = lst.size;
}
}
return hi;
}
/**
* 嘗試分割
* 1、總長度為陣列長度
* 2、分割成兩份
* 3、二分法分割
* 3、中間值為陣列長度+分割後的陣列長度,就是二分法啦
* @return
*/
public ArrayListSpliterator<E> trySplit() {
int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
return (lo >= mid) ? null : // divide range in half unless too small
new ArrayListSpliterator<E>(list, lo, index = mid,
expectedModCount);
}
/**
* 併發執行操作
* @param action
* @return
*/
public boolean tryAdvance(Consumer<? super E> action) {
if (action == null)
throw new NullPointerException();
int hi = getFence(), i = index;
//如果沒有超過這個分割的長度則繼續操作否則返回false
if (i < hi) {
index = i + 1;
@SuppressWarnings("unchecked") E e = (E)list.elementData[i];
action.accept(e);
//如果出現了併發改變,則丟擲異常
if (list.modCount != expectedModCount)
throw new ConcurrentModificationException();
return true;
}
return false;
}
/**
* 併發ForEach輸出
* @param action
*/
public void forEachRemaining(Consumer<? super E> action) {
int i, hi, mc; // hoist accesses and checks from loop
ArrayList<E> lst; Object[] a;
if (action == null)
throw new NullPointerException();
if ((lst = list) != null && (a = lst.elementData) != null) {
if ((hi = fence) < 0) {
mc = lst.modCount;
hi = lst.size;
}
else
mc = expectedModCount;
if ((i = index) >= 0 && (index = hi) <= a.length) {
for (; i < hi; ++i) {
@SuppressWarnings("unchecked") E e = (E) a[i];
action.accept(e);
}
if (lst.modCount == mc)
return;
}
}
throw new ConcurrentModificationException();
}
public long estimateSize() {
return (long) (getFence() - index);
}
public int characteristics() {
return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
}
}
ArrayList遍歷
8.1 使用迭代器進行遍歷
Iterator<Double> iterator = list.iterator();
while (iterator.hasNext()){
Double next = iterator.next();
System.out.println(next);
}
使用fori進行遍歷
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
8.3 使用for進行遍歷
for (Double next : list) {
System.out.println(next);
}
8.4 使用流進行訪問(JDK1.8)
list.forEach(System.out::println);
8.5 以上幾種模式的效率比較
測試程式碼如下
package cn.lonecloud;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
/**
* @author lonecloud
* @version v1.0
* @date 2019/4/3 19:56
*/
public class ListTest {
public static void main(String[] args) {
int len=20000000;
List<Integer> list=new ArrayList<>(len);
for (int i = 0; i < len; i++) {
list.add(i);
}
test(list);
}
public static void test(List<Integer> list){
long itrBegin = System.currentTimeMillis();
//1. 使用迭代器
Iterator<Integer> iterator = list.iterator();
Integer a=0;
while (iterator.hasNext()){
Integer next = iterator.next();
a=next;
}
long itrEnd=System.currentTimeMillis();
long foriStart=System.currentTimeMillis();
//2. fori
for (int i = 0; i < list.size(); i++) {
a=list.get(i);
}
long foriEnd=System.currentTimeMillis();
long forStart=System.currentTimeMillis();
//3. for
for (Integer next : list) {
a=next;
}
long forEnd=System.currentTimeMillis();
long streamstart=System.currentTimeMillis();
//4. stream
list.forEach((value)->{
Integer b=value;
});
long streamEnd=System.currentTimeMillis();
System.out.println("迭代器時間:"+(itrEnd-itrBegin));
System.out.println("fori時間:"+(foriEnd-foriStart));
System.out.println("for時間:"+(forEnd-forStart));
System.out.println("stream時間:"+(streamEnd-streamstart));
}
}
結果:
迭代器時間:31
fori時間:32
for時間:32
stream時間:74
總結:
效率:
fori和for和迭代器大致相同,由於事先了RandomAccess
stream時間高一些,原因是需要進行更多的方法呼叫產生的時間
使用到的設計模式
- 迭代器設計模式
- 模板設計模式
總結
ArrayList是基於陣列的集合,適合迴圈迭代多的場景,不適合修改多的場景
在使用ArrayList時候需要注意在建立的時候(預估一下你需要的容器大小)
由於如果你在使用的時候超過了初始化容量(10),這將會導致容器進行一次(容器擴容),而陣列複製是一件非常耗時的操作
ArrayList中的clone()方法以及copy方法,都是淺克隆的。
在一般情況下,如果集合容器出現容量不足需要擴容的時候,其集合會擴容為原集合的1.5倍大小
如果需要將List轉換成陣列,推薦使用泛型方法
T[] toArray(T[] a)
而不是Object[] toArray()
方法如果涉及在指定位置上插入指定元素的操作,如果這種操作比較多,推薦使用LinkedList而不是使用ArrayList,因為你每次在指定的位置上插入元素會導致陣列拷貝操作。
如果List涉及到頻繁修改的時候,建議使用LinkedList,而不是使用ArrayList。
ArrayList是一個非執行緒安全類,如果需要設計到執行緒安全,請使用併發包相關的類
subList(int fromIndex, int toIndex)
方法返回的SubList
類,其中如果你對該List操作時候,原集合也會改變