Java List 容器原始碼分析的補充
之前我們通過分析原始碼的方式學習了 ArrayList
以及 LinkedList
的使用方法。但是在分析原始碼之餘,總免不了去網上查詢一些相關資料,站在前人的肩膀上,發現前兩篇文章多多少少有些遺漏的地方,比如跟 ArrayList
很相似的 Vector
還沒有提及過,所以本文想從面試中對於 List
相關問題出發,來填一填之前的坑,並對 List
家族中的實現類成員的異同點試著做出總結。
- Vector 介紹及與 ArrayList 的區別
- ArrayList 與 LinkedList 的區別
- Stack 類的介紹及實現一個簡單的 Stack
- SynchronizedList 與 Vector的區別
Vector 介紹
Vector
是一個相當古老的 Java
容器類,始於 JDK 1.0,並在 JDK 1.2 時代對其進行修改,使其實現了 List
和 Collection
。從作用上來看,Vector
和 ArrayList
很相似,都是內部維護了一個可以動態變換長度的陣列。但是他們的擴容機制卻不相同。對於 Vector
的原始碼大部分都和 ArrayList
差不多,這裡簡單看下 Vector
的建構函式,以及 Vector
的擴容機制。
Vector
的建構函式可以指定內部陣列的初始容量和擴容係數,如果不指定初始容量預設初始容量為 10,但是不同於 ArrayList
的是它在建立的時候就分配了容量為10的記憶體空間,而 ArrayList 則是在第一次呼叫 add 的時候才生成一個容量為 10 陣列。
public Vector() {
this(10);//建立一個容量為 10 的陣列。
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
// 此方法在 JDK 1.2 後新增
public Vector(Collection<? extends E> c) {
elementData = c.toArray();//建立與引數集合長度相同的陣列
elementCount = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
複製程式碼
對於 Vector
的擴容機制,我們只需要看下內部的 grow 方法原始碼:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 如果我們沒有指定擴容係數,那麼 newCapacity = 2 * oldCapacity
// 如果我們指定了擴容係數,那麼每次增加指定的容量
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
複製程式碼
由上邊的方法結合我們的建構函式,我們便可知道 Vector
的需要擴容的時候,首先會判斷 capacityIncrement
即在構造的 Vector
的時候時候指定了擴容係數,如果指定了則按照指定的係數來擴大容量,擴大後新的容量為 oldCapacity + capacityIncrement
,如果沒有指定capacityIncrement
的大小,則預設擴大原來容量的一倍,這點不同於 ArrayList 的 0.5 倍長度。
對於 Vector
與 ArrayList
的區別最重要的一點是 Vector
所有的訪問內部陣列的方法都帶有synchronized
,這意味著 Vector
是執行緒安全的,而ArrayList
並沒有這樣的特性。
對於 Vector
而言,除了 for 迴圈,高階 for 迴圈,迭代的迭代方法外,還可以呼叫 elements()
返回一個 Enumeration
。
Enumeration
是一個介面,其內部只有兩個方法hasMoreElements
和 nextElement
,看上去和迭代器很相似,但是並沒迭代器的 add
remove
,只能作用於遍歷。
public interface Enumeration<E> {
boolean hasMoreElements();
E nextElement();
}
// Vector 的 elements 方法。
public Enumeration<E> elements() {
return new Enumeration<E>() {
int count = 0;
public boolean hasMoreElements() {
return count < elementCount;
}
public E nextElement() {
synchronized (Vector.this) {
if (count < elementCount) {
return elementData(count++);
}
}
throw new NoSuchElementException("Vector Enumeration");
}
};
}
複製程式碼
使用方法:
Vector<String> vector = new Vector<>();
vector.add("1");
vector.add("2");
vector.add("3");
Enumeration<String> elements = vector.elements();
while (elements.hasMoreElements()){
System.out.print(elements.nextElement() + " ");
}
複製程式碼
事實上,這個介面也是很古老的一個介面,JDK 為了適配老版本,我們可以呼叫類似 Enumeration<String> enumeration = Collections.enumeration(list);
來返回一個Enumeration
。其原理就是呼叫對應的迭代器的方法。
// Collections.enumeration 方法
public static <T> Enumeration<T> enumeration(final Collection<T> c) {
return new Enumeration<T>() {
// 構造對應的集合的迭代器
private final Iterator<T> i = c.iterator();
// 呼叫迭代器的 hasNext
public boolean hasMoreElements() {
return i.hasNext();
}
// 呼叫迭代器的 next
public T nextElement() {
return i.next();
}
};
}
複製程式碼
Vector 與 ArrayList 的比較
Vector
與ArrayList
底層都是陣列資料結構,都維護著一個動態長度的陣列。Vector
對擴容機制在沒有通過構造指定擴大系數的時候,預設增長現有陣列長度的一倍。而ArrayList
則是擴大現有陣列長度的一半長度。Vector
是執行緒安全的, 而ArrayList
不是執行緒安全的,在不涉及多執行緒操作的時候ArrayList
要比Vector
效率高- 對於
Vector
而言,除了 for 迴圈,高階 for 迴圈,迭代器的迭代方法外,還可以呼叫elements()
返回一個Enumeration
來遍歷內部元素。
ArrayList 與 LinkedList 的區別
我們先回過頭來看下,這兩個 List 的繼承體系有什麼不同:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
複製程式碼
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
複製程式碼
可以看出 LinkedList
沒有實現 RandomAccess
介面,我們知道RandomAccess
是一個空的標記介面,標誌著實現類具有隨機快速訪問的特點。那麼我們有必要重新認識下這個介面,根據 RandomAccess
的 Java API 說明:
公共介面 RandomAccess 標記介面用於List實現,以表明它們支援快速(通常是恆定時間)的隨機訪問。該介面的主要目的是允許通用演算法改變其行為,以便在應用於隨機或順序訪問列表時提供良好的效能。
我們可以意識到,隨機訪問和順序訪問之間的區別往往是模糊的。例如,如果列表很大時,某些 List 實現提供漸進的訪問時間,但實際上是固定的訪問時間,這樣的 List 實現通常應該實現這個介面。作為一個經驗法則, 如果對於典型的類例項,List實現應該實現這個介面:
for(int i = 0,n = list.size(); i <n; i ++)
list.get(ⅰ);
複製程式碼
比這個迴圈執行得更快:
for(Iterator i = list.iterator(); i.hasNext();)
i.next();
複製程式碼
上述 API 說有一個經驗法則,如果 for 遍歷某個 List 實現類的時候要比迭代器遍歷執行的快,就需要實現 RandomAccess
隨機快速訪問介面,標識這個容器支援隨機快速訪問。通過這個理論我們可以猜測,LinkedList
不具有隨機快速訪問的特性,換句話說LinkedList
的 for 迴圈遍歷要比 迭代器遍歷慢。下面我們來測試一下:
private static void loopList(List<Integer> list) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
list.get(i);
}
System.out.println(list.getClass().getSimpleName() + "使用普通for迴圈遍歷時間為" +
(System.currentTimeMillis() - startTime) + "ms");
startTime = System.currentTimeMillis();
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
iterator.next();
}
System.out.println(list.getClass().getSimpleName() + "使用iterator 迴圈遍歷時間為" +
(System.currentTimeMillis() - startTime) + "ms");
}
public static void main(String[] args){
//測試 10000個整數的訪問速度
List<Integer> arrayList = new ArrayList<Integer>(10000);
List<Integer> linkedList = new LinkedList<Integer>();
for (int i = 0; i < 10000; i++){
arrayList.add(i);
linkedList.add(i);
}
loopList(arrayList);
loopList(linkedList);
System.out.println();
}
複製程式碼
我們來看下輸出結果:
ArrayList使用普通for迴圈遍歷時間為6ms
ArrayList使用iterator 迴圈遍歷時間為4ms
LinkedList使用普通for迴圈遍歷時間為133ms
LinkedList使用iterator 迴圈遍歷時間為2ms
複製程式碼
可以看出 LinkedList
的 for迴圈的確耗費時間很長,其實這並不難理解,結合上一篇我們分析 LinkedList
的原始碼的時候,看到的 get(int index)
方法 :
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
複製程式碼
node 方法內部根據 index 和 size/2 的大小作比較,來區分是從雙連結串列的頭節點開始尋找 index 位置的節點還是從尾部開始尋找,內部仍是 for 迴圈,而基於陣列資料結構的 ArrayList
則不同了,在陣列建立的時候,就可以很方便的通過索引去獲取指定位置的元素了。所以 ArrayList
具有隨機快速訪問能力,而LinkedList
沒有。所以我們在使用 LinkedList
應儘量避免使用 for 迴圈去遍歷。
至此我們可以對 LinkedList
和 ArrayList
的區別做出總結:
-
ArrayList
是底層採用陣列結構,儲存空間是連續的。查詢快,增刪需要進行陣列元素拷貝過程,當刪除元素位置比較靠前的時候效能較低。 -
LinkedList
底層是採用雙向連結串列資料結構,每個節點都包含自己的前一個節點和後一個節點的資訊,儲存空間可以不是連續的。增刪塊,查詢慢。 -
ArrayList
和LinkedList
都是執行緒不安全的。而Vector
是執行緒安全的 -
儘量不要使用 for 迴圈去遍歷一個
LinkedList
集合,而是用迭代器或者高階 for。
Stack 介紹
由開始的繼承體系可以知道 Stack
繼承自 Vector
,也就是 Stack 擁有 Vector
所有的增刪改查方法。但是我們一說 Stack
肯定就是指棧這中資料介面。
我們先來看下棧的定義:
棧(stack)又名堆疊,它是一種運算受限的線性表。其限制是僅允許在表的一端進行插入和刪除運算。這一端被稱為棧頂,相對地,把另一端稱為棧底。向一個棧插入新元素又稱作進棧、入棧或壓棧,它是把新元素放到棧頂元素的上面,使之成為新的棧頂元素;從一個棧刪除元素又稱作出棧或退棧,它是把棧頂元素刪除掉,使其相鄰的元素成為新的棧頂元素。
簡單來說,棧這種資料結構有一個約定,就是向棧中新增元素和從棧中取出元素只允許在棧頂進行,而且先入棧的元素總是後取出。 我們可以用陣列和連結串列來實現棧的這種資料結構的操作。
一般來說對於棧有一下幾種操作:
- push 入棧
- pop 出棧
- peek 查詢棧頂
- empty 棧是否為空
Java 中的 Stack
容器是以陣列為底層結構來實現棧的操作的,通過呼叫 Vector 對應的新增刪除方法來實現入棧出站操作。
// 入棧
public E push(E item) {
addElement(item);//呼叫 Vector 定義的 addElement 方法
return item;
}
// 出棧
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);//呼叫 Vector 定義的 removeElementAt 陣列末尾的元素的方法
return obj;
}
// 查詢棧頂元素
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);//查詢陣列最後一個元素。
}
複製程式碼
上邊簡單介紹了 Java 容器中的 Stack 實現,但是事實上官方並不推薦在使用這些陳舊的集合容器類。對於棧從資料結構上而言,相對於線性表,其實現也存在,順序儲存(陣列),非連續儲存(連結串列)的實現方法。而我們上一篇文章最後看到的 LinkedList
是可以取代 Stack
來進行棧操作的。
最近在一個技術群裡,有一位美團大佬說他面試了一個位 Android 開發者,考察了一下這個 Android 開發者對於棧的理解,考察的題目是自己實現一個簡單棧,這個棧包含基本的peek ,push,pop
操作,結果不知道為何那個面試的人沒有寫出來,最終被 pass 掉了。所以在分析完 Stack 後,我決定自己手動嘗試寫一下這個面試題。我覺得我是面試官,如果回答者只寫出了出棧入棧的操作方法應該算是不及格的,面試官關注的應該是在寫 push 操作的時候有沒有考慮過 StackOverFlow
也就是棧滿的情況。
public class SimpleStack<E> {
//預設容量
private static final int DEFAULT_CAPACITY = 10;
//棧中存放元素的陣列
private Object[] elements;
//棧中元素的個數
private int size = 0;
//棧頂指標
private int top;
public SimpleStack() {
this(DEFAULT_CAPACITY);
}
public SimpleStack(int initialCapacity) {
elements = new Object[initialCapacity];
top = -1;
}
public boolean isEmpty() {
return size == 0;
}
public int size() {
return size;
}
@SuppressWarnings("unchecked")
public E pop() throws Exception {
if (isEmpty()) {
throw new EmptyStackException();
}
E element = (E) elements[top];
elements[top--] = null;
size--;
return element;
}
@SuppressWarnings("unchecked")
public E peek() throws Exception {
if (isEmpty()) {
throw new Exception("當前棧為空");
}
return (E) elements[top];
}
public void push(E element) throws Exception {
//新增之前確保容量是否滿足條件
ensureCapacity(size + 1);
elements[size++] = element;
top++;
}
private void ensureCapacity(int minSize) {
if (minSize - elements.length > 0) {
grow();
}
}
private void grow() {
int oldLength = elements.length;
// 更新容量操作 擴充為原來的1.5倍 這裡也可以選擇其他方案
int newLength = oldLength + (oldLength >> 1);
elements = Arrays.copyOf(elements, newLength);
}
}
複製程式碼
同步 vs 非同步
對於 Vector
和 Stack
從原始碼上他們在對應的增刪改查方法上都使用
synchronized
關鍵字修飾了方法,這也就代表這個方法是同步方法,執行緒安全的。而 ArrayList
和 LinkedList
並不是執行緒安全的。不過我們在介紹 ArrayList
和 LinkedList
的時候提及到了我們可以使用Collections
的靜態方法,將一個 List
轉化為執行緒同步的 List
:
List<Integer> synchronizedArrayList = Collections.synchronizedList(arrayList);
List<Integer> synchronizedLinkedList = Collections.synchronizedList(linkedList);
複製程式碼
那麼這裡又有一道面試題是這樣問的:
請簡述一下
Vector
和SynchronizedList
區別,
SynchronizedList
即Collections.synchronizedList(arrayList);
後生成的List 型別,它本身是 Collections
一個內部類。
我們來看下他的原始碼:
static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E> {
private static final long serialVersionUID = -7754090372962971524L;
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
.....
}
複製程式碼
對於 SynchronizedList
構造可以看到有一個 Object
的引數,但是看到 mutex
這個單詞應該就明白了這個引數的含義了,就是同步鎖,其實我們點選 super 方法可以看到,單個引數的建構函式鎖就是其物件自身。
SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
mutex = this;
}
SynchronizedCollection(Collection<E> c, Object mutex) {
this.c = Objects.requireNonNull(c);
this.mutex = Objects.requireNonNull(mutex);
}
複製程式碼
接下來我們看看增刪改查方法吧:
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
public int indexOf(Object o) {
synchronized (mutex) {return list.indexOf(o);}
}
public int lastIndexOf(Object o) {
synchronized (mutex) {return list.lastIndexOf(o);}
}
public boolean addAll(int index, Collection<? extends E> c) {
synchronized (mutex) {return list.addAll(index, c);}
}
//注意這裡沒加 synchronized(mutex)
public ListIterator<E> listIterator() {
return list.listIterator(); // Must be manually synched by user
}
public ListIterator<E> listIterator(int index) {
return list.listIterator(index); // Must be manually synched by user
}
複製程式碼
可以很清楚的看到,讓一個集合變成執行緒安全的,Collocations
只是包裝了引數集合的增刪改查方法,加了同步的限制。與 Vector
相比可以看出來,兩者第一個區別在於是同步方法還是同步程式碼塊,對於這兩個區別如下:
- 同步程式碼塊在鎖定的範圍上可能比同步方法要小,一般來說鎖的範圍大小和效能是成反比的。
- 同步塊可以更加精確的控制鎖的作用域(鎖的作用域就是從鎖被獲取到其被釋放的時間),同步方法的鎖的作用域就是整個方法。
由上述兩個方法看出來,``Collections.synchronizedList(arrayList);生成的同步集合看起來更高效一些,其實這種差異在 Vector 和 ArrayList上體現的很不明顯,因為其 add 方法內部實現大致相同。而從構造引數上來看
Vector不能像
SynchronizedList` 一樣指定加鎖物件。
而我們也看到了 SynchronizedList
並沒有給迭代器進行加鎖,但是翻看 Vector
的迭代器方法確實枷鎖的,所以我們在使用SynchronizedList
的的迭代器的時候需要手動做同步處理:
synchronized (list) {
Iterator i = list.iterator(); // Must be in synchronized block
while (i.hasNext())
foo(i.next());
}
複製程式碼
至此我們可以總結出 SynchronizedList
與 Vector
的三點差異:
SynchronizedList
作為一個包裝類,有很好的擴充套件和相容功能。可以將所有的List
的子類轉成執行緒安全的類。- 使用
SynchronizedList
的獲取迭代器,進行遍歷時要手動進行同步處理,而Vector
不需要。 SynchronizedList
可以通過引數指定鎖定的物件,而Vector
只能是物件本身。
總結
本文是繼 ArrayList
和 LinkedList
原始碼分析完成後,針對List
這個家族進行的補充。我們分析了
Vector
和ArrayList
的區別。ArrayList
和LinkedList
的區別,引出了RandomAccess
這個介面的定義,論證了LinkedList
使用 for 迴圈遍歷是低效的。Stack
繼承自Vector
,操作也是執行緒安全的,但是同樣比較老舊。而後分析了實現一個簡單的Stack
類的面試題。- 最後我們從執行緒安全方面總結了
SynchronizedList
與Vector
的三點差異。
這些知識貌似都是面試官愛問的問題,也是平時工作中容易忽略的問題。通過這篇文章做出相應總結,以備不時之需。