ArrayList與LinkedList

在Java的List類型集合中,ArrayList和LinkedList大概是最常用到的2個了,細看了一下它們的實現,發現區別還是很大的,這裏簡單的列一下個人比較關心的區別。

類聲明

ArrayList

1
2
3
4
public class ArrayList<E>
        extends AbstractList<E>
        implements List<E>, RandomAccess,
              Cloneable, java.io.Serializable

LinkedList

1
2
3
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

二者的定義有些相近,除了都實現List、Cloneable和Serializable以外,繼承的類不一樣,以及接口有細微的區別。

1
public abstract class AbstractSequentialList<E> extends AbstractList<E>

AbstractSequentialList也繼承自AbstractList,它只是多了一些實現的方法,參照API的doc,這個類用於按順序訪問的List的實現,所謂順序訪問(sequential access),可以與隨即訪問(random access)的ArrayList對比去理解。

Deque是一個雙向(double ended queue)的Queue的接口,因爲這個接口的區別,LinkedList裏實現的方法要比ArrayList多一些。

元素存儲方式

ArrayList:採用數組方式

1
private transient Object[] elementData;

LinedList:採用鏈表

1
2
3
4
5
6
7
8
9
10
11
12
13
private transient Entry<E> header = new Entry<E>(null, null, null);
private static class Entry<E> {
    E element;
    Entry<E> next;
    Entry<E> previous;
    Entry(E element, Entry<E> next, Entry<E> previous) {
        this.element = element;
        this.next = next;
        this.previous = previous;
    }
}

很好理解,從字面都可以理解出來,一個是數組實現,一個是鏈表實現。

元素添加

二者都有幾個add()方法,

void add(E item)  向滾動列表的末尾添加指定的項。
void add(E item, int index)  向滾動列表中索引指示的位置添加指定的項。

先看看ArrayList的實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void add(int index, E element) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(
        "Index: "+index+", Size: "+size);
    ensureCapacity(size+1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
             size - index);
    elementData[index] = element;
    size++;
    }
    public boolean add(E e) {
    ensureCapacity(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
    }

對於add(E e)方法,非常簡單,首先確保數組容量,然後直接賦值。在不需要擴充數組容量的情況下,效率非常高,而一旦需要數組擴容,代價就會上升:

1
2
3
4
5
6
7
8
9
10
11
12
public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
    Object oldData[] = elementData;
    int newCapacity = (oldCapacity * 3)/2 + 1;
        if (newCapacity < minCapacity)
    newCapacity = minCapacity;
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
}
}

因爲它需要將已有的數組複製到新的數組裏去。由此便可以想到一個提高add()效率的方法,在一開始儘量設定一個合理的數組容量,那麼可以有效地減少數組的擴容和大量的複製。

對於add(int index, E e),比起add(E e),多一個可能的複製操作,這樣才能保證在合理的位置插入新的元素。

LinkedList的實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public boolean add(E e) {
addBefore(e, header);
    return true;
}
private Entry<E> addBefore(E e, Entry<E> entry) {
Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
size++;
modCount++;
return newEntry;
}
public void add(int index, E element) {
    addBefore(element, (index==size ? header : entry(index)));
}
/**
* Returns the indexed entry.
*/
private Entry<E> entry(int index) {
    if (index < 0 || index >= size)
        throw new IndexOutOfBoundsException("Index: "+index+
                                            ", Size: "+size);
    Entry<E> e = header;
    if (index < (size >> 1)) {
        for (int i = 0; i <= index; i++)
            e = e.next;
    } else {
        for (int i = size; i > index; i--)
            e = e.previous;
    }
    return e;
}

粗略看起來要複雜一些,因爲LinkedList同時還是一個Deque(JDK 1.6新添加的),所以它的實現也要兼顧雙向隊列。

下面從一個空的LinkedList開始,看看新的元素是如何添加進來的:

1
2
3
4
5
6
7
List<Integer> ints = new LinkedList<Integer>();
ints.add(1);
ints.add(2);
ints.add(3);
System.out.println(ints); //[1, 2, 3]

下面一步一步看List內部header和元素之間的關係:

  • 初始化: header.element = null; header.next=header.previous=header 這裏是一個環狀的結構,自己的p和n指針都指向自己

  • 添加第一個元素“1”:header.element=null;header.next=1;header.previous=1; 2個元素相互連接
  • 添加第二個元素“2” 這裏很明顯看來了,是一個環狀結構
  • 添加第三個元素“3” 既然是一個環狀,乾脆用圓形顯示好了,貌似畫的不太圓。。。

這裏總結一下兩種的差別:

  • 對於元素的add()來說,LinkedList要比ArrayList要快一些,因爲ArrayList可能需要額外的擴容操作,當然如果沒有擴容,二者沒有很大的差別
  • 對於元素的add(int, element),對於LinkedList來說,代價主要在遍歷獲取插入的位置的元素,而ArrayList的主要代價在於可能有額外的擴容和大量元素的移動
  • 小結:對於簡單的元素添加,如果事先知道元素的個數,採用預置大小的ArrayList要更好,反之可以考慮LinkedList

