ArrayList
ArrayList中有兩個方法可以返回數據
public Object[] toArray();
public <T> T[] toArray(T[] a);
ArrayList中有一個靜態方法asList可以返回對應的List
Integer[] a = {1, 2, 3};
List<Integer> list = Arrays.asList(a);
這個方法返回的List,內部就是傳入的數組,所以對數據的修改也會反映到List中,對List調用add、remove方法會拋出異常
要使用ArrayList完整的方法,應該新建一個ArrayList
List<Integer> list = new ArrayList<Integer>(Arrays.asList(a));
ArrayList特點總結:
1. 可以隨機訪問,按照索引位置進行訪問效率很高。O(1)
2. 除非數組已排序,否則按照內容查找元素效率比較低。O(N)
3. 插入和刪除元素效率比較低。O(N)
4. 非線程安全的
LinkedList
LinkedList還實現了隊列接口Queue,隊列的特點就是先進先出。
public interface Queue<E> extends Collection<E> {
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
}
Queue主要擴展了Collection接口,主要是三個操作:
- 在尾部添加元素(add、offer)
- 查看頭部元素(element、peek),返回頭部元素,但不改變隊列
- 刪除頭部元素(remove、poll),返回頭部元素,並且從隊列中刪除
每個操作都對應了兩個方法。它們的區別在於,當隊列爲空的時候,element和remove會拋出異常,而peek和poll會返回null;當隊列爲滿的時候,add會拋出異常,offer返回false
LinkedList其實是直接實現的Duque接口,該接口表示雙端隊列。它可以實現棧相關的操作,棧的特點是先進後出,後進先出。
public interface Deque<E> extends Queue<E> {
// 表示入棧,在頭部添加元素,如果棧滿了,會拋出異常
void push(E e);
// 出棧,返回頭部元素,並且從棧中刪除,如果棧爲空,拋出異常
E pop();
// 查看棧頭部元素,如果棧爲空,返回null
E peek();
// 從後往前遍歷
Iterator<E> descendingIterator();
}
LinkedList和ArrayList用法上類似,只是LinkedList增加了一個接口Deque,可以把它看作隊列、棧、雙端隊列。
LinkedList實現原理
ArrayList內部是數組,元素在內存是連續存放的,但LinkedList不是。LinkedList直譯就是鏈表,它的內部是雙向鏈表,每個元素在內存都是單獨存放的,元素之間通過鏈接連在一起。
爲了表示鏈接關係,需要一個節點,節點包括實際的元素,兩個鏈接,分別指向前一個節點(前驅)和後一個節點(後繼)
private static class Node<E> {
// 實際的元素
E item;
// 指向後一個節點
Node<E> next;
// 指向前一個節點
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkedList內部組成主要由如下三哥實例變量
int size = 0; // 表示鏈表長度
Node<E> first; // 指向頭節點
Node<E> last; // 指向尾節點
添加元素 add
public boolean add(E e) {
linkLast(e);
return true;
}
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++;
}
與ArrayList不同,LinkedList的內存是按需分配的,添加元素也很簡單,直接在尾節點添加鏈接即可
獲取元素 get
public E get(int index) {
// 檢查index是否超出範圍
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// sieze >> 1相當於size/2
// 如果index在size的前半部分,則從頭節點開始找,否則從尾節點開始
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;
}
}
與ArrayList不同,ArrayList中數組元素連續存放,可以根據索引直接定位,而在LinkedList中,則必須從頭或尾節點順着鏈接查找
插入元素
add是在尾部添加元素,如果在頭部或中間插入元素,可以使用如下的方法
public void add(int index, E element) {
// 檢查index是否合法
checkPositionIndex(index);
// 如果index正好等於size,那麼就直接調用添加方法
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
/**
* @succ 目標位置當前的節點 這裏插入後,就變成後繼了
*/
void linkBefore(E e, Node<E> succ) {
// 獲取當前位置節點的前驅
final Node<E> pred = succ.prev;
// 新建節點,前驅是pred,後繼是之前位置上的節點
final Node<E> newNode = new Node<>(pred, e, succ);
// 後驅的前驅重新給值
succ.prev = newNode;
// 前驅的後繼重新給值
if (pred == null)
first = newNode;
else
pred.next = newNode;
// 增加長度
size++;
modCount++;
}
在中間插入元素,LinkedList只需要按需分配內存,修改前驅和後繼節點的鏈接,而ArrayList可能需要分配很多額外的空間,並移動後續元素
remove 刪除元素
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
E unlink(Node<E> x) {
// assert x != null;
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;
}
刪除x節點,基本思路就是讓x的前驅和後繼鏈接起來。
LinkedList特點
- 按需分配空間,不需要預先分配額外的空間
- 不可隨機訪問,按照索引位置訪問效率比較低,必須從頭或尾順着鏈接找,效率爲O(N/2)
- 不管列表是否已排序,只要是按照內容查找元素,效率都比較低,必須逐個比較,效率爲O(N)
- 在兩端添加、刪除元素效率很高,爲O(1)
- 在中間插入、刪除元素,要先定位,效率較低,爲O(N),但修改本身的效率很高,爲O(1)
如果列表長度未知,添加、刪除操作比較多,尤其經常從兩端進行操作,而按照索引位置訪問相對比較少,則LinkedList是比較理想的選擇
ArrayDeque
ArrayDeque是基於數組實現的雙端隊列,主要有以下幾個屬性
// 存儲隊列中節點的數組
transient Object[] elements;
// 代表頭指針
transient int head;
// 代表尾指針
transient int tail;
// 代表創建一個隊列的最小容量
private static final int MIN_INITIAL_CAPACITY = 8;
再來看看構造函數
public ArrayDeque() {
elements = new Object[16];
}
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}
private void allocateElements(int numElements) {
// 如果numElements小於8,那麼數組長度就分配8
intinitialCapacity = MIN_INITIAL_CAPACITY;
if (numElements >= initialCapacity) {
// 如果numElements大於8,分配的長度是嚴格大於numElements並且爲2的整數次冪
initialCapacity = numElements;
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++;
if (initialCapacity < 0) // Too many elements, must back off
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
}
elements = new Object[initialCapacity];
}
如果沒有指定顯式傳入elements的長度,則默認16。如果顯式傳入一個代表elements的長度的變量,那麼會調用allocateElements做一些簡單的處理,主要的處理都在上面的代碼註釋中。 這裏它計算嚴格大於numElements並且爲2的整數次冪的方式,就是先將numElements二進制形式的所有位置1,然後+1就是了。
從尾部添加 add
public boolean add(E e) {
addLast(e);
return true;
}
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
// 將元素添加到尾指針的位置
elements[tail] = e;
// 將tail指向向一個位置,如果滿了,就擴容
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
這裏我們重點看一下判斷當前隊列是否滿了的語句
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
我們之前在構造elements元素的時候,說過它的長度一定是2的指數級,所以對於任意一個2的指數級的值減去1之後它的二進制必然所有位全爲1,例如:8-1之後爲111,16-1之後1111。而對於tail來說,當tail+1小於等於elements.length - 1,兩者與完之後的結果還是tail+1,但是如果tail+1大於elements.length - 1,兩者與完之後就爲0。就等於頭指針的位置了。
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p; // number of elements to the right of p
int newCapacity = n << 1;
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
// 分配一個長度翻倍的數組
Object[] a = new Object[newCapacity];
// 將head右邊的元素賦值到新數組開頭
System.arraycopy(elements, p, a, 0, r);
// 將head左邊的元素賦值到新數組中
System.arraycopy(elements, 0, a, r, p);
elements = a;
// 重新設置head和tail
head = 0;
tail = n;
}
從頭部添加 addFirst
public void addFirst(E e) {
if(e == null)
throw new NullPointerException();
// 讓head指向前一個位置
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
出棧
public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
E result = (E) elements[h];
if (result == null)
return null;
elements[h] = null;
head = (h + 1) & (elements.length - 1);
return result;
}
該方法很簡單,直接獲取數組頭部元素即可,然後head往後移動一個位置。這是出隊操作,其實刪除操作也是一種出隊,內部還是調用了pollFirst方法。
查看長度
public int size() {
return (tail - head) & (elements.length - 1);
}
ArrayDeque特點
- 在兩端添加、刪除元素的效率很高,動態擴展需要的內存分配以及數組複製開銷可以被平攤,具體來說,添加N個元素的效率爲O(N)
- 根據元素內容查找和刪除的效率比較低,爲O(N)
- 與ArrayList和LinkedList不同,沒有索引的概念
迭代器
Iterable Iterator
Iterable 表示可迭代的,它有一個方法iterator(),返回Iterator對象。
public interface Iterable<T> {
Iterator<T> iterator();
}
public interface Iterator<E> {
// 判斷是否還有元素未訪問
boolean hasNext();
// 返回下一個元素
E next();
// 刪除最後返回的元素。如果沒有調用過next(),直接調用remove()是會報錯的
void remove();
}
如果對象實現了Iterable,就可以使用foreach語法。foreach語法,編譯器會轉換爲調用Iterable和Iterator接口的方法。
ListIterator
ArrayList還提供了兩個返回Iterator接口的方法:
// 返回的迭代器從0開始
public ListIterator<E> listIterator();
// 返回的迭代器從指定位置index開始
public ListIterator<E> listIterator(int index);
ListIterator擴展了Iterator接口
public interface ListIterator<E> extends Iterator<E> {
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
void set(E e);
void add(E e);
}
迭代器常見問題
有一種常見的誤用,就是在迭代的中間調用容器的刪除方法:
public void remove(ArrayList<Integer> list) {
for(Integer a : list) {
if(a<=100) {
list.remove(a);
}
}
}
這樣做是會拋出異常的。因爲迭代器內部會維護一些索引位置相關的數據,要求在迭代過程中,容器不能發生結構性變化(添加、刪除元素)
使用迭代器的remove方法可以避免錯誤
public static void remove(ArrayList<Integer> list) {
Iterator<Integer> it = list.iterator();
while(it.hasNext()) {
if(it.next() < 100) {
it.remove();
}
}
}
爲什麼使用迭代器的remove方法就可以呢?
迭代器基本原理
先看看ArrayList中的iterator方法
public Iterator<E> iterator() {
return new Itr();
}
Itr是ArrayList的成員內部類,
private class Itr implements Iterator<E> {
// 下一個要返回的元素的位置
int cursor;
// 最後一個返回的索引位置,如果沒有爲-1
int lastRet = -1;
// 期望的修改次數
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
public E next() {
// 檢查是否發生了結構變化
checkForComodification();
// 更新cursor和lastRet
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
// 返回對應的元素
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// 通過ArrayList的remove方法刪除
ArrayList.this.remove(lastRet);
// 更新cursor和lastRet
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}