這篇文章來自個人我的博客java
最近在複習Java基礎,感受又學到了好多,集合做爲Java的一大重點,以前是看着《Java編程思想》學的,此次恰好配合着源碼從新學一遍,首先學的是ArrayList,順便作點總結(迭代器無關):編程
- ArrayList的基本概念
- ArrayList的源碼剖析(常量,構造器,經常使用方法)
關於ArrayList的源碼,至少要寫三四篇纔夠吧,這源碼但是有足足1500行左右(一半是註釋),慢慢積累吧數組
剛開始學的時候管它叫動態數組,由於它的大小是根據數據量而自動變化的,它的結構是基於動態數組(Dynamic array)瀏覽器
我在IDEA中之間右鍵點擊ArrayList查看了源碼,先將註釋做爲HTML放在瀏覽器中看看:bash
把它稱做Resizable-array就能夠看出它的性質了多線程
接下來講說這一整頁的大概內容:dom
它實現了List接口,實現了全部List的方法,它能夠容納全部元素類型(包括null),ArrayList除了不是多線程同步以外,其他的內容都與Vector大體相同測試
size, isEmpty, get, set, iterator 和 listIterator 操做用時是常數級別的,add 操做耗時是和插入位置有關,除此以外,其餘操做的時間是線性增加的,下面會作個實驗驗證一下ui
Arraylist有着容量的概念,容量隨着數據量的增加而增加,也能夠在直接定義這個容器的大小,使用ensureCapacity操做,可以一次增加所須要的容量,提升效率,避免一個個添加從而不斷增加容量this
劃重點,上面說過這個容器不是同步的,若是在多線程中使用,有一個線程要改變容器的結構時,就須要在容器外部進行同步,官方推薦的作法是用同步的容器將它包裝起來:
List list = Collections.synchronizedList(new ArrayList(...));
複製代碼
測試一下第2點中的運行時間:
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
long startTime = 0, endTime = 0;
//插入操做
startTime = System.currentTimeMillis();
//須要大一點的數據量才能算出時間
for (int i = 0; i < 100000; i++) {
list.add(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
//
startTime = System.currentTimeMillis();
for (int i = 100000; i < 200000; i++) {
list.add(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
list.clear();
}
}
複製代碼
從十萬的位置開始插入,多用了一毫秒,說明運行時間和插入位置是有關的
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
long startTime = 0, endTime = 0;
String index;
//插入操做
startTime = System.currentTimeMillis();
for (int i = 0; i < 2000000; i++) {
list.add(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("插入兩百萬數據時間: ");
System.out.println(endTime - startTime);
startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
index = list.get(i);
}
endTime = System.currentTimeMillis();
System.out.println("獲得前一百萬數據時間: ");
System.out.println(endTime - startTime);
startTime = System.currentTimeMillis();
for (int i = 1000000; i < 2000000; i++) {
index = list.get(i);
}
endTime = System.currentTimeMillis();
System.out.println("獲得前一百萬數據時間: ");
System.out.println(endTime - startTime);
list.clear();
}
}
複製代碼
get操做時間在相同數據量的前提下是同樣的
其餘的都是用差很少的方法來驗證,就不作了
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
複製代碼
說明這個類是實現了幾個接口:
第一個不說,說說後面三個,這三個接口都是空的,只是用來講明:
支持快速隨機訪問
可以使用Object.clone()方法
可序列化
常量部分在方法中會使用,直接截取源碼部分:
private static final int DEFAULT_CAPACITY = 10;
複製代碼
private static final Object[] EMPTY_ELEMENTDATA = {};
複製代碼
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
複製代碼
transient Object[] elementData;
複製代碼
private int size;
複製代碼
構造器共有三種:
public ArrayList(int initialCapacity) {} public ArrayList() {} public ArrayList(Collection<? extends E> c) {}
用到上面所說的常量中的兩個
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);
}
}
複製代碼
這個簡單,直接按照默認容量10,定義一個大小爲10的數組
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
複製代碼
public ArrayList(Collection<? extends E> c) {
//先將參數轉爲數組類型,toArray()方法重載自AbstractCollection<E>和List<E>類
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//這是一個官方的bug,說不必定可以返回Object[]類
if (elementData.getClass() != Object[].class)
//若是真出現了這個bug,就強制轉回Object[]類
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
//若是傳入的容器參數的容量爲0,就替換爲空數組
this.elementData = EMPTY_ELEMENTDATA;
}
}
複製代碼
這裏先給出的都是日常使用的方法,不包括迭代器的,關於迭代器的內容是須要單獨拿一篇來講的
先從增刪改查 下手,最後會有一個Demo
增長元素有好幾種,末尾添加,定點添加,以及直接添加一整個容器的元素
末尾添加:
//直接在列表末尾添加元素
public boolean add(E e) {
//先擴容
ensureCapacityInternal(size + 1); // Increments modCount!!
//把下標爲size的位置的值設爲e,而後size自增
elementData[size++] = e;
return true;
}
複製代碼
末尾添加其餘容器元素:
public boolean addAll(Collection<? extends E> c) {
//轉換爲數組
Object[] a = c.toArray();
int numNew = a.length;
//擴容
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
複製代碼
定點添加:
這裏有一個檢查數組下標是否越界的方法,須要先說明一下:
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
複製代碼
關於異常信息,源碼是這樣的:
//在指定位置添加元素,可能拋出數組越界,數組存儲異常和空指針異常
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;
//數組大小加1
size++;
}
複製代碼
關於System.arraycopy(),我查了一下源碼中的解釋:
這裏面的參數就是:1. 原始數組 2. 原始位置 3. 目標數組 4. 目標位置 5. 須要移動的元素數量
其實上面添加的方法就是在同一個數組裏面移動元素
而後還有直接添加其餘容器的元素到指定位置
//在指定位置添加其餘容器的元素,可能會拋出空指針異常和數組下標越界異常
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
//將容器轉換爲數組形式
Object[] a = c.toArray();
int numNew = a.length;
//按照轉換後的數組長度來擴容
ensureCapacityInternal(size + numNew); // Increments modCount
//先肯定要移動幾個數字
int numMoved = size - index;
//若是要移動,日後移
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//而後將其餘容器的元素複製過來
System.arraycopy(a, 0, elementData, index, numNew);
//改變大小
size += numNew;
//判斷列表結構是否改變
return numNew != 0;
}
複製代碼
關於刪除,有定點刪除,有刪除特定元素,批量刪除,還有清空列表,思想就是把要刪除的位置的值設爲NULL,而後讓垃圾回收器處理
定點刪除:
首先仍是要給出一個檢查數組是否越界的方法,和上面的相似:
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
複製代碼
//在指定位置刪除元素,
public E remove(int index) {
rangeCheck(index);
modCount++;
//獲得要刪除的元素
E oldValue = elementData(index);
//須要移動的元素數量
int numMoved = size - index - 1;
if (numMoved > 0)
//這些元素所有前移一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
//返回刪除的值
return oldValue;
}
複製代碼
刪除特定元素
須要先說明一個私有方法,下面會用到:
//快速刪除,不檢查是否有數組下標越界
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//先刪除,再減值
elementData[--size] = null; // clear to let GC do its work
}
複製代碼
public boolean remove(Object o) {
//若是沒找到特定元素,數組就不變化
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//遍歷數組
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
複製代碼
批量刪除:
這個方法是私有的,實現它的有另外兩個方法 removeAll()和retainAll(),批量刪除方法中的布爾類型參數就和這兩個實現方法有關:
傳入一個容器,若是complement爲true,就只保留和容器元素 相同的元素,若是complement爲false,就刪除和容器元素相同的元素
這個測試中,第一個complement爲true,第二個complement爲false:
private boolean batchRemove(Collection<?> c, boolean complement) {
//複製數組來存放篩選後的數據
final Object[] elementData = this.elementData;
//r用來遍歷數組,w用來給數組賦值
int r = 0, w = 0;
//判斷結構是否改變
boolean modified = false;
try {
//遍歷
for (; r < size; r++)
//若是傳入的容器含有和原先數組相同的元素,就向新數組賦值
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
//若是拋出異常,將r位置以後的元素複製到w位置以後
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
//在篩選完以後,若是w位置後面有空位,就清理掉
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
//結構改變了
modified = true;
}
}
return modified;
}
複製代碼
接下來就是調用批量刪除的方法了:
刪除容器元素:
//可能拋出強制類型轉換異常和空指針異常
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
複製代碼
保留容器元素:
//也可能拋出一樣的異常
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
複製代碼
清空列表:
removeAll()方法也能夠清空列表,只不過效率低,仍是推薦使用下面這個:
public void clear() {
modCount++;
// clear to let GC do its work
//遍歷數組,設值爲NULL
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
複製代碼
可能會拋出數組下標越界的錯誤
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
複製代碼
可能會拋出數組下標越界的異常
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
複製代碼