JDK原始碼分析系列02---ArrayList和LinkList
ArrayList和LinkList的原始碼分析
概要
ArrayList和LinkList是常用的儲存結構,不看原始碼先分析字面意思,Array意思是陣列,可知其底層是用陣列實現的,Link意思是連結,可知是以連結串列實現,這兩種資料結構各有什麼特點呢?在實際開發中,我們要如何選擇?
1.ArrayList
- ArrayList是實現了List介面的可變陣列,即動態陣列,它不僅實現了List的可選操作,同時允許元素為null,它提供了一些方法來運算元組的大小來儲存元素,該類大致相當於Vector,只是ArrayList不是執行緒安全的.
- 每個ArrayList例項都有一個容量capacity,它是儲存元素的陣列的大小,其值至少等於list的size,當元素新增到ArrayList中,它的capacity會自動增大.
- 注意ArrayList的實現是非執行緒安全的,如果多個執行緒同時訪問ArrayList例項,且至少有一個執行緒修改了元素,那麼必須要加鎖synchronized.
- 下面是ArrayList的類關係圖,可以看出其繼承了AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable介面.
1.1基本屬性
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{ 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 = {}; /** * 儲存元素的陣列,它的大小就是ArrayList的容量,不用序列化 */ transient Object[] elementData; // non-private to simplify nested class access /** * ArrayList元素的大小 */ private int size; }
1.2構造器
/** * 構造空的list,並指定初始容量 * * @param initialCapacity 初始容量 * @throws IllegalArgumentException 如果初始容量為負數 */ 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); } } /** * 構造空的list,並指定初始容量10 */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * 構造list,包含指定集合的所有元素 * * @param c 指定的集合 * @throws NullPointerException 如果指定集合為null */ 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; } }
ArrayList list = new ArrayList(),預設容量為10,如果新增超過10個元素,就會造成list的擴容,內部會建立一個新的陣列,對效率會有影響,如果有大量的元素新增,那麼list就會頻繁擴容,效率低下.因此在開發中可以指定初始容量,細節之中可以看出一個人的基本功.
1.3常用方法
/**
* 返回list元素的數量
*/
public int size() {
return size;
}
/**
* 判斷list是否包含元素,返回true或false
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 判斷是夠包含指定的元素,返回true或false
*/
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
/**
* 返回元素第一次出現的索引index
* 如果list沒有這個元素,返回-1,否則返回第一次出現的index
* 注意:也可以查詢null的索引
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
/**
* 返回元素最後一次出現的索引index
* 如果list沒有這個元素,返回-1,否則返回最後一次出現的index
* 注意:也可以查詢null的索引
*/
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
/**
* 返回指定索引的元素
* @param index 索引
* @return 元素
* @throws IndexOutOfBoundsException, 如果index超出陣列的範圍
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
/**
* 替換指定索引的元素
*
* @param index 索引
* @param element 新元素
* @return 老元素
* @throws IndexOutOfBoundsException
*/
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
下面重點介紹add()和remove()方法,元素的新增和刪除內部是如何實現的呢?
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
ArrayList新增元素時,也就是在list的尾部新增一個元素,首先修改元素的數量+1
ensureCapacityInternal(size+1),即list的size+1,元素數量加1,詳細實現如下:
private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
元素的數量+1後,判斷minCapacity有沒有超出當前的容量,如果超出了,就要進行擴容操作grow()
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
其邏輯如下:新容量 = 老容量 + 老容量右移1位(即除以2),也就是大約1.5倍的老容量,為什麼說大約呢?因為如果老容量是偶數,那麼新容量正好等於1.5倍老容量,如果老容量是奇數11,那麼新容量是15.
容量在大,也是有限制的,最大MAX_ARRAYSIZE = Integer.MAXVALUE - 8,有21億,估計沒有人會放這麼多的資料吧.elementData[size++] = e, 然後把新元素e放在新索引的位置,也就是陣列的尾部.
ArrayList刪除元素有兩種,一種是根據索引刪除,二是直接刪除物件
-
根據索引刪除物件
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; }
numMoved = 4 - 2- 1 = 1;
System.arraycopy(elementData, 3, elementData, 2,1);
流程圖如下:
本質上是把要刪除的元素替換為它後面的元素,然後把最後一個元素賦值為null,手動GC,返回老的元素.
-
直接刪除物件
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; }
就是迴圈遍歷陣列,當元素第一次出現的時候,刪除這個元素,同時返回true,如果元素不存在,那麼陣列不改變,同時返回false.
2.LinkList
- LinkList是List介面的雙向連結串列實現,不僅實現了List的方法,同時允許元素為null
- 獲取index索引的元素時,會從連結串列的頭部或尾部進行查詢,哪邊近從哪邊開始
- 注意該實現是執行緒非安全
下面是LinkList的類關係圖,它繼承了AbstractSequentialList,實現了List, Deque, Cloneable, java.io.Serializable介面
2.1基本屬性
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
transient int size = 0;
/**
* 第一個節點,不用序列化
*/
transient Node<E> first;
/**
* 最後一個節點,不用序列化
*/
transient Node<E> last;
}
2.2構造器
/**
* 構造空的list
*/
public LinkedList() {
}
/**
* 構造指定集合的list
* @param c 集合
* @throws NullPointerException 如果集合為null
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
2.3常用方法
新增元素add()
/**
* 在LinkList的尾部新增元素
* 這個方法相當於addLast
* 返回true/false
*/
public boolean add(E e) {
linkLast(e);
return true;
}
linkLast的具體實現如下:
void linkLast(E e) { //l賦值為last節點 final Node<E> l = last; //建立新的節點e,前面元素是l,後面是null final Node<E> newNode = new Node<>(l, e, null); //把新的節點標記為last節點 last = newNode; //判斷是不是第一個節點 //如果是,新的節點就是第一個節點 if (l == null) first = newNode; else //如果不是,之前的最後一個節點後面是新的節點 l.next = newNode; size++; modCount++; }
刪除元素remove()
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
從頭部遍歷所有的節點,如果當前節點元素與要刪除的節點元素相同,那麼移除當前節點,如果LinkList包含多個要刪除的元素,那麼只會刪除index較小的那個節點.
3.總結
- ArrayList實現了RandomAccess,隨機讀取資料速度較快,它有索引index,會直接讀取陣列的索引,效率很高,但是新增和刪除元素,內部進行陣列的動態複製,效率低,如果遇到大量的資料add操作,頻繁擴容,效能更差
- LinkList新增元素就是建立了一個新的節點,只要把last節點指向最後一個元素即可,效率很高,刪除元素,就是移除指定的節點,同時調整前後的節點即可,但是對於隨機訪問元素,它需要判斷當前index離頭部和尾部哪個更近,然後去依次查詢,效率低下
- 所以對於隨機快速讀取資料,可以使用ArrayList,對於快速新增和刪除元素,可以使用LinkList