2018-08-27

Java集合:ArrayList,Vector與Stack

本文非常詳盡地介紹了Java中的三個集合類:ArrayList,Vector與Stack。

集合是Java中非常重要而且基礎(chǔ)的內(nèi)容,因為任何數(shù)據(jù)必不可少的就是該數(shù)據(jù)是如何存儲的,集合的作用就是以一定的方式組織、存儲數(shù)據(jù)。之所以把這三個集合類放在一起講解,是因為這三個集合類的底層都是數(shù)組實現(xiàn)(Stack繼承自vector)并且比較常用。

1、ArrayList

ArrayList是實現(xiàn)List接口的動態(tài)數(shù)組,所謂動態(tài)就是它的大小是可變的。實現(xiàn)了所有可選列表操作,并允許包括 null 在內(nèi)的所有元素。除了實現(xiàn) List 接口外,此類還提供一些方法來操作內(nèi)部用來存儲列表的數(shù)組的大小。

每個ArrayList實例都有一個容量,該容量是指用來存儲列表元素的數(shù)組的大小。默認初始容量為10。隨著ArrayList中元素的增加,它的容量也會不斷的自動增長。

在每次添加新的元素時,ArrayList都會檢查是否需要進行擴容操作,擴容操作帶來數(shù)據(jù)向新數(shù)組的重新拷貝,所以如果我們知道具體業(yè)務數(shù)據(jù)量,在構(gòu)造ArrayList時可以給ArrayList指定一個初始容量,這樣就會減少擴容時數(shù)據(jù)的拷貝問題。當然在添加大量元素前,應用程序也可以使用ensureCapacity操作來增加ArrayList實例的容量,這可以減少遞增式再分配的數(shù)量。

注意,ArrayList實現(xiàn)不是同步的。如果多個線程同時訪問一個ArrayList實例,而其中至少一個線程從結(jié)構(gòu)上修改了列表,那么它必須保持外部同步。所以為了保證同步,最好的辦法是在創(chuàng)建時完成,以防止意外對列表進行不同步的訪問:

List list = Collections.synchronizedList(new ArrayList(...));

1.1、底層數(shù)據(jù)結(jié)構(gòu)

ArrayList的底層是一個object數(shù)組,并且由trasient修飾。

//transient Object[] elementData; //

non-private to simplify nested class access

//ArrayList底層數(shù)組不會參與序列化,而是使用另外的序列化方式。

//使用writeobject方法進行序列化,具體為什么這么做歡迎查看我之前的關(guān)于序列化的文章

//總結(jié)一下就是只復制數(shù)組中有值的位置,其他未賦值的位置不進行序列化,可以節(jié)省空間。

// private void writeObject(java.io.ObjectOutputStream s)

// throws java.io.IOException{

//? ? ? ? ? ? // Write out element count, and any hidden stuff

// int expectedModCount = modCount;

// s.defaultWriteObject();

//? ? ? ? ? //Write out size as capacity for behavioural compatibility with clone()

//? s.writeInt(size);

?//// Write out all elements in the proper order.

1.2、增刪改查

添加元素時,首先判斷索引是否合法,然后檢測是否需要擴容,最后使用System.arraycopy方法來完成數(shù)組的復制。

這個方法無非就是使用System.arraycopy()方法將C集合(先準換為數(shù)組)里面的數(shù)據(jù)復制到elementData數(shù)組中。這里就稍微介紹下System.arraycopy(),因為下面還將大量用到該方法

。該方法的原型為:

public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)。

它的根本目的就是進行數(shù)組元素的復制。即從指定源數(shù)組中復制一個數(shù)組,復制從指定的位置開始,到目標數(shù)組的指定位置結(jié)束。

將源數(shù)組src從srcPos位置開始復制到dest數(shù)組中,復制長度為length,數(shù)據(jù)從dest的destPos位置開始粘貼。

// 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; // size++; // } //

刪除元素時,同樣判斷索引是否和法,刪除的方式是把被刪除元素右邊的元素左移,方法同樣是使用System.arraycopy進行拷貝。

