Java中的集合(五)繼承Collection的List接口
一、List接口簡介
List是有序的Collection的,此接口能夠精確的控制每個元素插入的位置。用戶能夠根據索引(元素在List接口的中位置)訪問List中的元素,類似於Java中的數組。
List接口有如下特點:
-
- 有序的集合。存儲順序和獲取元素的順序都是一致的;
- 可重復。允許存儲重復的元素;
- 提供索引。提供一些索引的方法,供用戶操作。
二、List類圖結構
通過上面類圖可用看出,List接口下有4個實現類,分別為:LinkedList、ArrayList、Vector和Stack。
三、List接口中帶索引的方法(特有):
需要注意的是:操作索引時,一定要注意防止索引越界異常提示。
- IndexOutOfBoundsException:索引越界異常,集合會報;
- ArrayIndexOutOfBoundsException:數組索引越界異常;
- StringIndexOutOfBoundsException:字符串索引越界異常。
四、ArrayList
(一)、簡介、繼承結構
ArrayList是List接口的實現類,也是最常用的集合類。底層數據結構是一個可變的動態數組,它允許任何符合規則的元素插入(包括null)。每一個ArrayList都有一個初始容量(10),該容量代表了數組的大小。隨着容器中的元素不斷增加,容器的大小也會隨着增加。在每次向容器中增加元素的同時都會進行容量檢查,當快溢出時,就會進行擴容操作。
如果我們明確所插入元素的多少,最好指定一個初始容量值,避免過多的進行擴容操作而浪費時間、效率。
1、ArrayList繼承結構
通過結構圖可以看出,ArrayList繼承自AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable這些接口。
-
- 繼承AbstractList,實現了List接口:它是一個動態數組,提供了相關添加,刪除,修改和遍歷等功能;
- 實現RandomAccess接口:提供隨機訪問的能力;RandmoAccess是Java中用來被List接口實現,為List提供快速訪問功能的。在ArrayList中,可以通過元素的序號快速獲取元素對象,這就是快速隨機訪問。
- 實現Cloneable接口:覆蓋函數clone(),可以被克隆;
- 實現Serializable接口:支持序列化和反序列化,可以通過序列化傳輸數據。
2、ArrayList的特性:
- 增刪慢,查詢快(底層數據結構是數組);
- 效率高,線程不安全的(非同步);
- 擅長隨機訪問(實現了RandomAccess接口);
(二)、ArrayList構造方法
(三)、ArrayList的兩個重要的屬性:elementData 和 size
1、elementData:Object[]的數組,保存了添加到ArrayList中的元素。
- 通過ArrayList()初始化ArrayList時,elementData的大小為默認值10,即Object[10];
- 通過ArrayList(int initialCapacity)初始化ArrayList時,elementData的大小為指定值initialCapacity,即Object[initialCapacity];
- ArrayList(Collection<? extends E> c)初始化ArrayList時,elementData的大小為參數集合的大小,即Object[c.length];
2、size:動態數組的實際大小
(四)、ArrayList的遍歷方式
1、Iterator迭代器遍歷方式
ArrayList類中封裝了Iterator接口,調用Iterator()方法獲得Iterator對象,Iterator對象調用hashNext()、next()方法進行迭代遍歷。

1 Integer value = null; 2 Iterator iter = list.iterator(); 3 while (iter.hasNext()) { 4 value = (Integer)iter.next(); 5 }
2、隨機訪問。通過索引值遍歷
由於ArrayList實現了RandomAccess接口,它支持通過索引值去隨機訪問元素,通過調用public E get(int index)方法遍歷。

1 Integer value = null; 2 for (int i=0; i<list.size(); i++) { 3 value = (Integer)list.get(i); 4 }
3、for-each循環遍歷

