一.線性表
定義:零個或者多個元素的有限序列。
也就是說它得滿足以下幾個條件:
??①該序列的數據元素是有限的。
??②如果元素有多個,除了第一個和最后一個元素之外,每個元素只有一個前驅元素和一個后驅元素。
??③第一元素沒有前驅元素,最后一個元素沒有后繼元素。
??④序列中的元素數據類型相同。
則這樣的數據結構為線性結構。在復雜的線性表中,一個數據元素(對象)可以由若干個數據項組成,組成一張數據表,類似于數據庫。
二.線性表的抽象數據類型
1.相關概念
抽象數據類型(abstract data type,ADT)是帶有一組操作的一組對象的集合。這句話表明,線性表的抽閑數據類型主要包括兩個東西:數據集合和數據集合上的操作集合。
①數據集合:我們假設型如a0,a1,a2,...an-1的表,我們說這個表的大小是N,我們將大小為0的標稱為空表。
②集合上的操作:一般常用的操作包括,查找,插入,刪除,判斷是否為空等等。
2.線性表的順序儲存結構
線性表的順序儲存結構,指的是用一段地址連續的儲存單元依次儲存線性表的數據元素。我們可以用一維數組實現順序儲存結構,并且上述對表的所有操作都可由數組實現。但是一維數組有一個很大缺點就是它得長度是固定的,也就是我們創建的額時候就聲明的長度,一旦我們要存儲的數據超出了這個長度,我們就不得不手動的創建一個更長的新數組并將原來數組中的數據拷貝過去。
int [] arr = new int[10]
......
int [] newArr = new int[arr.length*2]
for(int i=0;i<arr;i++){
newArr[i] = arr[i];
}
arr = new Arr;
顯然這樣做非常的不明智,因此java中的ArrayList就應用而生了。ArrayList也就是傳說中的動態數組,也就是我們可以隨著我們數據的添加動態增長的數組。
??實際上不管是簡單數組也好,動態數組也好,在具體的操作中都存在這樣的問題:
??①如果我們在線性表的第i個位置插入/刪除一個元素,那么我們需要怎么做呢?首先我們得從最后一個元素開始遍歷,到第i個位置,分辨將他們向后/前移動一個位置;在i位置處將要插入/刪除的元素進行相應的插入/刪除操作;整體的表長加/減1.
??②如果我們在線性表的第一個位置插入/刪除一個元素,那么整個表的所有元素都得向后/向前移動一個單位,那么此時操作的時間復雜度為O(N);如果我們在線性表的最末位置進行上面兩種操作,那么對應的時間復雜度為O(1)——綜合來看,在線性表中插入或者刪除一個元素的平均時間復雜度為O(N/2)。
??總結一下,線性表的缺點——插入和刪除操作需要移動大量的元素;造成內存空間的"碎片化"。這里有些童鞋就說了,ArrayList是一個線性表吧,我再ArrayList中添加/刪除一個元素直接調用add()/remove()方法就行了啊,也是一步到位啊——這樣想就不對了,如果我們看ArrayList的源碼就會發現,實際上他內部也是通過數組來實現的,remove()/add()操作也要通過上面說的一系列步驟才能完成,只不過做了封裝讓我們用起來爽。之后我們會通過源碼分析ArrayList等的內部的實現方式。
??當然了,優缺點就會有優點——由于順序儲存結構的元素數目,元素相對位置都確定的,那么我們在存取確定位置的元素數據的時候就比較方便,直接返回就行了。
3.線性表的鏈式儲存結構
上面我們說過,順序結構的最大不足是插入和刪除時需要移動大量元素;造成內存空間的"碎片化。那么造成這中缺點的原因是什么呢?原因就在于這個線性表中相鄰兩元素之間的儲存位置也就有相鄰關系,也就是說元素的位置是相對固定的,這樣就造成了"牽一發而動全身"的尷尬局面;同時,什么是"碎片化"呢?就是說上述順序結構中,以數組為例來說,如果我們先在內存中申請一個10位長度的數組,然后隔3個位置放一個5位長度的數組,這個時候如果再來一個8位長度的數組,那么顯然不能放在前兩個數組之間(他們之間只有三個空位),那只能另找地方,中間的這三個位置就空下了,久而久之類似的事情多了就發生了"碎片化"的現象。
(1)簡單鏈表
為了解決上述兩個問題,我們前輩的科學家們就發明了偉大的"鏈式儲存結構",也就是傳說中的鏈表(Linked List)。鏈表的結構有兩大特點:
??①用一組任意的儲存單元儲存線性表的數據元素,這組儲存單元可以是連續的,也可以是不連續的。這就意味著,這些數據元素可以存在內存中任意的未被占用的位置。
??②在上面的順序數據結構中,每個數據元素只要儲存數據信息就可以了,但是在鏈表中,由于數據元素之間的位置不是固定的,因此為了保證數據元素之間能相互找到前后的位置,每個數據元素不僅需要保存自己的數據信息,還需要保存指向下一個指針位置的信息。
如圖,我們畫出了簡單鏈表的示意圖。鏈表由一系列結點組成,這些結點不必再內存中相連,每個結點含有表元素(數據域)和到包含該元素后繼結點的鏈(link)(指針域)。
??對于一個線性表來說,總得有頭有尾,鏈表也不例外。我們把第一個鏈表儲存的位置叫做"頭指針",整個鏈表的存取就是從頭指針開始的。之后的每一個結點,其實就是上一個結點的指針域指向的位置。最后一個結點由于沒有后繼結點,因此它的指針域為NULL。
??有時我們會在第一個指針前面加上一個頭結點,頭結點的數據域可以不表示任何數值,也可以儲存鏈表長度等公共信息,頭結點的指針域儲存指向第一個結點的位置的信息。
??需要注意的是,頭結點不是鏈表的必要要素,但是有了頭結點,在對第一個結點之前的位置添加/刪除元素時,就與其他元素的操作統一了。
(1.1)簡單鏈表的讀取
??在上面的線性表的順序儲存結構中,我們知道它的一個優點就是存取確定位置的元素比較的方便。但是對于鏈式儲存結構來說,假設我們要讀取i位置上的元素信息,但是由于我們事先并不知道i元素到底在哪里,所以我們只能從頭開始找,知道找到第i個元素為止。由于這個算法的時間復雜度取決于i的位置,最壞的情況就是O(n),即元素在末尾。
??由于單鏈表沒有定義表長,所以我們沒有辦法使用for循環來查找。因此這里查找算法的核心思想是"工作指針后移",也就是當前的元素查完之后,移動到下一個位置,檢查下一個位置的元素,以此循環,直到找到第i個位置的元素。
??可以看到,這是鏈表結構的一個缺點,對于確定位置的元素查找平均時間復雜度為O(N/2)。
(1.2)簡單鏈表的插入與刪除
??當然了,有缺點就有優點,我們說過鏈表解決了順序表中"插入和刪除時需要移動大量元素"的缺點。也就是說,鏈表中插入刪除元素不需要移動其他的元素,那么鏈表是怎么做到的呢?
我們用“.next”表示一個節點的后繼指針,X.next = Y表示將X的后繼指針指向Y節點。這里不得不說一下,由于Java中沒有指針的概念,而是引用(關于指針和引用的區別我們這里不做過多的說明,他們本質上都是指向儲存器中的一塊內存地址)。在java語言描述的鏈表中,上面所說的“指針域”更準確的說是“引用域”,也就是說,這里的x.next實際上是X的下一個節點x->next元素的引用。說的更直白一點,我們可以簡單的理解為x.next = x->next,就像我們經常做的Button mbutton = new Button();
一樣,在實際操作中我們處理mbutton這個引用實際上就是在處理new Button()對象。
??我們先說刪除的做法,如上圖所示,我們假設一個x為鏈表節點,他的前一個結點為x->prev,后一個結點為x->next,我們用x.next表示他的后繼引用。現在我們要刪除x結點,需要怎么做呢?實際上很簡單,直接讓前一個結點元素的后繼引用指向x的下一個節點元素(向后跳過x)就可以了x->prev.next = x.next
。
??同理插入一個節點呢?首先我們把Node的后繼節點Next的變成P的后繼節點,接著將Node的后繼引用指向P,用代碼表示就是:P.next = Node.next; Node.next = P;
。解釋一下這兩句代碼,P.next = Node.next;
實際上就是用Node.next的引用覆蓋了P.next的引用,P.next的引用本來是一個空引用(剛開始還沒插入鏈表不指向任何節點),而Node.next本來是指向Next節點的,這一步操作完之后實際上P.next這個引用就指向了Next節點;這個時候Node.next = P;
這句代碼中,我們將Node.next這個引用重新賦值,也就是指向了P這個節點,這樣整個插入過程就完成了。
??為什么我們啰里啰嗦的為兩句代碼解釋了一堆內容呢?就是要強調,上面兩個步驟順序是不能顛倒的!為什么呢?我們不妨顛倒順序看一下——我們首先進行Node.next = P;
這一步,開始的時候,P的引用域是空的(或者指向無關的地址),此時如果我們進行了這一步,那么Node.next這個引用就指向了P節點,這個時候我們再進行P.next = Node.next
這一步就相當于P.next = p
,p.next引用域指向自己,這沒有任何意義。
(2)雙鏈表(LinkedList)
上面我們說了簡單鏈表的各種事項,但是在實際的運用中,為了我們的鏈表更加靈活(比如既可以工作指針后移向后查找,也可以指針向前移動查詢),我們運用更多的是雙向鏈表,即每個節點持有前面節點和后面節點的引用。java的雙向鏈表通過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;
}
}
Java雙鏈表中的結點是通過一個靜態內部類定義。一個結點包含自身元素(item),該節點對后一個節點的引用(next),該節點對前一個節點的引用(prev)。
(2.1)雙鏈表的刪除
如圖,我們假設在一個雙向鏈表中有一個節點X,他的前繼節點是prev,后繼節點是next.現在我們展示刪除節點X的源碼(sources/ansroid-24/java/util/LinkedList):
public boolean remove(Object o) { //刪除分為兩種情況,一種是刪鏈表中的null元素,一種是刪正常元素
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;
}
E unlink(Node<E> x) { //上面是找元素,這個方法是真正刪除元素
final E element = x.item; //x.item表示當前的x節點
final Node<E> next = x.next; //x.next表示x后繼引用,next同
final Node<E> prev = x.prev; //x.prev是x的前繼引用,prev同
......
if (prev == null) { //如果prev為null,則表示x為第一個結點,此時刪除x的做法只需要將x的
first = next; //下一個節點設為第一個節點即可,first表示鏈表的第一節點。
} else {
① prev.next = next; //否則的話,x為普通節點。那么只要將x的前一個結點(prev)的后繼引用指向x的下一個
x.prev = null; //節點就行了,也就是(向后)跳過了x節點。x的前繼引用刪除,斷開與前面元素的聯系。
}
if (next == null) {
last = prev; //如果x節點后繼無人,說明他是最后一個節點。直接把前一個結點作為鏈表的最后一個節點就行
} else {
② next.prev = prev; //否則x為普通節點,將x的下一個節點的前繼引用指向x的前一個節點,也就是(向前)跳過x.
x.next = null; //x的后繼引用刪除,斷了x的與后面元素的聯系
}
x.item = null; //刪除x自身
size--; //鏈表長度減1
modCount++;
return element;
}
我們在在上面的源碼中標出了刪除一個元素所需要的兩個步驟,即prev節點中原本指向X節點的后繼引用,現在向后越過X,直接指向next節點①(prev.next = next;);next節點中原本指向X節點的前繼引用,現在向前越過X節點,直接指向prev節點。;然后就是將X的前繼引用和后繼引用都設置為null,斷了他與前后節點之間的聯系;最后將X節點本身置為null,方便內存的釋放。
(2.2)雙鏈表的插入
這里我們選取較為容易的,在指定位置添加一個元素的add方法分析(sources/ansroid-24/java/util/LinkedList):
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size) //size表示整個鏈表的長度,如果指定的索引等于鏈表的長度,那么就把這個元素添加到鏈表末尾
linkLast(element);
else //否則執行linkBefore()方法,添加到末尾之前的元素前面
linkBefore(element, node(index));
}
這里我們看一下這個node(index)方法:
/**
* Returns the (non-null) Node at the specified element index.返回指定索引處的非空元素
*/
Node<E> node(int index) {
if (index < (size >> 1)) { //size >> 1,表示二進制size右移一位,相當于size除以2
Node<E> x = first;
for (int i = 0; i < index; i++) //如果指定的索引值小于size的一般,那么從第一個元素開始,
x = x.next; //指針向后移動查找,一直到指定的索引處
return x;
} else { //else,從最后一個元素開始,向前查找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
/**
* Inserts element e before non-null Node succ.
*/
void linkBefore(E e, Node<E> succ) { //e為我們要插入的元素,succ為我們要插入索引處的元素
final Node<E> pred = succ.prev; //先將succ的前繼引用(或者前一個結點的元素)保存在pred變量中
final Node<E> newNode = new Node<>(pred, e, succ); //創建一個新節點,也就是我們要插入的這個元素
//注意new Node<>(pred, e, succ);這三個參數,可以參照(1.3)處的源碼
① succ.prev = newNode; //succ的前繼引用指向NewNode
if (pred == null) //如果這個前繼引用為空,那就說明我們插入的元素是在最前面
first = newNode; //直接讓newNode做第一個元素就行了
else
② pred.next = newNode; //否則的話,在讓pred的后繼引用指向newNode節點
size++;
modCount++;
}
可以看到這里實際上和簡單鏈表的添加一樣,也是分兩步走,而且兩步的順序不能顛倒。這里需要說明的一點是,我們在圖上用灰色的點線多畫了兩條箭頭,其中①②分別表示的是新元素的前繼引用和后繼引用分別指向pred和succ的賦值過程。在筆者參考的《大話數據結構》中,雙鏈表的添加算上點線的那兩步一共是四步,但實際上在java中創建newNode的時候new Node<>(pred, e, succ)
這句代碼已經一次性做完了上述兩部過程,真正做事的就是我們在圖中用黑線標出的那兩步。
三.JAVA中相關的collection集合
java集合主要由兩個接口派生而出——Collection接口和Map接口。collection儲存一組類型相同的對象,且每個位置只能保存一個元素;Map保存的是鍵值對(key-value)。我們本節重點來說Collection集合。
??對于collection接口中的方法,我我們可以通過他的源碼來看:
public interface Collection<E> extends Iterable<E> {
int size(); //@return the number of elements in this collection
boolean isEmpty(); //@return <tt>true</tt> if this collection contains no elements
boolean contains(Object o); //Returns <tt>true</tt> if this collection contains the specified element.
Object[] toArray(); //Returns an array containing all of the elements in this collection.
boolean remove(Object o); //<tt>true</tt> if an element was removed as a result of this call
boolean add(E e); //<tt>true</tt> if this collection changed as a result of the call
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
void clear();
boolean equals(Object o);
......
}
上面我們列出了Collection集合的幾個主要的方法,從注釋中我們可以很清楚的知道他們的用途。
1.Iterable/Iterator接口
我們看上面的collection接口的時候,collection接口繼承了Iterable類,我們來看看這個Iterable類:
public interface Iterable<T> {
/**
* Returns an iterator over elements of type {@code T}.
*
* @return an Iterator.
*/
Iterator<T> iterator();
/*
* @param action The action to be performed for each element
* @throws NullPointerException if the specified action is null
* @since 1.8
*/
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
......
}
我們先看一下第二個函數forEach(Consumer<? super T> action),在java8中,Iterator新加了這個個forEach循環(注意與java5中的foreach循環的區別,大小寫,用法不同),主要用于更加方便的循環遍歷集合中的元素并對每一個元素迭代做出相應的處理,其中的參數Consumer<? super T> action就是我們對集合中的元素所施加的操作。舉例說明具體的用法:
傳統方式循環List:
List<String> items = new ArrayList<>();
items.add("A");
items.add("B");
items.add("C");
items.add("D");
items.add("E");
for(String item : items){
System.out.println(item);
}
在java8中使用forEach循環+Lambda表達式遍歷list:
List<String> items = new ArrayList<>();
items.add("A");
items.add("B");
items.add("C");
items.add("D");
items.add("E");
//lambda
//Output : A,B,C,D,E
items.forEach(item->System.out.println(item));
//Output : C
items.forEach(item->{
if("C".equals(item)){
System.out.println(item);
}
});
回到Iterable類中,我們可以看到他還返回了一個Iterator接口的實例。而這個接口類是什么呢?
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
可以看到,該接口類中一共有四個方法。其中forEachRemaining()方法是java8之后新增的方法,主要的作用和上面我們說過的foreach()循環是一樣的,用法也基本相同。
??Iterator類的作用是一個迭代器,主要用于操作colloection集合類中的元素。Iterator類必須依附于Collection對象,Iterator本身并不提供盛裝對象的能力。如果需要創建Iterator對象,則必需有一個被迭代的集合,如果沒有Colletion集合,Iterator也沒有存在的價值。
??Iterator類中的結果方法——hasNext(),用于判斷在遍歷時collection集合中是否還有剩下的元素;next()用于返回遍歷時集合中的下一個元素,這兩個方法很好理解。唯獨這個remove()方法有一些注意事項需要說明一下:
??我們可以先看看這個方法的注釋:
/**
* Removes from the underlying collection the last element returned
* by this iterator (optional operation). This method can be called
* only once per call to {@link #next}.
* ......
*
* @throws IllegalStateException if the {@code next} method has not
* yet been called, or the {@code remove} method has already
* been called after the last call to the {@code next}
* method
*/
翻譯一下:從底層集合中刪除此迭代器返回的最后一個元素。這個方法只能在每一次調用完next()方法之后調用一次。如果next()方法沒有調用,或者remove()方法在上一次next()方法調用完了之后,又調用了一次,則會拋出IllegalStateException異常。
結合這段注釋,我們交代一下next()方法調用的幾個注意事項:
??①remove()只能在next()方法調用完后緊接著調用一次,此時他刪除的是在他之前調用的那個next()返回的元素
??②remove()在調用之心完之后,只能等待下一次next()方法執行完了之后,才可以調用。
同時我們還需要注意:③在使用Iterator迭代的過程中,我們不能手動修改collection集合中的元素,即不能手動調用collection類本身的remove(),add(),clear()等方法,只能調用Iterator的remove()方法。舉個例子:
public class IteratorTest{
public static void main(String[] args){
...
Iterator it = books.iterator();
while(it.hasNext()){
String book = (String)it.next();
if("book.equals("hhhhhh"))
it.remove(); //刪除上一次next()返回的元素,也就是"hhhhhh"
book = "666666"; *
}
}
}
上面是一段"正常"的程序,這里需要說明的一點,上一段代碼星號處我們將book的賦值為“666666”,但是當我們再次打印出books的集合時會發現集合中的元素沒有什么改變,還是原來的值。這說明——當使用Iterator迭代器進行迭代時,Iterator并不是把集合元素本身傳遞給了迭代變量,而是把集合元素的額值出給了迭代變量,因此我們在后邊進行的各種賦值并不影響集合本身的元素。
public class IteratorTest{
public static void main(String[] args){
...
Iterator it = books.iterator();
while(it.hasNext()){
String book = (String)it.next();
if("book.equals("hhhhhh"))
books.remove(book); //錯誤,在迭代時調用了修改集合本身的方法
}
}
}
這是一段有問題的代碼。這里還需要注意一點,這個Iterator中的remove()和collection本身的remove(o)方法是不一樣的,一個有參數一個無參數。而且子刪除集合中元素時,我們優先考慮用Iterator的remve()方法,因為他有更高的效率,為什么呢?這里我們先作為一個遺留問題,后面我們再詳細證明。
??同樣我們可以用java5中的foreach循環來遍歷collection集合中的元素,這個更加簡潔一些:
public class ForeachTest{
public static void main(String[] args){
...
for(Object obj : books){
String book = (String)it.next(); //此處的book也不是變量本身
if("book.equals("hhhhhh"))
books.remove(book); //錯誤,在迭代時調用了修改集合本身的方法
}
}
}
上面加注釋的兩個地方,是foreach循環與Iterator迭代類似的地方。
2.List接口,ArrayList類,LinkedList類
colection接口有三個子類實現接口:List(有序集合,元素可以重復),Queue(對列),Set(無序集合,元素不可重復),其中List又有兩個最常見的實現類:ArrayList(動態數組)和LinkedList(雙向鏈表),這兩個也就是我們前面一直說的線性表的順序儲存結構和鏈式儲存結構。List接口作為collection接口的子類,當然實現了collection接口中的所有方法。并且由于List是有序集合,因此List集合中增加了一些根據元素索引來操作集合的方法。
(1)List源碼解析
public interface List<E> extends Collection<E> {
...... //省略一些collection中展示過的方法
/**同時List接口定義了一些自己的方來實現“有序”這一功能特點*/
/**
*返回列表中指定索引的元素
*/
E get(int index);
/**
*設定某個列表索引的值
*/
E set(int index, E element);
/**
*在指定位置插入元素,當前元素和后續元素后移
*這是可選操作,類似于順序表的插入操作
*/
void add(int index, E element);
/**
* 刪除指定位置的元素(可選操作)
*/
E remove(int index);
/**
* 獲得指定對象的最小索引位置,沒有則返回-1
*/
int indexOf(Object o);
/**
* 獲得指定對象的最大索引位置
* 可以知道的是若集合中無給定元素的重復元素下
* 其和indexOf返回值是一樣的
*/
int lastIndexOf(Object o);
/**
*一種更加強大的迭代器,支持向前遍歷,向后遍歷插入刪除操作
*/
ListIterator<E> listIterator(); *
ListIterator<E> listIterator(int index); *
/**
* 返回索引fromIndex(包括)和toIndex(不包括)之間的視圖。
*/
List<E> subList(int fromIndex, int toIndex);
}
這里解釋一下ListIterator類,該類繼承自Iterator類,提供了專門操作List的方法。ListIterator接口在Iterator接口的基礎上增加了洗下面幾個關鍵的方法:
public interface ListIterator<E> extends Iterator<E> {
boolean hasNext();
E next();
void remove();
/**下面是在Iterator基礎上增加的方法*/
boolean hasPrevious(); //是否還有前繼元素
E previous(); //返回前一個元素
int nextIndex(); //返回下一個元素的索引
int previousIndex(); //返回前一個元素的索引
void set(E e); //替換由上一次next()或者previous()方法返回的元素.
void add(E e); //在上一次由next()方法返回的元素之前,或者在上一次previous()方法返回的元素之后,添加一個元素
}
可以看到,ListIterator增加了向前迭代的功能(Iterator只能向后迭代),而且ListIterator可以通過add()方法向List中添加元素(Iterator只能刪除)。
(2)ArrayList源碼解析
(2.1)ArrayList類的頭部:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
RandomAccess:RandmoAccess是一個標記接口,用于被List相關類實現。他主要的作用表明這個相關類支持快速隨機訪問。在ArrayList中,我們即可以通過元素的序號快速獲取元素對象——這就是快速隨機訪問。稍后,我們會比較List的“快速隨機訪問”和“通過Iterator迭代器訪問”的效率。
Cloneable:實現該接口的類可以對該類的實例進行克隆(按字段進行復制)
Serializable:ArrayList支持序列化,能通過序列化去傳輸。
(2.2)ArrayList屬性
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10; //默認的初始容量
private static final Object[] EMPTY_ELEMENTDATA = {}; //共享的空數組實例
/**
* 儲存ArrayList元素的數組緩沖區。ArrayList的容量就是該緩沖數組的長度。任何以EMPTY_ELEMENTDATA作為
* 數據元素的空ArrayList,在他們添加第一個元素的時候,都將被擴展至DEFAULT_CAPACITY(默認為10)長度。
*/
transient Object[] elementData;
private int size; //ArrayList的大小,即他所包含的元素的個數
從ArrayList的屬性元素我們可以看出,他的內部是由一個數組(elementData)實現的。這里需要說明一下transient Object[] elementData;
這句中的transient關鍵字:
??我們都知道一個對象只要實現了Serilizable接口,這個對象就可以被序列化,java的這種序列化模式為開發者提供了很多便利,我們可以不必關系具體序列化的過程,只要這個類實現了Serilizable接口,這個類的所有屬性和方法都會自動序列化。
??然而在實際開發過程中,我們常常會遇到這樣的問題,這個類的有些屬性需要序列化,而其他屬性不需要被序列化,打個比方,如果一個用戶有一些敏感信息(如密碼,銀行卡號等),為了安全起見,不希望在網絡操作(主要涉及到序列化操作,本地序列化緩存也適用)中被傳輸,這些信息對應的變量就可以加上transient關鍵字。換句話說,這個字段的生命周期僅存于調用者的內存中而不會寫到磁盤里持久化。
??總之,java 的transient關鍵字為我們提供了便利,你只需要實現Serilizable接口,將不需要序列化的屬性前添加關鍵字transient,序列化對象的時候,這個屬性就不會序列化到指定的目的地中。
(3.1)ArrayList構造方法
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA; //EMPTY_ELEMENTDATA等于10,前面說過
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
可以看到ArrayList有三種構造方法:
??①指定長度的初始化
??②初始化一個空ArrayList,此時會自動將該初始化的ArrayList長度變為默認長度10。
??③將指定的集合作為參數提供給初始化的ArrayList,并在該構造函數內部,先通過toArray將該集合強制轉化為Object類型,然后通過Arrays.copyOf方法將其賦給elementData,也就是ArrayList緩存元素的地方。
(3.4)ArrayList的增加
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 擴容檢查,確保當前數組在擴容之后可以容納它的參數個大小的元素
elementData[size++] = e; // 將e增加至list的數據尾部,容量+1
return true;
}
public void add(int index, E element) { //在指定位置添加一個元素
if (index > size || index < 0) //判斷是否越界
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
// 對數組進行復制處理,目的就是空出index的位置插入element,并將index后的元素位移一個位置
ensureCapacityInternal(size + 1); // 擴容檢查
System.arraycopy(elementData, index, elementData, index + 1,size - index);
elementData[index] = element; //將元素添加到指定的位置
size++; //容量+1
}
public boolean addAll(Collection<? extends E> c) { //將整個collection集合和添加到List結尾
Object[] a = c.toArray(); //將c轉化成Object類數組
int numNew = a.length;
ensureCapacityInternal(size + numNew); //擴容檢查
System.arraycopy(a, 0, elementData, size, numNew); //將c添加到list尾部
size += numNew; //更新當前容器大小
return numNew != 0;
}
public boolean addAll(int index, Collection<? extends E> c) { //在指定額索引處添加整個集合
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // 擴容檢查
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;
}
這里我們需要注意的是~~在數組的增加過程中,有兩個過程是是比較耗費性能的:數組擴容(ensureCapacityInternal)與數組復制(System.arraycopy),這兩個步驟在上面四個添加方法中都存在,待會我們會詳細分析。
(3.5)ArrayList的刪除
public E remove(int index) { //根據索引位置刪除元素
if (index >= size) //越界檢查
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;
E oldValue = (E) elementData[index]; //去除要刪除的元素,該方法最終會返回這個值
int numMoved = size - index - 1; //計算數組要復制的值的數量
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,numMoved); //復制數組
//數組最后一個元素置空,讓垃圾回收器工作。因為刪了一個元素,index之后的元素都往前移動了一個位置
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
public boolean remove(Object o) { //根據內容刪除,只刪除匹配的那個
if (o == null) { //對要刪除的元素進行是否為null的判斷
for (int index = 0; index < size; index++) //遍歷數組,掘地三尺找這個要刪除的null元素
if (elementData[index] == null) { //找到null值了.(注意這個null值需要用"=="判斷)
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) { //非null是用.equals()比較
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
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
}
增加和刪除使我們在ArrayLisy中比較常用的兩個方法。下面我們來說說上面遺留的那個關于擴容和復制的問題。首先我們來看看ensureCapacityInternal方法的源碼:
private void ensureCapacityInternal(int minCapacity) { //minCapacity就是我們需要的最小容量
if (elementData == EMPTY_ELEMENTDATA) { //如果此時elementData等于空數組
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); //如果minCapacity比默認值10小,則
//minCapacity為10,否則為他自己。
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
//如果當前的數組長度小于我們所需要的minCapacity值(當前數組長度不夠),則進行擴容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //oldCapacity等于當前數組的長度
//oldCapacity >> 1,表示二進制的向右移一位,相當于十進制的除以2
int newCapacity = oldCapacity + (oldCapacity >> 1); //newCapacity = 1.5 * oldCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity; //如果此時newCapacity還是小于我們所需要的minCapacity,那就讓他等于minCapacity
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//以這個新的長度為標準重新創建,將原來數組中的元素拷貝一份到新的數組中去。Arrays.copyOf底層實現是System.arrayCopy()
elementData = Arrays.copyOf(elementData, newCapacity);
}
擴容的方法中包含三個個過程:
??①判斷需要的大小(minCapacity)是否超出了默認長度10.
??②超出了就開始擴容,用他的1.5倍長度去和minCapacity作比較(有些java版本是2.5倍)。
??③如果1.5倍大小還是小于所需要的minCapacity大小,那就將原來的元素復制到一個以minCapacity為長度的新數組中,并將elementData引用指向這個新數組。
??可以看到,擴容的過程伴隨著數組的復制。如果數組初試容量過小,假設為默認的10個大小,而我們使用ArrayList的操作時需要不斷的增加元素。在這種場景下,不斷的增加,意味著數組需要不斷的進行擴容,擴容的過程就是數組拷貝System.arraycopy的過程,每一次擴容就會開辟一塊新的內存空間和數據的復制移動(但是數組復制不需要開辟新內存空間,只需將數據進行復制),這樣勢必對性能造成影響。那么在這種以寫為主(寫會擴容,刪不會縮容)場景下,提前預知性的設置一個大容量,便可減少擴容的次數,提高了性能。需要注意一點的是ensureCapacity()方法是public的,我們可以通過它手動的增加容量。
??增加元素可能會進行擴容,而刪除元素卻不會進行縮容,如果在以刪除為主的場景下使用list,一直不停的刪除而很少進行增加,或者數組進行一次大擴容后,我們后續只使用了幾個空間,那就會造成控件的極大浪費。這個時候我們就可以將底層的數組elementData的容量調整為當前實際元素的大小(縮容),來釋放空間。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = Arrays.copyOf(elementData, size);
}
}
總結一下:
??ArrayList底層以數組實現,允許重復,默認第一次插入元素時創建數組的大小為10,超出限制時會增加50%的容量,每次擴容都底層采用System.arrayCopy()復制到新的數組,初始化時最好能給出數組大小的預估值(采用給定值初始化)。
(3.6)ArrayList的遍歷方式
ArrayList支持三種遍歷方式
①通過迭代器Itertor去遍歷
②隨機訪問,通過索引值去遍歷,由于ArrayList實現了RandomAccess接口,它支持通過索引值去隨機訪問元素。
③foreach循環遍歷
??下面我們用三段程序來測試這三種遍歷方法哪一個效率最高,同時展示三種遍歷的寫法。為了測量更為精準,我們新建三個類分別測試——randomAccessTest.java;iteratorTest.java;foreachTest.java。同時我們采用多次測量(筆者用的eclipse測試時,測試結果經常跳票)并采用納秒計時(毫秒誤差慘不忍睹)。
public class randomAccessTest {
private static long startTime;
private static long endTime;
public static void main(String[] args){
List list = new ArrayList();
for(int i=0; i<1000; i++){
list.add(i);
}
startTime = System.nanoTime();
randomAccess(list);
endTime = System.nanoTime();
long time = endTime - startTime;
System.out.println("randomAccessTime = " + time + "ns");
}
public static void randomAccess(List list){
for(int i=0; i<list.size(); i++){
}
}
}
public class iteratorTest {
private static long startTime;
private static long endTime;
public static void main(String[] args){
List list = new ArrayList();
for(int i=0; i<1000; i++){
list.add(i);
}
startTime = System.nanoTime();
iterator(list);
endTime = System.nanoTime();
long time = endTime - startTime;
System.out.println("iteratorTime = " + time + "ns");
}
public static void iterator(List list){
Iterator iter = list.iterator();
while( iter.hasNext()) {
iter.next();
}
}
}
public class foreachTest {
private static long startTime;
private static long endTime;
public static void main(String[] args){
List list = new ArrayList();
for(int i=0; i<1000; i++){
list.add(i);
}
startTime = System.nanoTime();
foreach(list);
endTime = System.nanoTime();
long time = endTime - startTime;
System.out.println("foreachTime = " + time + "ms");
}
public static void foreach(List list){
for(Object obj:list) {
}
}
}
最終的結果大致穩定為:
randomAccessTime = 7x10^5 ns
iteratorTime = 6x10^6ns
foreachTime = 5x10^6ns
可以看到,雖然結果經常跳票,但八九不離十,randomAccessTime顯然是用時最快的,畢竟少了一個數量級,這點機器還是沒有算錯的。也就是說遍歷ArrayList時,使用隨機訪問(即,通過索引序號訪問)效率最高,這點毋庸置疑,使用迭代器遍歷的效率最低(這點是網上的答案,由于兩者的測試結果處于同一個數量級,加上機器誤差,這點筆者很難證實,讀者可以自行驗證)。
??其實產生上面結果,我們并不感到意外,因為關于randomAccessTime這個接口的注釋中就已經很明確的說明了這個問題:
/**
* ......
* As a rule of thumb, a
* <tt>List</tt> implementation should implement this interface if,
* for typical instances of the class, this loop:
* <pre>
* for (int i=0, n=list.size(); i < n; i++)
* list.get(i);
* </pre>
* runs faster than this loop:
* <pre>
* for (Iterator i=list.iterator(); i.hasNext(); )
* i.next();
* </pre>
* /
* 根據經驗,一個list類的實現類應當實現這個接口,對于典型的該類的實例,上面的循環快于下面的循環。
(3)LinkedList源碼解析
上面我們在講雙鏈表的時候已經講了linkedList的remove(),add()等關鍵方法,以及LinkedList的一個結點(Node)的構成。下面我們來講一下LinkedList剩余的一些知識點:
(3.1)LinkedList的頭
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
可以看到LinkedList是一個繼承自AbstractSequentialList的雙鏈表,他可以被當做堆棧,隊列(實現了List接口),
雙端隊列(實現了Deque接口)使用。同時LinkedList 實現了Cloneable接口,即覆蓋了函數clone(),能克隆。LinkedList 實現java.io.Serializable接口,這意味著LinkedList支持序列化,能通過序列化去傳輸。
(3.2)LinkedList的屬性元素
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
其中size就是list的數量,和ArrayList一樣。這個Node<E> first和Node<E> last就是節點的前繼引用和后繼引用,Node表示鏈表上的一個結點。這里再貼一遍代碼,免得你忘了:
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;
}
}
(3.3)LinkedList的構造函數
/**構造一個空的構造函數,這個構造函數,也真夠空的**/
public LinkedList() {
}
/**構造一個包含指定collection元素的表,這些元素按照collection的迭代器返回的順序排列**/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
LinkedList就兩個構造函數,一個空構造函數,一個包含指定collection的構造函數。
(3.4)LinkedList的增加方法
上面我們在講雙鏈表的時候講過在指定位置插入元素的add(int index, E element)方法,現在我們補充一下其他幾種添加方法:
①在雙鏈表的尾部添加一個元素:
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last; //last表示雙鏈表中的最后一個元素,l表示指向最后一個元素的引用
final Node<E> newNode = new Node<>(l, e, null); //新建一個后繼引用為空的新節點,后繼為空,意味著他是最后一個元素
last = newNode; //直接讓這個新建的元素作為鏈表的最后一個元素就行了
if (l == null) //指向原本鏈表最后一個元素的引用l為空,說明原來的鏈表是一個空鏈表。
first = newNode; //此時讓這個新建的結點元素最為第一個結點(就他一個啊)
else
l.next = newNode; //否則的話,讓原鏈表的后繼引用指向我們新建的這個節點,插入完成
size++;
modCount++;
}
②在指定索引處插入一個集合
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index); //越域判斷
Object[] a = c.toArray();
int numNew = a.length; // 要插入元素的個數
if (numNew == 0)
return false;
Node<E> pred, succ; //申明兩個節點類的前繼,后繼引用(這個循環就是在找出原列表中插入索引處的前后引用)
if (index == size) { //索引等于鏈表的長度,說明是要求插在鏈表的最后面
succ = null;
pred = last; //此時后繼引用為空,前繼引用指向原鏈表的最后一個元素
} else {
succ = node(index); //否則后繼引用指向原鏈表索引處的元素。node()方法我們之前講過,二分法查找索引處元素
pred = succ.prev; //前繼引用指向原鏈表索引處元素的前一個元素,完成插入
}
for (Object o : a) { //遍歷我們要插入的這個集合
E e = (E) o;
//新建一個結點,以pred作為前繼引用,該for循環中每次遍歷得到的collection集合中的e作為結點本身元素,
//null作為后繼引用。在第一次進行該循環時,通過上一個if的賦值,pred指向原鏈表中指定索引處的前一個元素。
Node<E> newNode = new Node<>(pred, e, null); //在第一次循環遍歷的時候,這個新節點就是collection集合的第一個元素
if (pred == null) //如果pred為空,說明原鏈表為空
first = newNode; //新節點等于第一個節點
else
pred.next = newNode; //否則,原鏈表中指定索引處的前一個元素 的后繼引用指向這個新節點。
pred = newNode; //然后將這個pred指向這個新的節點(在第一次循環中,這個新節點表示collection集合中的第一個元素),相當于工作指針后移,然后重復上述過程。
}
//上述循環結束后,pred指針已經指向了collection集合的最后一個元素,此時由于succ沒動過,因此他還是指向原鏈表中指定索引處的后一個元素
if (succ == null) { //如果succ為null,和第一個if中的情況一樣,說明這是在原鏈表的末尾插入元素
last = pred; //直接讓此時的pred也就是collection中的最后一個元素作為插入后鏈表的最后一個元素就可以了
} else { //否則的話說明是在原鏈表中間插入元素
pred.next = succ; //此時讓collection中最后一個元素的后繼指針指向 原鏈表索引處的后一個元素,完成差誒工作
succ.prev = pred; //同時讓succ的前繼指針指向collection的最后一個元素,完成雙向鏈表的建立
}
size += numNew; //增加鏈表長度
modCount++;
return true;
}
這段代碼中,注釋已經寫的很詳細了,難點就是for (Object o : a)這個foreach循環中,我們插入的collection元素是如何建立雙鏈表聯系的,讀者務必要仔細分析流程。
(3.5)LinkedList的刪除方法
??刪除的方法我們上面講雙鏈表的時候已經說的很詳細了,這個沒有什么好說的,大家可以翻上去復習一下。這里我們通過LinkedList的remove()方法的幾種形式,來講一下算法選型的問題。
??這個例子的來源于《數據結構與算法分析(java語言描述)》這本書,筆者覺得很典型。題目的要求是,給出一個線性表,將表中所有偶數項全部刪除。
①首先第一種算法,使用普通for循環:
public class usuallyforTest {
private static long startTime;
private static long endTime;
private static final int SIZE = 10000;
public static void main(String[] args){
List<Integer> arraylist = new ArrayList<Integer>(SIZE);
List<Integer> linkedlist = new LinkedList<Integer>();
for(int i=0; i<SIZE; i++){
linkedlist.add(i);
arraylist.add(i);
}
startTime = System.currentTimeMillis();
usuallyfor(linkedlist);
endTime = System.currentTimeMillis();
long linkedlistTime = endTime - startTime;
System.out.println("usuallyforLinkedlistTime = " + linkedlistTime + "ms");
startTime = System.currentTimeMillis();
usuallyfor(arraylist);
endTime = System.currentTimeMillis();
long arraylistTime = endTime - startTime;
System.out.println("usuallyforArraylistTime = " + arraylistTime + "ms");
}
public static void usuallyfor(List<Integer> list){
for(int i=0; i<list.size(); i++){
if(list.get(i) % 2 == 0){
list.remove(i);
}
}
}
}
運行的結果是:
usuallyforLinkedlistTime = 57ms
usuallyforArraylistTime = 7ms
如果我們將其中的線性表大小SIZE改為20000(擴大兩倍),得到結果為:
usuallyforLinkedlistTime = 232ms
usuallyforArraylistTime = 29ms
很顯然,對于ArrayList和LinkedList而言,這個算法都是時間復雜度為O(N^2)的二次算法。
public static void usuallyfor(List<Integer> list){
for(int i=0; i<list.size(); i++){
if(list.get(i) % 2 == 0){
list.remove(i);
}
}
}
這段代碼中,對于LinkedList而言,list.get(i)方法是O(N)時間,慢在尋址;而他的remove()方法確實O(1)的。對于ArrayList而言,他的get(i)方法是O(1)的,但是remove(i)方法卻是O(N)的,因為只要刪除一個元素,后面的元素都要跟著向前移位,并伴隨著數組的復制拷貝等耗時操作;但是他的get(i)卻是O(1)的。
??所以無論對誰,這種算法都不是明智的選擇。但是整體上來看,ArrayList用時更少一些(1/8的量)。
②使用迭代器Iterator
public class iteratorTest {
private static long startTime;
private static long endTime;
private static final int SIZE = 10000;
public static void main(String[] args){
List<Integer> arraylist = new ArrayList<Integer>(SIZE);
List<Integer> linkedlist = new LinkedList<Integer>();
for(int i=0; i<SIZE; i++){
linkedlist.add(i);
arraylist.add(i);
}
startTime = System.currentTimeMillis();
iterator(linkedlist);
endTime = System.currentTimeMillis();
long linkedlistTime = endTime - startTime;
System.out.println("iteratorLinkedlistTime = " + linkedlistTime + "ms");
startTime = System.currentTimeMillis();
iterator(arraylist);
endTime = System.currentTimeMillis();
long arraylistTime = endTime - startTime;
System.out.println("iteratorArraylistTime = " + arraylistTime + "ms");
}
public static void iterator(List<Integer> list){
Iterator<Integer> iter = list.iterator();
while( iter.hasNext()) {
if(iter.next() % 2 == 0){
iter.remove();
}
}
}
}
結果為:
iteratorLinkedlistTime = 2ms
iteratorArraylistTime = 10ms
將其中的線性表大小SIZE改為20000(擴大兩倍),得到結果為:
iteratorLinkedlistTime = 4ms
iteratorArraylistTime = 34ms
顯然,此時LikedList變成了O(N)一次時間,而ArrayList變成了O(N^2)二次時間,并且LinkedList所用的時間大大小于ArrayList所用的時間。為什么呢?因為此時用迭代器循環遍歷時,對于linkdList的next()方法,是O(1)時間關系,remove()也是一次時間關系;但是對于ArrayList而言,rwmove()仍然是O(N)時間關系。
??從這兩個例子中我們可以體驗到,算法的強大之處,實現同樣功能,采用不同的算法,成敗異變,功業相反~~妙哉!
3.總結——ArrayList和LinkedList的比較
寫的太多了,不知道該怎么總結,這里直接照搬一下Java集合干貨系列-(二)LinkedList源碼解析這篇文章最后的總結,這篇文章寫得很好,推薦讀者去看一下
??(1)順序插入速度ArrayList會比較快,因為ArrayList是基于數組實現的,數組是事先new好的,只要往指定位置塞一個數據就好了;LinkedList則不同,每次順序插入的時候LinkedList將new一個對象出來,如果對象比較大,那么new的時間勢必會長一點,再加上一些引用賦值的操作,所以順序插入LinkedList必然慢于ArrayList
??(2)基于上一點,因為LinkedList里面不僅維護了待插入的元素,還維護了Entry的前置Entry和后繼Entry,如果一個LinkedList中的Entry非常多,那么LinkedList將比ArrayList更耗費一些內存
??(3)數據遍歷的速度,看最后一部分,這里就不細講了,結論是:使用各自遍歷效率最高的方式,ArrayList的遍歷效率會比LinkedList的遍歷效率高一些
??(4)有些說法認為LinkedList做插入和刪除更快,這種說法其實是不準確的:
??①LinkedList做插入、刪除的時候,慢在尋址,快在只需要改變Node前后引用
??②ArrayList做插入、刪除的時候,慢在數組元素的批量copy,快在尋址
??所以,如果待插入、刪除的元素是在數據結構的前半段尤其是非常靠前的位置的時候,LinkedList的效率將大大快過ArrayList,因為ArrayList將批量copy大量的元素;越往后,對于LinkedList來說,因為它是雙向鏈表,所以在第2個元素后面插入一個數據和在倒數第2個元素后面插入一個元素在效率上基本沒有差別,但是ArrayList由于要批量copy的元素越來越少,操作速度必然追上乃至超過LinkedList。
從這個分析看出,如果你十分確定你插入、刪除的元素是在前半段,那么就使用LinkedList;如果你十分確定你刪除、刪除的元素在比較靠后的位置,那么可以考慮使用ArrayList。如果你不能確定你要做的插入、刪除是在哪兒呢?那還是建議你使用LinkedList吧,因為一來LinkedList整體插入、刪除的執行效率比較穩定,沒有ArrayList這種越往后越快的情況;二來插入元素的時候,弄得不好ArrayList就要進行一次擴容,記住,ArrayList底層數組擴容是一個既消耗時間又消耗空間的操作。
站在巨人的肩膀上摘蘋果:
Java集合干貨系列-(一)ArrayList源碼解析
Java集合干貨系列-(二)LinkedList源碼解析
《大話數據結構》
《數據結構與算法分析(java語言描述)》
《瘋狂java講義》