第一篇文章中介紹了List集合的一些通用知識。本篇文章將集中介紹了List集合相比Collection接口增加的一些重要功能以及List集合的兩個重要子類ArrayList及LinkedList。
一、List集合
關于List集合的介紹及方法,可以參考第一篇文章。
List集合判斷元素相等的標準
List判斷兩個對象相等只要通過equals()方法比較返回true即可(關于equals()方法的詳解可以參考第二篇文章中的內容)。
下面以用代碼具體展示。
創建一個Book類,并重寫equals()方法,如果兩個Book對象的name屬性相同,則認為兩個對象相等。
public class Book {
public String name;
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Book other = (Book) obj;
if (this.name == other.name) {
return true;
}
return false;
}
}
向List集合中加入book1對象,然后調用remove(Object o)方法,從集合中刪除指定對象,這個時候指定的對象是book2。
public static void main(String[] args){
Book book1 = new Book();
book1.name = "Effective Java";
Book book2 = new Book();
book2.name = "Effective Java";
List<Book> list = new ArrayList<Book>();
list.add(book1);
list.remove(book2);
System.out.println(list.size());
}
輸出結果:
0
可見把book1對象從集合中刪除了,這表明List集合判斷兩個對象相等只要通過equals()方法比較返回true即可。
與Set不同,List還額外提供了一個listIterator()方法,該方法返回一個ListIterator對象。下面具體介紹下ListIterator。
ListIterator
ListIterator接口在Iterator接口基礎上增加了如下方法:
**boolean hasPrevious(): **如果以逆向遍歷列表。如果迭代器有上一個元素,則返回 true。
Object previous():返回迭代器的前一個元素。
void add(Object o):將指定的元素插入列表(可選操作)。
與Iterator相比,ListIterator增加了前向迭代的功能,還可以通過add()方法向List集合中添加元素。
二、ArrayList
既然要介紹ArrayList,那么就順帶一起介紹Vector。因為二者的用法功能非常相似,可以一起了解比對。
ArrayList簡介
ArrayList和Vector作為List類的兩個典型實現,完全支持之前介紹的List接口的全部功能。
ArrayList和Vector類都是基于數組實現的List類,所以ArrayList和Vector類封裝了一個動態的、允許再分配的Object[]數組。ArrayList或Vector對象使用initalCapacity參數來設置該數組的長度,當向ArrayList或Vector中添加元素超過了該數組的長度時,它們的initalCapacity會自動增加。下面我們通過閱讀JDK 1.8 ArrayList源碼來了解這些內容。
ArrayList的本質
當以List<Book> list = new ArrayList<Book>(3);
方式創建ArrayList集合時,
//動態Object數組,用來保存加入到ArrayList的元素
Object[] elementData;
//ArrayList的構造函數,傳入參數為數組大小
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//創建一個對應大小的數組對象
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//傳入數字為0,將elementData 指定為一個靜態類型的空數組
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
當以List<Book> list = new ArrayList<Book>();
方式創建ArrayList集合時,不指定集合的大小
/**
*Constructs an empty list with an initial capacity of ten。意思是:構造一個空數組,默認的容量為10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
在這里可以看出private static final int DEFAULT_CAPACITY = 10;
默認容量確實為10。
當向數組中添加元素list.add(book1);
時:
先調用add(E e)方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 數組的大小增加1
elementData[size++] = e;
return true;
}
在該方法中,先調用了一個ensureCapacityInternal()方法,顧名思義:該方法用來確保數組中是否還有足夠容量。
經過一系列方法(不必關心),最后有個判斷:如果剩余容量足夠存放這個數據,則進行下一步,如果不夠,則需要執行一個重要的方法:
private void grow(int minCapacity) {
//......省略部分內容 主要是為了生成大小合適的newCapacity
//下面這行就是進行了數組擴容
elementData = Arrays.copyOf(elementData, newCapacity);
}
由此,我們就清楚地明白了,ArrayList是一個動態擴展的數組,Vector也同樣如此。
如果開始就知道ArrayList或Vector集合需要保存多少個元素,則可以在創建它們時就指定initalCapacity的大小,這樣可以提高性能。
此外,ArrayList還提供了兩個額外的方法來調整其容量大小:
void ensureCapacity(int minCapacity): 如有必要,增加此 ArrayList 實例的容量,以確保它至少能夠容納最小容量參數所指定的元素數。
void trimToSize():將此 ArrayList 實例的容量調整為列表的當前大小。
ArrayList和Vector的區別
1.ArrayList是線程不安全的,Vector是線程安全的。
2.Vector的性能比ArrayList差。
Stack
Stack是Vector的子類,用戶模擬“棧”這種數據結構,“棧”通常是指“后進先出”(LIFO)的容器。最后“push”進棧的元素,將被最先“pop”出棧。如下圖所示:
Stack類里提供了如下幾個方法:
Stack與Vector一樣,是線程安全的,但是性能較差,盡量少用Stack類。如果要實現棧”這種數據結構,可以考慮使用LinkedList(下面就會介紹)。
ArrayList的遍歷方式
ArrayList支持3種遍歷方式
(01) 第一種,通過迭代器遍歷
Integer value = null;
Iterator iter = list.iterator();
while (iter.hasNext()) {
value = (Integer)iter.next();
}
(02) 第二種,隨機訪問,通過索引值去遍歷
由于ArrayList實現了RandomAccess接口,它支持通過索引值去隨機訪問元素。
Integer value = null;
int size = list.size();
for (int i=0; i<size; i++) {
value = (Integer)list.get(i);
}
(03) 第三種,for循環遍歷
Integer value = null;
for (Integer integ:list) {
value = integ;
}
遍歷ArrayList時,使用隨機訪問(即,通過索引序號訪問)效率最高,而使用迭代器的效率最低。具體可以測試下。
三、LinkedList
LinkedList簡介
LinkedList類是List接口的實現類——這意味著它是一個List集合,可以根據索引來隨機訪問集合中的元素。除此之外,LinkedList還實現了Deque接口,可以被當作成雙端隊列來使用,因此既可以被當成“棧"來使用,也可以當成隊列來使用。
LinkedList的實現機制與ArrayList完全不同。ArrayList內部是以數組的形式來保存集合中的元素的,因此隨機訪問集合元素時有較好的性能;而LinkedList內部以鏈表的形式來保存集合中的元素,因此隨機訪問集合元素時性能較差,但在插入、刪除元素時性能比較出色。
由于LinkedList雙端隊列的特性,所以新增了一些方法。
LinkedList方法
void addFirst(E e):將指定元素插入此列表的開頭。
void addLast(E e): 將指定元素添加到此列表的結尾。
E getFirst(E e): 返回此列表的第一個元素。
E getLast(E e): 返回此列表的最后一個元素。
boolean offerFirst(E e): 在此列表的開頭插入指定的元素。
boolean offerLast(E e): 在此列表末尾插入指定的元素。
E peekFirst(E e): 獲取但不移除此列表的第一個元素;如果此列表為空,則返回 null。
E peekLast(E e): 獲取但不移除此列表的最后一個元素;如果此列表為空,則返回 null。
E pollFirst(E e): 獲取并移除此列表的第一個元素;如果此列表為空,則返回 null。
E pollLast(E e): 獲取并移除此列表的最后一個元素;如果此列表為空,則返回 null。
E removeFirst(E e): 移除并返回此列表的第一個元素。
boolean removeFirstOccurrence(Objcet o): 從此列表中移除第一次出現的指定元素(從頭部到尾部遍歷列表時)。
E removeLast(E e): 移除并返回此列表的最后一個元素。
boolean removeLastOccurrence(Objcet o): 從此列表中移除最后一次出現的指定元素(從頭部到尾部遍歷列表時)。
下面我們就以閱讀源碼的方式來了解LinkedList內部是怎樣維護鏈表的。
LinkedList本質
LinkedList調用默認構造函數,創建一個鏈表。由于維護了一個表頭,表尾的Node對象的變量。可以進行后續的添加元素到鏈表中的操作,以及其他刪除,插入等操作。也因此實現了雙向隊列的功能,即可向表頭加入元素,也可以向表尾加入元素
//成員變量:表頭,表尾
transient Node<E> first;
transient Node<E> last;
//默認構造函數,表示創建一個空鏈表
public LinkedList() {
}
下面來了解Node類的具體情況
private static class Node<E> {
//表示集合元素的值
E item;
//指向下個元素
Node<E> next;
//指向上個元素
Node<E> prev;
...................................省略
}
由此可以具體了解鏈表是如何串聯起來并且每個節點包含了傳入集合的元素。
下面以增加操作,具體了解LinkedList的工作原理。
public boolean add(E e) {
linkLast(e);
return true;
}
調用linkLast(e);
方法,默認向表尾節點加入新的元素
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
更新表尾節點,建立連接。其他操作類似,維護了整個鏈表。
下面具體來看,如何將“雙向鏈表和索引值聯系起來的”?
public E get(int index) {
checkElementIndex(index);//檢查索引是否有效
return node(index).item;
}
調用了node(index)
方法返回了一個Node對象,其中node(index)
方法具體如下
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
首先會比較“index”和“雙向鏈表長度的1/2”;若前者小,則從鏈表頭開始往后查找,直到index位置;否則,從鏈表末尾開始先前查找,直到index位置。這就是“雙線鏈表和索引值聯系起來”的方法。
到此我們便會明白,LinkedList在插入、刪除元素時性能比較出色,隨機訪問集合元素時性能較差。
LinkedList遍歷方式
LinkedList支持多種遍歷方式。
1.通過迭代器遍歷LinkedList
2通過快速隨機訪問遍歷LinkedList
3.通過for循環遍歷LinkedList
4.通過pollFirst()遍歷LinkedList
5.通過pollLast()遍歷LinkedList
6通過removeFirst()遍歷LinkedList
7.通過removeLast()遍歷LinkedList
實現都比較簡單,就不貼代碼了。
其中采用逐個遍歷的方式,效率比較高。采用隨機訪問的方式去遍歷LinkedList的方式效率最低。
LinkedList也是非線程安全的。
四、ArrayList與LinkedList性能對比
ArrayList 是一個數組隊列,相當于動態數組。它由數組實現,隨機訪問效率高,隨機插入、隨機刪除效率低。ArrayList應使用隨機訪問(即,通過索引序號訪問)遍歷集合元素。
LinkedList 是一個雙向鏈表。它也可以被當作堆棧、隊列或雙端隊列進行操作。LinkedList隨機訪問效率低,但隨機插入、隨機刪除效率高。LinkedList應使用采用逐個遍歷的方式遍歷集合元素。
如果涉及到“動態數組”、“棧”、“隊列”、“鏈表”等結構,應該考慮用List,具體的選擇哪個List,根據下面的標準來取舍。
(01) 對于需要快速插入,刪除元素,應該使用LinkedList。
(02) 對于需要快速隨機訪問元素,應該使用ArrayList。
(03) 對于“單線程環境” 或者 “多線程環境,但List僅僅只會被單個線程操作”,此時應該使用非同步的類(如ArrayList)。對于“多線程環境,且List可能同時被多個線程操作”,此時,應該使用同步的類(如Vector)。
由淺入深理解java集合(一)——集合框架 Collction、Map
由淺入深理解java集合(二)——集合 Set
由淺入深理解java集合(四)——集合 Queue
由淺入深理解java集合(五)——集合 Map
由淺入深理解java集合(六)——集合增刪改查的細節、性能及選擇推薦(待更新)