1 Integer value = null; 2 for (Integer integ : list) { 3 value = integ; 4 }
4、三種遍歷方式的性能
遍歷ArrayList時,在性能方面:隨機訪問 , 通過索引值遍歷> for-each遍歷 > Iterator迭代器遍歷。
(五)、使用toArray()異常
ArrayList提供了兩個“toArray“方法,分別是: Object[] toArray() 和 <T> T[] toArray(T[] contents) 。
當我們調用“toArray()”方法時會拋““java.lang.ClassCastException”異常,但是調用"toArray(T[] t)"可以返回正常的T[]。
調用“toArray()”方法時會拋“java.lang.ClassCastException”異常是因為返回值類型是“Object[]”,Object是Java中最頂層的對象,且Java不支持對象向下轉型,比如當Object[]轉成Integer[]時,就會拋“java.lang.ClassCastException”異常。
使用另一個<T> T[] toArray(T[] contents)可以解決該異常問題。
調用 toArray(T[] contents) 返回T[]的可以通過以下幾種方式實現。

1 // toArray(T[] contents)調用方式一 2 public static Integer[] vectorToArray1(ArrayList<Integer> v) { 3 Integer[] newText = new Integer[v.size()]; 4 v.toArray(newText); 5 return newText; 6 } 7 8 // toArray(T[] contents)調用方式二。最常用! 9 public static Integer[] vectorToArray2(ArrayList<Integer> v) { 10 Integer[] newText = (Integer[])v.toArray(new Integer[0]); 11 return newText; 12 } 13 14 // toArray(T[] contents)調用方式三 15 public static Integer[] vectorToArray3(ArrayList<Integer> v) { 16 Integer[] newText = new Integer[v.size()]; 17 Integer[] newStrings = (Integer[])v.toArray(newText); 18 return newStrings; 19 }
(六)、總結
1、ArrayList是基於動態數組的集合,當使用默認構造方法ArrayList()時,則ArrayList的默認容量大小是10;
2、當ArrayList容量不足時,ArrayList會動態擴容大小:JDK 1.7前 : 新的容量 = (原始容量 * 3 / 2) + 1 , JDK 1.7后 : 新的容量 = 原始容量 + 原始容量 >> 1;
3、ArrayList的克隆復制是將全部元素克隆至新的數組中;
4、ArrayList實現了Serializable接口,當寫入輸出流時,先寫入“容量”,再依次寫入“每一個節點的值”,當讀取輸入流時,先讀取“容量”,再依次讀取“每個節點的元素”;
五、Vector
(一)、簡介
Vector同ArrayList類似,底層數據結構是數組,特性即功能與ArrayList類似,也具有增刪慢,查詢快的特點,不同的是,Vector是同步的,線程安全的,所以在多線程環境下,使用Vector比ArrayList更合適。
Vector的特性:
-
- 增刪慢,查詢快;
- 效率低,線程安全(與ArrayList相反);
- 擅長隨機訪問,通過索引值遍歷;
(二)、Vector構造方法
(三)、Vector的三個重要的屬性:elementData , elementCount, capacityIncrement
elementData的特性與ArrayList中的elementData一致,這里不再贅述,這里主要講一下elementCount和capacityIncrement。
1、elementCount:動態數組的大小,同ArrayList的size
2、capacityIncrement:擴容數組大小的增長系數。
在創建Vector時如指定了capacityIncrement的系數,則在Vector需要擴容時,擴容的大小總是capacityIncrement的系數大小。
(四)、Vector的遍歷方式
Vector一共有4種遍歷方式:Iterator迭代器遍歷、通過索引值的隨機方法、for-each遍歷和Enumeration遍歷。
Iterator迭代器遍歷、通過索引值的隨機方法、for-each遍歷和ArrayList類似不再贅述,這里講一下Enumeration遍歷。
1、Enumeration遍歷

