目錄
List接口是Collection接口下的子接口List中的元素是有序的,可以重複的,List接口的主要實現類有三個:ArrayList、LinkedList和Vector。
一、相同之處
- 都是List接口的實現類,因此他們具有List列表的共同特性,他們是有序的,可以容納重複的元素,允許元素爲null。
- 都可以使用Collection下的Iterator接口的iterator()方法和Iterator接口的子接口ListIterator的listIterator()方法,並且Iterator和ListIterator迭代器都具有fail-fast機制。
二、不同之處
1、ArrayList與LinkedList區別
(1)底層數據結構不同,ArrayList和Vector都是基於動態數組實現的,LinkedList是基於雙向鏈表實現的。
ArrayList是由Object類型的數組實現的,如果傳入初始容量參數,則創建一個initialCapacity大小的數組,如果沒有傳入initialCapacity,那麼創建一個默認大小(10)的數組。
(2)實現接口不同,除了都有的Serializable、Cloneable、Iterable、Collection和List,ArrayList還實現了RadomAccess接口,LinkedList還實現了Deque接口
(3)不同操作效率不同,對於隨機訪問get和set效率不同,ArrayList優於LinkedList,因爲LinkedList要移動指針遍歷得到待查找的node。對於新增和刪除操作add和remove效率不同,LinedList比較佔優勢,因爲ArrayList要移動數據。
get和set | add | remove | |
ArrayList | O(I) | O(n) | O(n) |
LinkedList | O(n) | O(I) | O(n) |
這一點要看實際情況的。若只對單條數據插入或刪除,ArrayList的速度反而優於LinkedList。但若是批量隨機的插入刪除數據,LinkedList的速度大大優於ArrayList. 因爲ArrayList每插入一條數據,要移動插入點及之後的所有數據。
ArrayList的get
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
//elementData是用於存儲元素的動態數組
}
可以看出ArrayList的get就是從數組進行讀取,效率爲O(1)。
LinkedList的get
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) { //判斷index是否超過size的一般,如果不是則從head開始遍歷
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else { //否則從tail開始遍歷
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
可以看出,雖然LinkedList實現了List接口,因此必須實現get(index)方法,但是get()方法是使用迭代器遍歷來實現的,因此效率爲O(n)。
ArrayList的add和remove:通過System.arraycopy來實現
add操作:
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++;
}
remove操作:
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
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
return oldValue;
}
可以看出ArrayList的add和remove操作都是通過System.arraycopy來實現的,需要移動index後每一個元素,操作效率爲O(n)。
LinkedList的add和remove:通過link和unlink來實現
add操作:
public void add(int index, E element) {
checkPositionIndex(index); //查看index是否在鏈表範圍內
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
public void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null); //Node(NOde <E> pre, E element, Node<E> next)
last = newNode;
if (l == null) //如果鏈表還是空鏈表,那麼把當前節點設爲頭節點
first = newNode;
else
l.next = newNode; //否則把當前節點添加到last後面
size++;
modCount++; //用於fail-fast機制
}
public void linkBefore(E e, Node<E> succ) { //把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) //如果succ是頭結點,那麼把當前節點設置爲頭結點
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
可以看出,由於add操作只需要修改雙向鏈表的next和prev指針的地址,因此操作效率爲O(1)。
remove操作:
//刪除index下標位置的元素
public E remove(int index) {
checkElementIndex(index); //檢查index是否在鏈表範圍內
return unlink(node(index));
}
//刪除元素o,因爲LinkedList中的元素是可以重複且爲null的,所以要遍歷整個表,把所有o元素都刪掉
public boolean remove(Object o) {
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;
}
}
}
public 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++; //用於fail-fast機制
return element;
}
可以看出,雖然unlink操作的效率是O(1),但是不論是索引(index)刪除還是元素(object)刪除都需要遍歷最多半個鏈表(索引刪除調用node方法進行了遍歷查找)才能找到要刪除的node,因此效率爲O(n)
(4)佔用內存不同,LinkedList 比 ArrayList 需要更多的內存,因爲鏈表除了數據本身還需要存儲next指針指向下一個node的地址。
2、ArrayList與Vector區別
(1)同步性不同,LinkedList和ArrayList是不同步的,Vector是同步的(使用synchronized進行強同步)。
(2)擴容方法不同,LinkedList基於鏈表實現,容量是無限的;ArrayList和Vector是基於數組實現的,容量是有限的,在需要的時候要擴容,但是ArrayList和Vector的擴容方式稍微不同,ArrayList是newCapacity=oldCapacity*3/2-1,而Vector是與“增長係數有關”,若指定了“增長係數”,且“增長係數有效(即大於0)”;那麼,每次容量不足時,“新的容量”=“原始容量+增長係數”。若增長係數無效(即,小於/等於0),則“新的容量”=“原始容量 x 2”。
ArrayList的動態數組的擴容
因爲數組的大小是固定的,而ArrayList的大小是動態可調的,那麼他是如何實現的呢?首先,由ensureCapacity()方法確認ArrayList容量,然後由grow()方法擴容。
//ensureCapacity()方法用於確認當前的elementData[]數組是否能容納minCapacity的元素
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
//ensureExplicitCapacity()方法用於擴容,也就是說調用這個函數說明明確的要擴容,擴容對數組的結構進行了修改,因此modcount加1
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//數組的擴容:
//首先計算需要的容量,newCapacity = oldCapacity + (oldCapacity >> 1),說明newCapacity是oldCapacity的大概3/2,如果
//確切一點,就是newCapacity = 3/2*oldCapacity-1;
//然後,如果newCapacity<minCapacity,那麼newCapacity設爲minCapacity
//最後調用Array.copyOf()方法把舊數組複製到新數組中
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
這裏我們來看看Array.copyOf( )是怎麼操作的:
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
可以看到Array.copyOf( )方法中,是新建了一個數組,然後把就數組拷貝在新數組中,此外,如果新數組和舊數組的類型不同,還需要把舊類型輕質轉換爲新類型。那麼可以看出,ArrayList的擴容是通過新建一個數組來實現的。
三、使用場景
如果涉及到“隊列”、“棧”和“鏈表”等場景,那麼可以使用List下的實現類,具體要根據需求和各個實現類的特點選擇。
- 如果需要快速隨機訪問,那麼使用ArrayList
- 如果需要頻繁的插入和刪除中間節點,那麼使用LinkedList
- 如果需要在多線程中同步,那麼使用Vector,但是由於歷史原因Vector基本已經被棄用,可以用java.util.Concurrent包下的concurrentLinkedDeque或者concurrentQueue來代替Vector