以前咱們經過分析源碼的方式學習了 ArrayList
以及 LinkedList
的使用方法。可是在分析源碼之餘,總免不了去網上查找一些相關資料,站在前人的肩膀上,發現前兩篇文章多多少少有些遺漏的地方,好比跟 ArrayList
很類似的 Vector
尚未說起過,因此本文想從面試中對於 List
相關問題出發,來填一填以前的坑,並對 List
家族中的實現類成員的異同點試着作出總結。java
Vector
是一個至關古老的 Java
容器類,始於 JDK 1.0,並在 JDK 1.2 時代對其進行修改,使其實現了 List
和 Collection
。從做用上來看,Vector
和 ArrayList
很類似,都是內部維護了一個能夠動態變換長度的數組。可是他們的擴容機制卻不相同。對於 Vector
的源碼大部分都和 ArrayList
差很少,這裏簡單看下 Vector
的構造函數,以及 Vector
的擴容機制。node
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
。bash
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
。其原理就是調用對應的迭代器的方法。dom
// 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
不是線程安全的,在不涉及多線程操做的時候 ArrayList
要比 Vector
效率高Vector
而言,除了 for 循環,高級 for 循環,迭代器的迭代方法外,還能夠調用 elements()
返回一個 Enumeration
來遍歷內部元素。咱們先回過頭來看下,這兩個 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
繼承自 Vector
,也就是 Stack 擁有 Vector
全部的增刪改查方法。可是咱們一說 Stack
確定就是指棧這中數據接口。
咱們先來看下棧的定義:
棧(stack)又名堆棧,它是一種運算受限的線性表。其限制是僅容許在表的一端進行插入和刪除運算。這一端被稱爲棧頂,相對地,把另外一端稱爲棧底。向一個棧插入新元素又稱做進棧、入棧或壓棧,它是把新元素放到棧頂元素的上面,使之成爲新的棧頂元素;從一個棧刪除元素又稱做出棧或退棧,它是把棧頂元素刪除掉,使其相鄰的元素成爲新的棧頂元素。
簡單來講,棧這種數據結構有一個約定,就是向棧中添加元素和從棧中取出元素只容許在棧頂進行,並且先入棧的元素老是後取出。 咱們能夠用數組和鏈表來實現棧的這種數據結構的操做。
通常來講對於棧有一下幾種操做:
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);
}
}
複製代碼
對於 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
的三點差別。這些知識貌似都是面試官愛問的問題,也是平時工做中容易忽略的問題。經過這篇文章作出相應總結,以備不時之需。