1 Integer value = null; 2 Enumeration enu = vector.elements(); 3 while (enu.hasMoreElements()) { 4 value = (Integer)enu.nextElement(); 5 }
2、4種遍歷方式的性能
遍歷Vector時,在性能方面:隨機訪問,通過索引值遍歷 > for-each遍歷 > Enumeration遍歷 > Iterator迭代器遍歷
所以,無論是ArrayList還是Vector遍歷,推薦使用隨機訪問,通過索引值遍歷 。
(五)、ArrayList與Vector的區別
1、線程安全。ArrayList是線程不安全的,Vector是線程安全的;
2、擴容不同。ArrayList默認擴容是(原始容量 * 3 / 2) + 1,Vector默認擴容是原始容量 * 2;
3、遍歷方式不同,ArrayList有3種,Vector有4種,多的一種是Enumeration遍歷;
(六)、總結
1、Vector實際上是通過一個數組去保存數據的。當我們構造Vecotr時;若使用默認構造函數,則Vector的默認容量大小是10。
2、 當Vector容量不足以容納全部元素時,Vector的容量會增加。若容量增加系數 >0,則將容量的值增加“容量增加系數”;否則,將容量大小增加一倍。
3、 Vector的克隆函數,即是將全部元素克隆到一個數組中;
4、Vector默認擴容是原始容量 * 2;
六、LinkedList
(一)、簡介及繼承結構
1、簡介
LinkedList是List接口的另一種實現方式,底層數據結構是鏈表,具有增刪快,查詢慢的特性。自定義了操作頭部和尾部元素的方法,可以當做“棧”使用,同時又是Deque的實現類,所以有具有雙端隊列的特性。所以LinkedList既可以當做“棧”,也可以當做雙端隊列。
LinkedList的特性:
- 元素增刪快,查詢慢(基於鏈表的數據結構);
- 效率高,線程不安全(非同步);
- 既可以當作“棧”使用,又可以當作隊列使用。
2、繼承結構
通過結構圖可以看出,ArrayList繼承自AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable、Deque這些接口。
-
-
- 繼承AbstractList,實現了List接口:它是一個動態鏈表,提供了相關添加,刪除,修改和遍歷等功能;
- 實現RandomAccess接口:提供隨機訪問的能力;
- 實現Cloneable接口:覆蓋函數clone(),可以被克隆;
- 實現Serializable接口:支持序列化和反序列化,可以通過序列化傳輸數據。
-
(二)、LinkedList的構造方法
(三)、LinkedList三個重要屬性:size,first、last
1、size:雙向鏈表節點的個數;
2、first:雙向鏈表的頭部節點;
3、last:雙向鏈表的尾部節點。
1 transient Node<E> first; 2 3 transient Node<E> last; 4 5 private static class Node<E> { 6 E item; 7 Node<E> next; 8 Node<E> prev; 9 10 Node(Node<E> prev, E element, Node<E> next) { 11 this.item = element; 12 this.next = next; 13 this.prev = prev; 14 } 15 }
從部分源碼中可以看出,first和last是Node的實體實例,Node是LinkedList內部的私有靜態類,Node定義了三個變量:item、next和prev。
- item:保存當前節點的值;
- next:指向下一個節點的值;
- prev:指向上一個節點的值;
(四)、LinkedList的遍歷方式
LinkedList支持多種遍歷方式。建議不要采用隨機訪問的方式去遍歷LinkedList,而采用逐個遍歷的方式。
1、Iterator迭代器遍歷

Integer integer = null; Iterator iterator = list.iterator(); while(iterator.hasNext()){ integer = (Integer) iterator.next(); }
2、快速隨機訪問遍歷

1 Integer integer = null; 2 3 for (int i=0; i<list.size(); i++) { 4 integer = (Integer) list.get(i); 5 }
3、for-each方式遍歷

1 Integer integer = null; 2 3 for (Object object : list) { 4 integer = (Integer) object; 5 }
4、pollFirst()遍歷
while(list.pollFirst() != null) {}
5、pollLast()遍歷
while(list.pollLast() != null) {}
6、removeFirst()遍歷
while(list.removeFirst() != null) {}
7、removeLast()遍歷
while(list.removeLast() != null) {}
8、遍歷方式的性能
遍歷LinkedList時,removeFirst()和removeLast()效率最高,但是使用這兩種方式會在遍歷時刪除數據。如果只想單獨讀取數據,不刪除數據,推薦使用for-each方式遍歷。
(五)、LinkedList常用方法
1、add(E e)和 add(int index, E element)
add(E e)的添加方式:

1 // 添加元素,以添加最后一個元素為例 2 public boolean add(E e) { 3 linkLast(e); 4 return true; 5 } 6 7 void linkLast(E e) { 8 final Node<E> l = last; 9 final Node<E> newNode = new Node<>(l, e, null); 10 last = newNode; 11 if (l == null) 12 first = newNode; 13 else 14 l.next = newNode; 15 size++; 16 modCount++; 17 }
情況1:假設LinkedList集合為空的情況,看如下圖解:
情況2:假設LinkedList集合不為空,看如下圖解:
如果LinkedList不為空,那么添加進來的元素2就是last,元素2的prev指向以前的最后一個元素,即元素1,元素2的next為null;
注意:如果只在頭部或尾部添加元素,可以直接調用addFirst(E e)或addLast(E e);
add(int index,E element)添加方式:

