在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指針都指向自己
- 添加第三個元素“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