背景
ArrayList與LinkedList是Java程式設計中經常會用到的兩種基本資料結構,在書本上一般會說明以下兩個特點:
- 對於需要快速隨機訪問元素,應該使用ArrayList。
- 對於需要快速插入,刪除元素,應該使用LinkedList。
該文通過實際的例子分析這兩種資料的讀寫效能。
ArrayList
ArrayList是實現了基於動態陣列的資料結構:
private static final int DEFAULT_CAPACITY = 10;
...
transient Object[] elementData;
...
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
LinkedList
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;
}
}
...
transient Node<E> first;
transient Node<E> last;
...
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
例項分析
- 通過對兩個資料結構分別增加、插入、遍歷進行讀寫效能分析
1、增加資料
public class ArrayListAndLinkList {
public final static int COUNT=100000;
public static void main(String[] args) {
// ArrayList插入
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
Long start = System.currentTimeMillis();
System.out.println("ArrayList插入開始時間:" + sdf.format(start));
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < COUNT; i++) {
arrayList.add(i);
}
Long end = System.currentTimeMillis();
System.out.println("ArrayList插入結束時間:" + sdf.format(end));
System.out.println("ArrayList插入" + (end - start) + "毫秒");
// LinkedList插入
start = System.currentTimeMillis();
System.out.println("LinkedList插入開始時間:" + sdf.format(start));
LinkedList<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < COUNT; i++) {
linkedList.add(i);
}
end = System.currentTimeMillis();
System.out.println("LinkedList插入結束時間:" + sdf.format(end));
System.out.println("LinkedList插入結束時間" + (end - start) + "毫秒");
}
}
輸出如下:
兩者寫入的效能相差不大!
2、插入資料
在原有增加的資料上,在index:100的位置上再插入10萬條資料。
public class ArrayListAndLinkList {
public final static int COUNT=100000;
public static void main(String[] args) {
// ArrayList插入
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
Long start = System.currentTimeMillis();
System.out.println("ArrayList插入開始時間:" + sdf.format(start));
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < COUNT; i++) {
arrayList.add(i);
}
for (int i = 0; i < COUNT; i++) {
arrayList.add(100,i);
}
Long end = System.currentTimeMillis();
System.out.println("ArrayList插入結束時間:" + sdf.format(end));
System.out.println("ArrayList插入" + (end - start) + "毫秒");
// LinkedList插入
start = System.currentTimeMillis();
System.out.println("LinkedList插入開始時間:" + sdf.format(start));
LinkedList<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < COUNT; i++) {
linkedList.add(i);
}
for (int i = 0; i < COUNT; i++) {
linkedList.add(100,i);
}
end = System.currentTimeMillis();
System.out.println("LinkedList插入結束時間:" + sdf.format(end));
System.out.println("LinkedList插入結束時間" + (end - start) + "毫秒");
}
}
輸出如下:
ArrayList的效能明顯比LinkedList的效能差了很多。
看下原因:
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++;
}
ArrayList的插入原理:在index位置上插入後,在index後續的資料上需要做逐一複製。
LinkedList的插入原始碼:
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++;
}
LinkedList的插入原理:在原來相互連結的兩個節點(Node)斷開,把新的結點插入到這兩個節點中間,根本不存在複製這個過程。
3、遍歷資料
在增加和插入的基礎上,利用get方法進行遍歷。
public class ArrayListAndLinkList {
public final static int COUNT=100000;
public static void main(String[] args) {
// ArrayList插入
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
Long start = System.currentTimeMillis();
System.out.println("ArrayList插入開始時間:" + sdf.format(start));
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < COUNT; i++) {
arrayList.add(i);
}
for (int i = 0; i < COUNT; i++) {
arrayList.add(100,i);
}
Long end = System.currentTimeMillis();
System.out.println("ArrayList插入結束時間:" + sdf.format(end));
System.out.println("ArrayList插入" + (end - start) + "毫秒");
// LinkedList插入
start = System.currentTimeMillis();
System.out.println("LinkedList插入開始時間:" + sdf.format(start));
LinkedList<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < COUNT; i++) {
linkedList.add(i);
}
for (int i = 0; i < COUNT; i++) {
linkedList.add(100,i);
}
end = System.currentTimeMillis();
System.out.println("LinkedList插入結束時間:" + sdf.format(end));
System.out.println("LinkedList插入結束時間" + (end - start) + "毫秒");
// ArrayList遍歷
start = System.currentTimeMillis();
System.out.println("ArrayList遍歷開始時間:" + sdf.format(start));
for (int i = 0; i < 2*COUNT; i++) {
arrayList.get(i);
}
end = System.currentTimeMillis();
System.out.println("ArrayList遍歷開始時間:" + sdf.format(end));
System.out.println("ArrayList遍歷開始時間" + (end - start) + "毫秒");
// LinkedList遍歷
start = System.currentTimeMillis();
System.out.println("LinkedList遍歷開始時間:" + sdf.format(start));
for (int i = 0; i < 2*COUNT; i++) {
linkedList.get(i);
}
end = System.currentTimeMillis();
System.out.println("LinkedList遍歷開始時間:" + sdf.format(end));
System.out.println("LinkedList遍歷開始時間" + (end - start) + "毫秒");
}
}
輸出如下:
兩者的差異巨大:
我們看一下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)) {
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;
}
}
3.1、LinkedList遍歷改進
我們採用迭代器對LinkedList的遍歷進行改進:
...
// LinkedList遍歷
start = System.currentTimeMillis();
System.out.println("LinkedList遍歷開始時間:" + sdf.format(start));
Iterator<Integer> iterator = linkedList.iterator();
while(iterator.hasNext()){
iterator.next();
}
end = System.currentTimeMillis();
System.out.println("LinkedList遍歷開始時間:" + sdf.format(end));
System.out.println("LinkedList遍歷開始時間" + (end - start) + "毫秒");
再看下結果:
兩者的遍歷效能接近。
總結
- List使用首選ArrayList。對於個別插入刪除非常多的可以使用LinkedList。
- LinkedList,遍歷建議使用Iterator迭代器,尤其是資料量較大時LinkedList避免使用get遍歷。