1 //在指定位置添加一個元素 2 public void add(int index, E element) { 3 checkPositionIndex(index); 4 if (index == size) 5 linkLast(element); 6 else 7 linkBefore(element, node(index)); 8 } 9 10 private void checkPositionIndex(int index) { 11 if (!isPositionIndex(index)) 12 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); 13 } 14 15 private boolean isPositionIndex(int index) { 16 return index >= 0 && index <= size; 17 } 18 19 void linkBefore(E e, Node<E> succ) { 20 // assert succ != null; 21 final Node<E> pred = succ.prev; 22 final Node<E> newNode = new Node<>(pred, e, succ); 23 succ.prev = newNode; 24 if (pred == null) 25 first = newNode; 26 else 27 pred.next = newNode; 28 size++; 29 modCount++; 30 }
從代碼中看到和add(E e)的代碼實現沒有本質區別,都是通過新建一個Node實體,同時指定其prev和next來實現,不同點在於需要調用node(int index)通過傳入的index來定位到要插入的位置,這個也是比較耗時的。
如下圖解:
LinkedList插入效率高是相對的,因為它省去了ArrayList插入數據可能的數組擴容和數據元素移動時所造成的開銷,但數據擴容和數據元素移動卻並不是時時刻刻都在發生的。
2、remove(Object o) 和 remove(int index)

//刪除某個對象 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; } //刪除某個位置的元素 public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } //刪除某節點,並將該節點的上一個節點(如果有)和下一個節點(如果有)關聯起來 E unlink(Node<E> x) { final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }
先找到要刪除的節點,remove(Object o)方法遍歷整個集合,通過 == 或 equals方法進行判斷;remove(int index)通過node(index)方法。
(六)、LinkedList與ArrayList的區別
1、底層數據結構不同。ArrayList底層數據結構是數組。LinkedList底層數據結構是鏈表;
2、性能不同。ArrayList增刪慢,查詢快。LinkedLIst增刪快,查詢慢(底層數據結構不同);
3、內存不同。LinkedList比ArrayList更占內存(因為LinkedList每個節點存儲兩個引用,一個指向前元素,一個指向后元素)。
(七)、總結
1、LinkedList是基於鏈表結構的集合。內部定義了一個私有的Node實體實例,Node是雙向鏈表節點對應數據的數據結構,其定義了三個屬性:item(保存該節點的值)、prev(指向前一個元素的節點)、next(指向后一個元素的節點);
2、LinkedList不存在容量不足的問題;
3、LinkedList克隆時,是將全部元素克隆到新的LinkedLIst對象中;
4、LinkedList實現了Serializable接口,當寫入輸出流時,先寫入“容量”,再依次寫入“每一個節點的值”,當讀取輸入流時,先讀取“容量”,再依次讀取“每個節點的元素”;
5、LinkedList實現了Deque,而Deque接口定義了在雙端隊列兩端訪問元素的方法,提供插入、移除和檢查元素的方法。每種方法都存在兩種形式:一種形式在操作失敗時拋出異常,另一種形式返回一個特殊值(null 或 false)。
七、Stack
(一)、簡介及繼承結構
1、Stack是繼承自Vector,所以它的底層數據結構也是動態數組。實現了一個“先進后出(FILO, First In Last Out)”的棧結構。
2、Stack的繼承結構
由於繼承了Vector,所以具有了Vector的相關特性,這里不再贅述。
(二)、Stack的構造方法
(三)、常用的API方法
(四)、總結
1、Stack實際上也是通過數組去實現的。
執行push時(即,將元素推入棧中),是通過將元素追加的數組的末尾中。
執行peek時(即,取出棧頂元素,不執行刪除),是返回數組末尾的元素。
執行pop時(即,取出棧頂元素,並將該元素從棧中刪除),是取出數組末尾的元素,然后將該元素從數組中刪除。
2、Stack繼承於Vector,意味着Vector擁有的屬性和功能,Stack都擁有。
八、List與實現類的擴展