Java學習筆記-ArrayList(2)和LinkedList
上一篇中我們大致介紹了ArrayList的優點和隱藏的,不容易被發現的弊端。但是這一篇,我們還要再對ArrayList批判一番。
又因為它是陣列,當我們需要往列表最後丟一個數據的時候很簡單,但是如果要往中間丟呢?方法大家肯定都想到了。挪唄!後面的各位同學讓讓,擠個人進來:
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++; }
先看一下這個人是不是真的進來的地方對,別跑到很後面去了;再看看進來以後地方還夠不夠了,不夠還得擴容(grow),然後不好意思,這人位置後面的全部copy往後移一位。這個System.arraycopy和Arrays.copyof可不一樣,它不會新建一個新的陣列物件,但是會挨個去賦值交換,就跟我們自己寫for迴圈,arr[i+1]=arr[i]一樣。極端一點的情況,假如我們要往List的頭部插一個數據(雖然ArrayList並沒有addFirst方法),那就得把後面所有的資料都挨個移位!
而addall是怎麼操作的呢?
public boolean addAll(int index, Collection<? extends E> c) { rangeCheckForAdd(index); Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); // Increments modCount int numMoved = size - index; if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; }
一樣是移,只不過這次不是一個人了,我得算算要進來幾個人。怎麼算呢,先用toArray,再獲取length。這個toArray也是一比開銷,如果原來傳進來的也是個ArrayList還好,偷懶copyof就行了;如果不是,那就還得先轉換成陣列,再移位,再複製,是不是頭都大了?
同理,如果我想從列表裡刪掉一個或者幾個節點,那麼後面的也得統統移位,這個操作量就很大了。所以這裡我們隆重推薦ArrayList的一個兄弟:LinkedList。
不難想到,LinkedList的建構函式中不用去指定預設大小了。它裡面的資料結構也不是陣列了,而是節點(Node)。別誤會,這個Node可不是xml的,也跟org.w3c.dom半毛錢關係都沒有。這個Node是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的add和remove操作,實際上是通過一系列link和unlink進行操作的,這些方法有:linkFirst、linkLast、linkBefore(Node)、unlinkFirst、unLinkLast、unLink(Node)。他們實際做的,就是修改節點中指向前一個和後一個的指標。
我們用add(E, index)舉例子:
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
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;
}
}
首先判斷了一下下標是否合法,然後看是不是在隊尾(是則視作linkLast),然後呼叫了一個node方法去取得某個下標的節點。這個方法中實際上也進行了一遍遍歷,所以開銷其實也是不小的。然後就呼叫linkBefore的方法,修改了這個插入的節點之前節點的”向後的指標“讓它指向自己,修改這個插入的節點之後節點的”向前的“節點讓它也指向自己,自己的兩個指標則指向前後兩個節點,這樣這個節點就算插入進去了。
有點繞是不是?鑑於我的繪圖水平有限,建議看不懂的去搜一下連結串列的圖,很簡單就懂了。
那麼有人問了最後那個節點呢?它的往下一個節點的指標指向啥?null唄。
從上面這個例子我們可以看出,對一個LinkedList的頭和尾進行資料操作是很高效的,因為只需要改改指標就行了。但是如果要往中間增刪節點,由於有一個遍歷過程,效率就沒那麼高了,但是仍然優於ArrayList(因為不需要進行大規模的資料遷徙),而addAll方法需要先把傳入的集合變化成陣列,再往裡插,效率會更加低一些,和ArrayList孰優孰劣我也沒驗證過,大家有興趣可以去試一下。
由於LinkedList往頭尾增刪資料很方便這種特性,我們可以用它模擬棧(stack)這種資料結構,實際上LinkedList也提供了一系列的方法,其中就有棧操作的push和pop:
public void push(E e) {
addFirst(e);
} //往頭(棧頂)上插個數據(壓棧)
public E pop() {
return removeFirst();
} //刪除並返回頭上的資料(出棧)
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
} //返回但不刪除頭上的資料
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
} //等於peek
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
} //返回但不刪除尾巴的資料
public boolean offer(E e) {
return add(e);
} //等同於add,再尾部新增資料
public boolean offerFirst(E e) {
addFirst(e);
return true;
} //addFirst 不多解釋了,返回值不同而已
public boolean offerLast(E e) {
addLast(e);
return true;
}//類比上面
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
} //其實就是pop 寫法不同而已
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}//你懂的
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}//你懂的
public E element() {
return getFirst();
}//getFirst換個名字而已=_= 用來instanceof名字更直觀?
其他的不再贅述了,大家可以自己去翻原始碼。另外,它的toArray就比較痛苦了:
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}
一樣是要遍歷整個連結串列,效率比ArrayList低了不止一點半點,從這裡我們可以看出只要是addAll都得經歷一個痛苦地轉陣列的過程,而LinkedList要更加痛苦一些。對於將一大批物件丟到集合裡這個過程,set比List效率更優,即使不看具體實現也比較好理解:set不需要維護裡面物件的有序性,自然更有優勢。