// 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; // }

ArrayList提供一個清空數(shù)組的辦法,方法是將所有元素置為null,這樣就可以讓GC自動回收掉沒有被引用的元素了。

// // /** // * Removes all of the elements from this list. The list will // * be empty after this call returns. // */ // public void clear() { // modCount++; // // // clear to let GC do its work // 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); // } //

上述方法都使用了rangeCheck方法,其實就是簡單地檢查下標而已。

// private void rangeCheck(int index) { // if (index >= size) // throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); // }

1.3、modCount

// protected transient int modCount = 0;

由以上代碼可以看出,在一個迭代器初始的時候會賦予它調(diào)用這個迭代器的對象的mCount,如何在迭代器遍歷的過程中,一旦發(fā)現(xiàn)這個對象的mcount和迭代器中存儲的mcount不一樣那就拋異常

好的,下面是這個的完整解釋 Fail-Fast 機制 我們知道 java.util.ArrayList 不是線程安全的,ArrayList,那么將拋出ConcurrentModificationException,這就是所謂fail-fast策略。這一策略在源碼中的實現(xiàn)是通過 modCount 域,modCount 顧名思義就是修改次數(shù),對ArrayList 內(nèi)容的修改都將增加這個值,那么在迭代器初始化過程中會將這個值賦給迭代器的 expectedModCount。在迭代過程中,判斷 modCount 跟 expectedModCount 是否相等,如果不相等就表示已經(jīng)有其他線程修改了 ArrayList。所以在這里和大家建議,當大家遍歷那些非線程安全的數(shù)據(jù)結(jié)構(gòu)時,盡量使用迭代器

1.4、初始容量和擴容方式

初始容量是10,下面是擴容方法。

首先先取

// private static final int DEFAULT_CAPACITY = 10;擴容發(fā)生在add元素時,傳入當前元素容量加一 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true;}這里給出初始化時的數(shù)組private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};這說明:如果數(shù)組還是初始數(shù)組,那么最小的擴容大小就是size+1和初始容量中較大的一個,初始容量為10。因為addall方法也會調(diào)用該函數(shù),所以此時需要做判斷。private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity);}//開始精確地擴容private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code 如果此時擴容容量大于數(shù)組長度嗎,執(zhí)行g(shù)row,否則不執(zhí)行。 if (minCapacity - elementData.length > 0) grow(minCapacity);}

真正執(zhí)行擴容的方法grow

擴容方式是讓新容量等于舊容量的1.5被。

當新容量大于最大數(shù)組容量時,執(zhí)行大數(shù)擴容

// 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);// }

當新容量大于最大數(shù)組長度,有兩種情況,一種是溢出,拋異常,一種是沒溢出,返回整數(shù)的最大值。

private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;}

在這里有一個疑問,為什么每次擴容處理會是1.5倍,而不是2.5、3、4倍呢?通過google查找,發(fā)現(xiàn)1.5倍的擴容是最好的倍數(shù)。因為一次性擴容太大(例如2.5倍)可能會浪費更多的內(nèi)存(1.5倍最多浪費33%,而2.5被最多會浪費60%,3.5倍則會浪費71%……)。但是一次性擴容太小,需要多次對數(shù)組重新分配內(nèi)存,對性能消耗比較嚴重。所以1.5倍剛剛好,既能滿足性能需求,也不會造成很大的內(nèi)存消耗。

處理這個ensureCapacity()這個擴容數(shù)組外,ArrayList還給我們提供了將底層數(shù)組的容量調(diào)整為當前列表保存的實際元素的大小的功能。它可以通過trimToSize()方法來實現(xiàn)。該方法可以最小化ArrayList實例的存儲量。

public void trimToSize() { modCount++; int oldCapacity = elementData.length; if (size < oldCapacity) { elementData = Arrays.copyOf(elementData, size); }}

1.5、線程安全

ArrayList是線程不安全的。在其迭代器iteator中,如果有多線程操作導致modcount改變,會執(zhí)行fastfail。拋出異常。

