本文對Java中的Vector集合的源碼進行了深度解析,包括各種方法、特有的Enumeration迭代器機制,並且給出了Vector和ArrayList的區別以及使用建議。
1 Vector的概述
public class Vector
extends AbstractList
implements List, RandomAccess, Cloneable, Serializable
Vector,來自於JDK1.0 的古老集合類,繼承自 AbstractList,實現了 List 接口 ,底層是數組結構,元素可重複,有序(存放順序),支持下標索引訪問,允許null元素。
該類當中所有方法的實現都是同步的,方法採用了synchronized修飾,數據安全,效率低!可以看成ArrayList的同步版本,但是並不完全相同,比如迭代器。
實現了 RandomAccess標誌性接口,這意味着這個集合支持 快速隨機訪問 策略,那麼使用傳統for循環的方式遍歷數據會優於用迭代器遍歷數據,即使用get(index)方法獲取數據相比於迭代器遍歷更加快速!
還實現了Cloneable、Serializable兩個標誌性接口,所以Vector支持克隆、序列化。
2 Vector的源碼解析
Vector的方法的原理和ArrayList非常相似,因此如果瞭解AarrayList那麼理解Vector將會很簡單!對於某些方法,本文在ArrayList集合中會有詳細介紹,因此強烈建議先學習ArrayList:Java的ArrayList集合源碼深度解析以及應用介紹。
Vector的底層數據結構就是一個數組,數組元素的類型爲Object類型,對Vector的所有操作底層都是基於數組的。
初始容量:
調用空構造器時,立即初始化爲10個容量的數組,也可以指定初始容量。
加載因子:
1,即存放數據時,如果存放數據後的容量大於底層數組的容量,那麼首先擴容。
擴容增量:
新容量默認增加原容量的1倍,但是也可以在構造器指定擴容時的容量增量!如果新容量還是小於最小容量,則新容量還是等於最小容量!
2.1 主要類屬性
相比於ArrayList,屬性還是很簡單的,多了一個capacityIncrement。
/**
* 存放元素的底層容器,就是一個數組,當前數組的長度就是Vector的容量
*/
protected Object[] elementData;
/**
* 元素的個數
*/
protected int elementCount;
/**
* 當vector的大小大於其容量時,其容量擴充的大小,即容量增量。如果容量增量小於或等於零,則每次需要增長時vector的容量都會加倍。
*/
protected int capacityIncrement;
2.2 構造器與初始化容量
2.2.1 Vector()
構造一個空集合,使其內部數據數組的大小初始化爲10,容量增量爲零。其源碼爲:
public Vector() {
this(10);
}
可以看到調用了另外一個構造方法。
2.2.2 Vector(int initialCapacity)
構造具有指定初始容量並且其容量增量等於零的空集合。
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
可以看到調用了另外一個構造方法。
2.2.3 Vector(int initialCapacity, int capacityIncrement)
構造具有指定的初始容量和容量增量的空集合。
public Vector(int initialCapacity, int capacityIncrement) {
//調用父類構造器
super();
//檢查初始化容量,如果小於0就拋出異常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
//初始化指定容量的數組
this.elementData = new Object[initialCapacity];
//初始化容量增量
this.capacityIncrement = capacityIncrement;
}
2.2.4 Vector(Collection<? extends E> c)
構造一個包含指定 collection 中的元素的集合,這些元素按其 collection 的迭代器返回元素的順序排列。
public Vector(Collection<? extends E> c) {
//獲取集合的元素數組,賦值給elementData
elementData = c.toArray();
//獲取此時集合的容量,賦值給elementCount
elementCount = elementData.length;
if (elementData.getClass() != Object[].class)
//如果新加入的數組不是object[]類型的數組,則轉換爲object[]類型的數組
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
2.3 add方法與擴容機制
add方法的原理基本和ArrayList的add方法一致,在ArrayList的文章中有詳細介紹,這裏不再贅述。Java的ArrayList集合源碼深度解析以及應用介紹。
/**
* 加了synchronized的同步方法
* @param e 需要添加的元素
* @return 添加成功,返回true
*/
public synchronized boolean add(E e) {
//集合結構修改次數自增1
modCount++;
//確保數組容量夠用,最小容量爲當前元素個數+1
ensureCapacityHelper(elementCount + 1);
//添加元素
elementData[elementCount++] = e;
return true;
}
private void ensureCapacityHelper(int minCapacity) {
//如果最小容量減去數組的長度的值大於0
if (minCapacity - elementData.length > 0)
//那麼有可能是需要擴容,或者數組長度移除
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
//獲取老的容量
int oldCapacity = elementData.length;
//如果容量增量大於0,增新容量爲老容量加上容量增量,否則新容量是老容量的兩倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
//如果此時新容量減去老容量的值還是小於0,那麼新容量等於最小容量,或者數組長度溢出
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果此時新容量減去建議最大容量的值還是小於0,那麼新容量等於最小容量,或者數組長度溢出
//此時需要拋出異常或者重新分配新容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
//拋出異常或者重新分配新容量
newCapacity = hugeCapacity(minCapacity);
//數組拷貝
elementData = Arrays.copyOf(elementData, newCapacity);
}
/**
* 和arraylist是同樣的邏輯
* @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;
}
2.4 addAll方法
public synchronized boolean addAll(Collection<? extends E> c) {
//結構改變+1
modCount++;
//獲取加入集合的元素數組
Object[] a = c.toArray();
//獲取加入的元素的數量
int numNew = a.length;
//確保容量能夠容納這些元素
ensureCapacityHelper(elementCount + numNew);
//元素的拷貝存放
System.arraycopy(a, 0, elementData, elementCount, numNew);
//元素數量增加
elementCount += numNew;
//如果此集合由於調用而更改了結構,即numNew>0,則返回 true
return numNew != 0;
}
2.5 remove方法
public E remove(int index)
移除此集合中指定索引位置上的元素,向左移動所有後續元素(將其索引減1),並返回此集合中移除的元素。
從源碼中可以看到,需要調用System.arraycopy() 將刪除元素 index+1 後面的元素都複製到 index 位置上,該操作的時間複雜度爲 O(N),可以看出 ArrayList 刪除元素和擴容一樣,代價是非常高的。
remove的源碼,還是比較簡單的:
public synchronized E remove(int index) {
modCount++;
//檢查要移除的元素索引是否越界(上界)
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
//獲取該索引處的元素
E oldValue = elementData(index);
//要移動的數據長度elementCount-(index + 1) 最小值0最大值size-1
int numMoved = elementCount - index - 1;
if (numMoved > 0)
//將index+1後面的列表對象前移一位,該操作將會覆蓋index以及之後的元素,相當於刪除了一位元素
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 數組前移一位,size自減-,空出來的位置(原數組的有效數據的最後一位)置null,原來的具體的對象的銷燬由Junk收集器負責
elementData[--elementCount] = null;
//返回被移除的元素
return oldValue;
}
2.6 get方法
public E get(int index)
返回此集合中指定索引位置上的元素。
public synchronized E get(int index) {
//檢查要獲取的元素索引是否越界(上界)
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
//返回索引處的元素
return elementData(index);
}
2.7 set方法
public E set(int index,E element)
用指定的元素替代此列表中指定索引位置上的元素。
public synchronized E set(int index, E element) {
//檢查要設置的元素索引是否越界(上界)
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
//獲取舊的值
E oldValue = elementData(index);
//替換值
elementData[index] = element;
//返回舊值
return oldValue;
}
2.8 clone方法
返回的是一個全新的Vector實例對象,但是其elementData,也就是存儲數據的數組,存儲的對象還是指向了舊的Vector存儲的那些對象。也就是Vector這個類實現了深拷貝,但是對於存儲的對象還是淺拷貝。
public synchronized Object clone() {
try {
@SuppressWarnings("unchecked")
//克隆集合對象
Vector<E> v = (Vector<E>) super.clone();
//克隆內部數組,導致雖然數組的引用不一樣,但是但是數組內部的相同索引處的元素引用指向同一個堆內存地址,即還是同一個對象
v.elementData = Arrays.copyOf(elementData, elementCount);
//設置新集合的modCount爲0
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
2.9 序列化
同ArrayList一樣,Vector也有自己的writeObject方法,區別是Vector的內部數組被全部序列化存儲了,包括沒有使用道的部分,而ArrayList的內部數組沒有全部進行序列化,只是序列化了儲存了元素的部分。
並且Vector沒有實現readObject方法,那麼將會按照默認的方式進行反序列化。
writeObject:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
final java.io.ObjectOutputStream.PutField fields = s.putFields();
final Object[] data;
synchronized (this) {
fields.put("capacityIncrement", capacityIncrement);
fields.put("elementCount", elementCount);
//直接對整個數組進行了序列化,相比於ArrayList代碼更加簡單,但是更佔用空間
data = elementData.clone();
}
fields.put("elementData", data);
s.writeFields();
}
3 迭代器
3.1 Enumeration迭代器的概述
由於Vector屬於List接口集合體系,因此具有通用的iterator 和 listIterator迭代器(關於這兩個迭代器在ArrayList文章部分有詳細講解,這裏不再贅述)。但是我們知道List接口是JDK1.2的時候加進來的,但是Vector在JDK1.0的時候就出現了,因此Vector還具有自己獨有迭代器Enumeration,也被稱爲“枚舉”!
Enumeration原本是一個接口,實現Enumeration接口的對象,又稱爲枚舉對象。通過它的api方法可以遍歷Vector集合的元素,但是該接口不屬於集合體系,並且JDK的API給出瞭如下建議:此接口的功能與Iterator接口的功能是重複的。此外,Iterator 接口添加了一個可選的移除操作,並使用較短的方法名。新的實現應該優先考慮使用 Iterator 接口而不是 Enumeration 接口。
由於Enumeration迭代器比較古老,因此功能也很簡略,並沒有iterator 和 listIterator迭代器的快速失敗機制,因此可能出現一些不會拋出異常的“異常情況”,比如下面的代碼:
/**
* Enumeration迭代器的死循環
*/
@Test
public void test2() {
Vector<Integer> vector = new Vector<>(Collections.singletonList(1));
//獲取自己的迭代器Enumeration
Enumeration elements = vector.elements();
int j = 0;
//是否存在更多元素
while (elements.hasMoreElements()) {
//內部採用集合的方法添加元素,如果是iterator 和 listIterator 迭代器,那麼會馬上拋出ConcurrentModificationException異常
//但是由於Enumeration迭代器,沒有這個功能,因此會導致死循環,直到OOM
vector.add(j++);
//獲取下一個元素
Object o = elements.nextElement();
//打印元素
System.out.println(o);
}
}
上面的代碼將會造成死循環!
3.2 Enumeration迭代器的實現
public Enumeration elements()
返回此集合的枚舉。返回的 Enumeration 對象將具有此集合中的所有元素。第一項爲索引0處的元素,然後是索引1處的元素,依此類推。
下面是源碼:
public Enumeration<E> elements() {
//和iterator 與 listIterator迭代器不一樣
//Enumeration迭代器甚至簡陋得"沒有自己的內部類實現"
//這裏返回的就是一個匿名內部類,即返回一次實現一次。
return new Enumeration<E>() {
//用於遍歷元素個數的計數
int count = 0;
//是否還存在元素 通過比較count和elementCount的值
//如果遍歷的元素個數小於外部集合的元素個數,那就說明有元素,可以獲取,返回true
public boolean hasMoreElements() {
return count < elementCount;
}
//返回下一個元素
public E nextElement() {
synchronized (Vector.this) {
//如果遍歷的元素個數小於外部集合的元素個數,那就說明有元素,可以獲取
if (count < elementCount) {
//返回該遍歷的元素個數索引處的元素,同時遍歷的元素個數自增1
return elementData(count++);
}
}
throw new NoSuchElementException("Vector Enumeration");
}
};
}
我們看到,Enumeration的實現非常簡單,返回的是一個匿名內部類,並沒有“併發修改”的檢測,並且內部只提供了兩個方法hasMoreElements()和nextElement()方法。並沒有add、remove等修改集合元素方法,功能更加簡陋!
boolean hasMoreElements()
當且僅當此枚舉對象至少還包含一個可提供的元素時,才返回 true;否則返回 false。
E nextElement()
如果此枚舉對象至少還有一個可提供的元素,則返回此枚舉的下一個元素。
3.3 分析Enumeration迭代器的死循環
我們來分析上面的案例是如何導致死循環的!
首先獲取迭代器,該迭代器和外部集合共用一個內部數組。
進入循環,hasMoreElements()判斷是否存在下一個元素,由於原集合存在一個元素,因此count=0 < elementCount=1,即返回true。
然後循環體內部,首先對外部集合添加了元素,這會導致elementCount++,變成2。繼續nextElement()方法,同樣判斷存在元素,因此返回elementData(0++),即返回elementData[0],然後count自增一變成1,第一次循環結束。
第二次循環,這是hasMoreElements()發現count=1 < elementCount=2,即返回true。
然後循環體內部,又首先對外部集合添加了元素,這會導致elementCount++,變成3。繼續nextElement()方法,同樣判斷存在元素,因此返回elementData(1++),即返回elementData[1],然後count自增一變成2,第二次循環結束。
我們發現,無論怎麼循環,count始終小於elementCount,這就是導致死循環的原因,在開發過程如果要使用Vector集合,那麼要避免這種情況,並且最好是不去使用老舊的Enumeration迭代器!
3.4 使用枚舉遍歷ArrayList?
/**
1. 使用枚舉遍歷ArrayList
*/
@Test
public void test3() {
ArrayList<String> al = new ArrayList<>(Arrays.asList("b", "a", "s", "c", "11", null));
Vector<String> v = new Vector<>(al);
Enumeration<String> elements = v.elements();
while (elements.hasMoreElements()) {
String s = elements.nextElement();
System.out.println(s);
}
}
4 Vector的總結和使用
Vector和ArrayList的異同點:
相同點:
- 底層是數組結構,元素可重複,有序(存儲順序),支持下標索引訪問,允許null元素。
不同點:
- Vector:在JDK1.0固有的集合類。該類當中所有方法的實現都是:線程同步,數據安全,效率低。在JDK1.2被加入到集合框架當中。
- ArrayList:在JDK1.2新添加的集合類。該類當中所有的方法的實現都是:線程異步,數據不安全,效率高。
- 擴容:Vector每次擴容變成原來的2倍(也可以通過構造函數設置容量增量),而 ArrayList 是 每次擴容變成原來的1.5 倍左右。當然對於它們兩個都是:如果計算出的擴容後的容量還是小於最小容量,那麼擴容後的容量就是最小容量。
Vector使用建議:
Vector集合的方法都加了snchronized,因此效率較低,如果想要使用線程安全的List集合,那麼可以使用Collections.synchronizedList()得到一個線程安全的List,實際上它內部使用snchronized關鍵字修飾的代碼塊,效率也很一般,推薦使用JUC包下的 CopyOnWriteArrayList類,效率比較高,但是它的“寫時複製”機制可能會造成數據不同步。總之,面試中,Vector被問到的概率比較小,而在開發過程中,不推薦使用Vector集合。
我們後續將會介紹的更多集合,比如LinkedList、TreeMap、HashMap,LinkedHashMap等基本集合以及JUC包中的高級併發集合。如果想學習集合源碼的關注我的更新!
如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!