概述
Java集合類位於java.util包下,主要包含Collection和Map兩大類,在該包下還包含一些常用的集合工具類如Collections等。這裏我們可以看一下java下集合框架的整體繼承關係圖,如下
其中,Queue(隊列)是在Java SE1.5中引入的新的集合類,除此之外在Java SE1.5中還引入了一些用於多線程併發的集合框架,這個在這裏不做介紹。
今天,我們主要介紹的框架類Colleciton下的一些常用的集合類。這裏我們可以看下Collection的整體繼承關係圖,該圖中隱藏了一些抽象類及接口,僅僅列舉了具體實現類。如下所示。
List介紹
List是有序的Collection,依賴索引能夠準確對元素進行CRUD操作。同時,List允許插入重複的元素(包括null)。
通過上面的繼承圖可知,List類下主要有四個實現類:
ArrayList:基於線性表的數據結構,內部使用數組實現(可變數組,自動擴容)。ArrayList的相關操作都沒有進行同步,所以是線程不安全的。
LinkedList:基於鏈表(而且是雙向鏈表)的數據結構,LinkedList除了實現List接口外,還實現了Queue接口,所以LinkedList可以被當做堆棧(stack),隊列(queue)或雙向隊列(deque)。同樣LinkedList和ArrayList一樣,也是線程不安全的。
Vector:基本的實現ArrayList一樣,但是Vector是線程同步的。
Stack:繼承自Vector,主要實現棧的功能(後進先出)。
其中Vector和Stack都不被建議使用了,主要原因就是Vector的操作是線程安全的導致操作效率較低,同時使用LinkedList也可以實現Stack的功能。
List CRUD性能比較
測試代碼如下:
import java.util.*;
/**
*
* @author zhangke
*/
public class TestList {
private static final int COUNT = 20000;
private static ArrayList arrayList = new ArrayList<>();
private static LinkedList linkedList = new LinkedList<>();
private static Vector vector = new Vector<>();
private static Random random = new Random();
private static final String FIRST_INSERT = "首位插入:list.add(0, i)";
private static final String LAST_INSERT = "末位插入:list.add(i, i)";
private static final String RANDOM_INSERT = "隨機插入:list.add(random.nextInt(i), i)";
private static final String FIRST_QUERY = "首位查詢:list.get(0)";
private static final String LAST_QUERY = "末位查詢:list.get(i)";
private static final String RANDOM_QUERY = "隨機查詢:list.get(random.nextInt(i))";
private static final String FIRST_DELETE = "首位刪除:list.remove(0)";
private static final String LAST_DELETE = "末位刪除:list.remove(i)";
private static final String RANDOM_DELETE = "隨機刪除:list.remove(random.nextInt(i))";
public static void main(String[] args) {
System.out.println(RANDOM_INSERT);
randomInsert(arrayList);
randomInsert(linkedList);
randomInsert(vector);
System.out.println("\n" + RANDOM_QUERY);
randomQuery(arrayList);
randomQuery(linkedList);
randomQuery(vector);
System.out.println("\n" + RANDOM_DELETE);
randomDelete(arrayList);
randomDelete(linkedList);
randomDelete(vector);
}
/**
* 插入
*/
private static void randomInsert(List list) {
long startTime = System.currentTimeMillis();
Random random = new Random();
list.add(0);
for (int i = 1; i < COUNT; i++) {
// 首位插入
// list.add(0, i);
// 末位插入
// list.add(i, i);
// 隨機插入
list.add(random.nextInt(i), i);
}
long interval = System.currentTimeMillis() - startTime;
System.out.println("\t" + getListName(list) + " : 隨機插入 " + COUNT + "個元素花費時間:" + interval + "ms");
}
/**
* 刪除
*/
private static void randomDelete(List list) {
long startTime = System.currentTimeMillis();
for (int i = COUNT - 1; i >= 1; i--) {
// 首位刪除
// list.remove(0);
// 末位刪除
// list.remove(i);
// 隨機刪除
list.remove(random.nextInt(i));
}
long interval = System.currentTimeMillis() - startTime;
System.out.println("\t" + getListName(list) + " : 隨機刪除 " + COUNT + "個元素花費時間:" + interval + "ms");
}
/**
* 查詢
*
* @param list
*/
private static void randomQuery(List list) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < COUNT; i++) {
// list.get(0);
// list.get(i);
list.get(random.nextInt(COUNT));
}
long interval = System.currentTimeMillis() - startTime;
System.out.println("\t" + getListName(list) + " : 隨機讀取 " + COUNT + "個元素花費時間:" + interval + "ms");
}
/**
* 獲取List具體實現類名稱
*/
private static String getListName(List list) {
if (list instanceof LinkedList) {
return "LinkedList";
} else if (list instanceof ArrayList) {
return "ArrayList";
} else if (list instanceof Vector) {
return "Vector";
}
return null;
}
}
對不同情況下的操作結果如下:
/*** 插入 ***/
首位插入:list.add(0, i)
ArrayList : 隨機插入 20000個元素花費時間:46ms
LinkedList : 隨機插入 20000個元素花費時間:3ms
Vector : 隨機插入 20000個元素花費時間:44ms
末位插入:list.add(i, i)
ArrayList : 隨機插入 20000個元素花費時間:3ms
LinkedList : 隨機插入 20000個元素花費時間:2ms
Vector : 隨機插入 20000個元素花費時間:2ms
隨機插入:list.add(random.nextInt(i), i)
ArrayList : 隨機插入 20000個元素花費時間:23ms
LinkedList : 隨機插入 20000個元素花費時間:433ms
Vector : 隨機插入 20000個元素花費時間:24ms
/*** 查詢 ***/
首位查詢:list.get(0)
ArrayList : 隨機讀取 20000個元素花費時間:1ms
LinkedList : 隨機讀取 20000個元素花費時間:1ms
Vector : 隨機讀取 20000個元素花費時間:1ms
末位查詢:list.get(i)
ArrayList : 隨機讀取 20000個元素花費時間:1ms
LinkedList : 隨機讀取 20000個元素花費時間:150ms
Vector : 隨機讀取 20000個元素花費時間:1ms
隨機查詢:list.get(random.nextInt(i))
ArrayList : 隨機讀取 20000個元素花費時間:1ms
LinkedList : 隨機讀取 20000個元素花費時間:1135ms
Vector : 隨機讀取 20000個元素花費時間:1ms
/*** 刪除 ***/
首位刪除:list.remove(0)
ArrayList : 隨機刪除 20000個元素花費時間:41ms
LinkedList : 隨機刪除 20000個元素花費時間:2ms
Vector : 隨機刪除 20000個元素花費時間:40ms
末位刪除:list.remove(i)
ArrayList : 隨機刪除 20000個元素花費時間:1ms
LinkedList : 隨機刪除 20000個元素花費時間:1ms
Vector : 隨機刪除 20000個元素花費時間:1ms
隨機刪除:list.remove(random.nextInt(i))
ArrayList : 隨機刪除 20000個元素花費時間:22ms
LinkedList : 隨機刪除 20000個元素花費時間:493ms
Vector : 隨機刪除 20000個元素花費時間:23ms
總結如下:
List性能比較結論
根據上面的測試Log,我們可以得出以下結論:
在不考慮線程同步的,ArrayList和Vector在操作上性能基本是一樣的。
在首位插入數據時,LinkedList的性能要明顯優於ArrayList,這是因爲ArrayList每次在首位插入一次數據,後面的數據都要被整體後移一位,隨着數組的長度越來越大,被消耗的時間也就越多。而LinkedList在首位插入只需要修改指針即可。
在末位插入數據,LinkedList的性能和ArrayList基本保持一致。ArrayList在插入時會直接在數組末位添加一位,而LinkedList也只需要在末位添加一位(此時LinkedList會保存末位元素,不需要尋址)
隨機插入大量數據的時候,ArrayList的性能要好於LinkedList。雖然ArrayList每次在插入的時候都需要移動大量的元素,但是LinkedList在每次插入到指定位置時,都需要重新尋址,在尋址上回耗費大量的時間,所以會影響LinkedList的插入性能。
LinekedList和ArrayList在插入和刪除上的操作性能基本相同。
對於隨機訪問,ArrayList基本要明顯優於LinkeList(首位查詢除外)。
通過上面的結論我們不難發現,有很多說法認爲LinkedList做插入和刪除更快,這樣的說法存在一定的侷限性。
- LinkedList做增刪操作時,慢在尋址,快在只需要修改元素的前驅和後繼就能完成。
- ArrayList做增刪操作時,快在尋址,慢在數組元素的整體移動上。
對於上面所提到的尋址我們可以理解爲隨機訪問,爲什麼這樣說呢?
因爲LinkedList在每次做插入的時候都要先找到所要插入的位置,這就是一個隨機訪問的過程。所以在增刪時主要是尋址的過程會耗費佔用整個過程中的大部分時間,這個我們可以通過閱讀LinkedList的add(int, E)方法可知。如下:
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
// 尋址的方法
Node<E> node(int index) {
// assert isElementIndex(index);
// 這個地方就是每次添加時尋址的過程,這裏採用了二分查找的思想
// 因爲LinkedList是一個雙鏈表,可以從兩頭分別查找
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;
}
}
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單獨的增刪還是非常快的,因爲只需要修改前驅和後繼即可。
for和foreach方式對List進行遍歷
在上面的測試代碼中,我們可以分別使用for和foreach方式對ArrayList和LinkedList對集合進行遍歷,代碼和結果如下:
private static void randomQuery(List list) {
long startTime = System.currentTimeMillis();
// foreach方式遍歷
for (Object object : list) {
}
// for方法遍歷
// for (int i = 0; i < COUNT; i++) {
// list.get(i);
// }
long interval = System.currentTimeMillis() - startTime;
System.out.println("\t" + getListName(list) + " : 隨機讀取 " + COUNT + "個元素花費時間:" + interval + "ms");
}
// for方式遍歷結果
ArrayList : 隨機讀取 20000個元素花費時間:1ms
LinkedList : 隨機讀取 20000個元素花費時間:1035ms
// foreach方式遍歷結果
ArrayList : 隨機讀取 20000個元素花費時間:1ms
LinkedList : 隨機讀取 20000個元素花費時間:2ms
通過結果可以發現,使用for和foreach方式遍歷ArrayList基本上性能差距不大,但是對於LinkedList而言,foreach遍歷的性能要明顯優於for,對於這樣的結果又是什麼原因呢?
其實,造成for循環遍歷LinkedList比較慢的原因也可以通過上面一段LinkedList.get(int)代碼可以說明,當每次使用for遍歷時,都需要進行尋址,這樣隨着集合長度的增加尋址會花費越來越多的時間,這樣會造成for循環花費的時間越來越多。
而對於使用foreach循環,是因爲foreach循環底層使用的是iterator的方式完成遍歷的,這裏我們看看LinkedList裏面Iterator中的遍歷的方法,也就是next()方法,代碼如下:
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
// lastReturned 表示當前遍歷的元素
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
通過代碼一目瞭然,通過iterator遍歷linkedList每次都會記住下一個元素的位置,這樣在遍歷的時候屏蔽了尋址的過程,所以使用foreach完成linkedlist的遍歷性能會大大提高。
對於遍歷我們可以得出結論:
- 對於ArrayList我們使用for或者foreach方式遍歷都可以。
- 但是對於LinkedList,最好使用foreach方式遍歷避免使用for方式遍歷。