final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException();}

2、Vector簡介

Vector可以實現(xiàn)可增長的對象數(shù)組。與數(shù)組一樣,它包含可以使用整數(shù)索引進行訪問的組件。不過,Vector的大小是可以增加或者減小的,以便適應創(chuàng)建Vector后進行添加或者刪除操作。

Vector實現(xiàn)List接口,繼承AbstractList類,所以我們可以將其看做隊列,支持相關(guān)的添加、刪除、修改、遍歷等功能。

Vector實現(xiàn)RandmoAccess接口,即提供了隨機訪問功能,提供提供快速訪問功能。在Vector我們可以直接訪問元素。

Vector 實現(xiàn)了Cloneable接口,支持clone()方法,可以被克隆。

vector底層數(shù)組不加transient,序列化時會全部復制

protected Object[] elementData;

// 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);// data = elementData.clone();// }// fields.put("elementData", data);// s.writeFields();// }

Vector除了iterator外還提供Enumeration枚舉方法,不過現(xiàn)在比較過時。

// public Enumeration elements() {// return new Enumeration() {// 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");// }// };// }//

2.1、增刪改查

vector的增刪改查既提供了自己的實現(xiàn),也繼承了abstractList抽象類的部分方法。

下面的方法是vector自己實現(xiàn)的。

//// public synchronized E elementAt(int index) {// if (index >= elementCount) {// throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);// }//// return elementData(index);// }////// public synchronized void setElementAt(E obj, int index) {// if (index >= elementCount) {// throw new ArrayIndexOutOfBoundsException(index + " >= " +// elementCount);// }// elementData[index] = obj;// }//// public synchronized void removeElementAt(int index) {// modCount++;// if (index >= elementCount) {// throw new ArrayIndexOutOfBoundsException(index + " >= " +// elementCount);// }// else if (index < 0) {// throw new ArrayIndexOutOfBoundsException(index);// }// int j = elementCount - index - 1;// if (j > 0) {// System.arraycopy(elementData, index + 1, elementData, index, j);// }// elementCount--;// elementData[elementCount] = null; /* to let gc do its work */// }// public synchronized void insertElementAt(E obj, int index) {// modCount++;// if (index > elementCount) {// throw new ArrayIndexOutOfBoundsException(index// + " > " + elementCount);// }// ensureCapacityHelper(elementCount + 1);// System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);// elementData[index] = obj;// elementCount++;// }//// public synchronized void addElement(E obj) {// modCount++;// ensureCapacityHelper(elementCount + 1);// elementData[elementCount++] = obj;// }

2.2、初始容量和擴容

擴容方式與ArrayList基本一樣,但是擴容時不是1.5倍擴容,而是有一個擴容增量。

// protected int elementCount;// protected int capacityIncrement;////// }// public Vector() {// this(10);// }

capacityIncrement:向量的大小大于其容量時,容量自動增加的量。如果在創(chuàng)建Vector時,指定了capacityIncrement的大小;則,每次當Vector中動態(tài)數(shù)組容量增加時>,增加的大小都是capacityIncrement。如果容量的增量小于等于零,則每次需要增大容量時,向量的容量將增大一倍。

// public synchronized void ensureCapacity(int minCapacity) {// if (minCapacity > 0) {// modCount++;// ensureCapacityHelper(minCapacity);// }// }// private void ensureCapacityHelper(int minCapacity) {// // overflow-conscious code// if (minCapacity - elementData.length > 0)// grow(minCapacity);// }//// private void grow(int minCapacity) {// // overflow-conscious code// int oldCapacity = elementData.length;// 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);// }

2.3、線程安全

vector大部分方法都使用了synchronized修飾符,所以他是線層安全的集合類。

3、Stack

在Java中Stack類表示后進先出(LIFO)的對象堆棧。棧是一種非常常見的數(shù)據(jù)結(jié)構(gòu),它采用典型的先進后出的操作方式完成的。每一個棧都包含一個棧頂,每次出棧是將棧頂?shù)臄?shù)據(jù)取出。