元素移除

ArrayList的元素移除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,
             numMoved);
elementData[--size] = null; // Let gc do its work
return oldValue;
}
public boolean remove(Object o) {
if (o == null) {
        for (int index = 0; index < size; index++)
    if (elementData[index] == null) {
        fastRemove(index);
        return true;
    }
} else {
    for (int index = 0; index < size; index++)
    if (o.equals(elementData[index])) {
        fastRemove(index);
        return true;
    }
    }
return false;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
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; // Let gc do its work
}

remove(int)和remove(Object)兩種方式的返回值是有區別的哦

對於ArrayList來說,主要是的仍然會有元素的移動(這裏就是數組的複製),雖然採用的是System的arrayCopy,但是本質上還是複製的思路。還有一點需要注意的是,remove(Object)對null值進行單獨處理,這裏也說明ArrayList是可以存取null的。

LinkedList元素移除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public E remove(int index) {
     return remove(entry(index));
}
/**
  * Returns the indexed entry.
  */
private Entry<E> entry(int index) {
     if (index < 0 || index >= size)
         throw new IndexOutOfBoundsException("Index: "+index+
                                             ", Size: "+size);
     Entry<E> e = header;
     if (index < (size >> 1)) {
         for (int i = 0; i <= index; i++)
             e = e.next;
     } else {
         for (int i = size; i > index; i--)
             e = e.previous;
     }
     return e;
}
public boolean remove(Object o) {
     if (o==null) {
         for (Entry<E> e = header.next; e != header; e = e.next) {
             if (e.element==null) {
                 remove(e);
                 return true;
             }
         }
     } else {
         for (Entry<E> e = header.next; e != header; e = e.next) {
             if (o.equals(e.element)) {
                 remove(e);
                 return true;
             }
         }
     }
     return false;
}

這裏的實現就是典型的鏈表刪除的實現,其中有幾個細節需要提一下:

  • modCount的處理,這個變量是用來存儲List的修改的次數的,僅僅存儲添加和刪除的操作此書,用來在Iterator中判斷List的狀態和行爲,防止不同步的修改,拋出ConcurrentModificationException
  • 通過索引訪問元素的實現entry(int),這裏有一個小細節,
     if (index < (size >> 1)) {

如果元素的位置在前半段,那麼通過next指針查找,否則通過previous指針查找。這一行代碼有2個值得學習的地方,第一查找的優化,根據位置判斷查找的方向,第二移位操作的運用。不得不佩服Bloch的編程功底。

小結一下:

刪除操作中,LinkedList更有優勢,一旦找到了刪除的節點,它僅僅只是斷開鏈接關係,並沒有元素複製移動的行爲,而ArrayList不可避免的又要進行元素的移動。

元素索引

indexOf(Object o)  回此列表中第一次出現的指定元素的索引;如果此列表不包含該元素,則返回 -1。

ArrayList的實現:

1
2
3
4
5
6
7
8
9
10
11
12
public int indexOf(Object o) {
if (o == null) {
    for (int i = 0; i < size; i++)
    if (elementData[i]==null)
        return i;
} else {
    for (int i = 0; i < size; i++)
    if (o.equals(elementData[i]))
        return i;
}
return -1;
}

LinkedList的實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int indexOf(Object o) {
    int index = 0;
    if (o==null) {
        for (Entry e = header.next; e != header; e = e.next) {
            if (e.element==null)
                return index;
            index++;
        }
    } else {
        for (Entry e = header.next; e != header; e = e.next) {
            if (o.equals(e.element))
                return index;
            index++;
        }
    }
    return -1;
}

ArrayList:基於數組的遍歷查找

LinkedList:基於鏈表的遍歷查找

按照對象在內存中存儲的順序去考慮,數組的訪問要比鏈接錶快,因爲對象都存儲在一起。

遍歷

基於以上的分析,可以得出,按照索引遍歷,ArrayList是更好的選擇,按照Iterator遍歷,也許LinkedList會好一些。

反過來理解,如果是ArrayList,Iterator和index遍歷都可以,如果是LinkedList,優先選擇Iterator比較好。

其他

  • 對於ArrayList和LinkedList, size() isEmpty() 這些都是常量計算,代價很低
  • LinkedList實現了更多的方法,包括Queue,所以它也是一種隊列
  • 對於少量得元素臨時存儲,優先考慮ArrayList
  • 頻繁的添加和刪除操作的時候,優先使用LinkedList
  • 頻繁的按索引訪問遍歷,優先使用ArrayList
發佈了21 篇原創文章 · 獲贊 4 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章