Stack通過五個操作對Vector進行擴展,允許將向量視為堆棧。這個五個操作如下:

empty()測試堆棧是否為空。peek()查看堆棧頂部的對象,但不從堆棧中移除它。pop()移除堆棧頂部的對象,并作為此函數(shù)的值返回該對象。push(E item)把項壓入堆棧頂部。search(Object o)返回對象在堆棧中的位置,以 1 為基數(shù)。

Stack繼承Vector,他對Vector進行了簡單的擴展:

public class Stack?extends Vector

Stack的實現(xiàn)非常簡單,僅有一個構(gòu)造方法,五個實現(xiàn)方法(從Vector繼承而來的方法不算與其中),同時其實現(xiàn)的源碼非常簡單

/** * 構(gòu)造函數(shù) */public Stack() {}/** * push函數(shù):將元素存入棧頂 */public E push(E item) { // 將元素存入棧頂。 // addElement()的實現(xiàn)在Vector.java中 addElement(item); return item;}/** * pop函數(shù):返回棧頂元素,并將其從棧中刪除 */public synchronized E pop() { E obj; int len = size(); obj = peek(); // 刪除棧頂元素,removeElementAt()的實現(xiàn)在Vector.java中 removeElementAt(len - 1); return obj;}/** * peek函數(shù):返回棧頂元素,不執(zhí)行刪除操作 */public synchronized E peek() { int len = size(); if (len == 0) throw new EmptyStackException(); // 返回棧頂元素,elementAt()具體實現(xiàn)在Vector.java中 return elementAt(len - 1);}/** * 棧是否為空 */public boolean empty() { return size() == 0;}/** * 查找“元素o”在棧中的位置:由棧底向棧頂方向數(shù) */public synchronized int search(Object o) { // 獲取元素索引,elementAt()具體實現(xiàn)在Vector.java中 int i = lastIndexOf(o); if (i >= 0) { return size() - i; } return -1;}

Stack的源碼很多都是基于Vector,所以這里不再累述

4、區(qū)別

ArrayList的優(yōu)缺點

從上面的幾個過程總結(jié)一下ArrayList的優(yōu)缺點。ArrayList的優(yōu)點如下:

1、ArrayList底層以數(shù)組實現(xiàn),是一種隨機訪問模式,再加上它實現(xiàn)了RandomAccess接口,因此查找也就是get的時候非常快2、ArrayList在順序添加一個元素的時候非常方便,只是往數(shù)組里面添加了一個元素而已

不過ArrayList的缺點也十分明顯

1、刪除元素的時候,涉及到一次元素復制,如果要復制的元素很多,那么就會比較耗費性能2、插入元素的時候,涉及到一次元素復制,如果要復制的元素很多,那么就會比較耗費性能因此,ArrayList比較適合順序添加、隨機訪問的場景。

ArrayList和Vector的區(qū)別

ArrayList是線程非安全的,這很明顯,因為ArrayList中所有的方法都不是同步的,在并發(fā)下一定會出現(xiàn)線程安全問題。那么我們想要使用ArrayList并且讓它線程安全怎么辦?一個方法是用Collections.synchronizedList方法把你的ArrayList變成一個線程安全的List,比如:

List synchronizedList = Collections.synchronizedList(list);synchronizedList.add("aaa");synchronizedList.add("bbb");for (int i = 0; i < synchronizedList.size(); i++){ System.out.println(synchronizedList.get(i));}

另一個方法就是Vector,它是ArrayList的線程安全版本,其實現(xiàn)90%和ArrayList都完全一樣,區(qū)別在于:

1、Vector是線程安全的,ArrayList是線程非安全的2、Vector可以指定增長因子,如果該增長因子指定了,那么擴容的時候會每次新的數(shù)組大小會在原數(shù)組的大小基礎(chǔ)上加上增長因子;如果不指定增長因子,那么就給原數(shù)組大小*2,源代碼是這樣的:

int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容