JDK提供了一組主要的數據結構的實現,如List、Set、Map等常用結構,這些結構都繼承自java.util.collection接口。
List有三種不同的實現,ArrayList和Vector使用數組實現,其封裝了對內部數組的操作。LinkedList使用了循環雙向鏈表的數據結構,LinkedList鏈表是由一系列的鏈表項連接而成,一個鏈表項包括三部分:鏈表內容、前驅表項和後驅表項。
LinkedList的表項結構如圖:

LinkedList表項間的連接關系如圖:

可以看出,無論LinkedList是否為空,鏈表都有一個header表項,它即表示鏈表的開頭也表示鏈表的結尾。表項header的後驅表項便是鏈表的第一個元素,其前驅表項就是鏈表的最後一個元素。
對基於鏈表和基於數組的兩種List的不同實現做一些比較:
1、增加元素到列表的末尾:
在ArrayList中源代碼如下:
1 public boolean add(E e) {
2 ensureCapacityInternal(size + 1); // Increments modCount!!
3 elementData[size++] = e;
4 return true;
5 }
add()方法性能的好壞取決於grow()方法的性能:
1 private void grow(int minCapacity) {
2 // overflow-conscious code
3 int oldCapacity = elementData.length;
4 int newCapacity = oldCapacity + (oldCapacity >> 1);
5 if (newCapacity - minCapacity < 0)
6 newCapacity = minCapacity;
7 if (newCapacity - MAX_ARRAY_SIZE > 0)
8 newCapacity = hugeCapacity(minCapacity);
9 // minCapacity is usually close to size, so this is a win:
10 elementData = Arrays.copyOf(elementData, newCapacity);
11 }
可以看出,當ArrayList對容量的需求超過當前數組的大小是,會進行數組擴容,擴容的過程中需要大量的數組復制,數組復制調用System.arraycopy()方法,操作效率是非常快的。
在LinkedList源碼中add()方法:
1 public boolean add(E e) {
2 linkLast(e);
3 return true;
4 }
linkLast()方法如下:
1 void linkLast(E e) {
2 final Node<E> l = last;
3 final Node<E> newNode = new Node<>(l, e, null);
4 last = newNode;
5 if (l == null)
6 first = newNode;
7 else
8 l.next = newNode;
9 size++;
10 modCount++;
11 }
LinkedList是基於鏈表實現,因此不需要維護容量大小,但是每次都新增元素都要新建一個Node對象,並進行一系列賦值,在頻繁系統調用中,對系統性能有一定影響。性能測試得出,在列表末尾增加元素,ArrayList比LinkedList性能要好,因為數組是連續的,在末尾增加元素,只有在空間不足時才會進行數組擴容,大部分情況下追加操作效率還是比較高的。
2、增加元素到列表的任意位置:
List接口還提供了在任意位置插入元素的方法:void add(int index,E element)方法,由於實現方式不同,ArrayList和LinkedList在這個方法上存在一定的差異。由於ArrayList是基於數組實現的,而數組是一塊連續的內存,如果在數組的任意位置插入元素,必然會導致該位置之後的所有元素重新排序,其效率相對較低。
ArrayList源碼實現:
1 public void add(int index, E element) {
2 rangeCheckForAdd(index);
3 ensureCapacityInternal(size + 1); // Increments modCount!!
4 System.arraycopy(elementData, index, elementData, index + 1,
5 size - index);
6 elementData[index] = element;
7 size++;
8 }
可以看出每次插入都會進行數組復制,大量的數組復制操作導致系統性能效率低下。並且數組插入的位置越靠前,數組復制的開銷就越大。因此,盡可能插入元素在其尾端附近,有助於提高該方法的性能。
LinkedList的源碼實現:
1 public void add(int index, E element) {
2 checkPositionIndex(index);
3
4 if (index == size)
5 linkLast(element);
6 else
7 linkBefore(element, node(index));
8 }
9 void linkBefore(E e, Node<E> succ) {
10 // assert succ != null;
11 final Node<E> pred = succ.prev;
12 final Node<E> newNode = new Node<>(pred, e, succ);
13 succ.prev = newNode;
14 if (pred == null)
15 first = newNode;
16 else
17 pred.next = newNode;
18 size++;
19 modCount++;
20 }
對於LinkedList的在尾端插入和對任意位置插入數據是一樣的,並不會因為插入位置靠前而導致效率低下。因此,在應用中,如果經常往任意位置插入元素,可以考慮使用LinkedList提到ArrayList。
3、刪除任意位置的元素:
List接口還提供了在任意位置刪除元素的方法:remove(int index)方法。在ArrayList中,對於remove()方法和add()方法一樣,在任意位置移除元素,都需要數組復制。
ArrayList的remove()方法的源碼如下:
1 public E remove(int index) {
2 rangeCheck(index);
3
4 modCount++;
5 E oldValue = elementData(index);
6
7 int numMoved = size - index - 1;
8 if (numMoved > 0)
9 System.arraycopy(elementData, index+1, elementData, index,
10 numMoved);
11 elementData[--size] = null; // clear to let GC do its work
12
13 return oldValue;
14 }
可以看出,在ArrayList的每一次刪除操作,都需要進行數組重組,並且刪除元素的位置越靠前,數組重組的開銷就越大。
LinkedList的remove()方法的源碼:
1 public E remove(int index) {
2 checkElementIndex(index);
3 return unlink(node(index));
4 }
5 E unlink(Node<E> x) {
6 // assert x != null;
7 final E element = x.item;
8 final Node<E> next = x.next;
9 final Node<E> prev = x.prev;
10
11 if (prev == null) {
12 first = next;
13 } else {
14 prev.next = next;
15 x.prev = null;
16 }
17
18 if (next == null) {
19 last = prev;
20 } else {
21 next.prev = prev;
22 x.next = null;
23 }
24
25 x.item = null;
26 size--;
27 modCount++;
28 return element;
29 }
1 Node<E> node(int index) {
2 // assert isElementIndex(index);
3
4 if (index < (size >> 1)) {
5 Node<E> x = first;
6 for (int i = 0; i < index; i++)
7 x = x.next;
8 return x;
9 } else {
10 Node<E> x = last;
11 for (int i = size - 1; i > index; i--)
12 x = x.prev;
13 return x;
14 }
15 }
在LinkedList中首先通過循環找到要刪除的元素,如果元素位於前半段則,從前往後找;若位置位於後半段,則從後往前找,但是要移除中間的元素,卻幾乎要遍歷半個List。所有,無論元素位於較前還是較後,效率都比較高,但是位於中間效率就非常低。
4、容量參數:
容量參數是ArrayList和Vector等基於數組的List特有的性能參數,它表示初始化數組的大小。當數組所存儲的元素的數量超過其原有的大小時,它就會進行擴容,即進行一次數組復制,因此,合理設置數組大小有助於減少擴容次數,從而提升系統性能。
5、遍歷列表:
在JDK1.5之後,至少有三種遍歷列表的方式:forEach操作,迭代器,for循環。通過測試發現,forEach綜合性能不如迭代器,而for循環遍歷列表時,ArrayList的性能表現最好,而LinkedList的性能差的無法忍受,因為LinkedList進行隨機訪問,總會進行一次列表的遍歷操作。
對於ArrayList是基於數組來實現的,隨機訪問效率快,因此有限選擇隨機訪問。而LinkedList是基於鏈表實現的,隨機訪問的性能差,應該避免使用。
圍繞著Map接口,最主要的實現類有:HashMap、hashTable、LinkedHashMap和TreeMap。在HashMap的子類中還有Properties類的實現。
1、HashMap和Hashtable
首先說一下,HashMap和Hashtable的區別:Hashtable的大部分方法都實現了同步,而HashMap沒有。因此,HashMap不是線程安全的。其次,Hashtable不允許key或value使用null值,而HashMap可以。第三是內部的算法不同,它們對key的hash算法和hash值到內存索引的映射算法不同。
HashMap就是將key做hash算法,然後將hash值映射到內存地址,直接取得key所對應的數據。在HashMap的底層使用的是數組,所謂的內存地址即數組的下標索引。
HashMap中不得不提的就是hash沖突,需要存放到HashMap中的元素1和元素2經過hash計算,發現對應的內存地址一樣。如下圖:

HashMap底層使用的是數組,但是數組內的元素不是簡單的值,而是一個Entry對象。如下圖所示:

可以看出,HashMap的內部維護了一個Entry數組,每個entry表項包括:key、value、next、hash。next部分表示指向另一個Entry。在HashMap的put()方法中,可以看到當put()方法有沖突時,新的entry依然會安放在對應的索引下標內,並替換掉原來的值,同時為了保證舊值不丟失,會將新的entry的next指向舊值。這樣便實現了在一個數組索引空間內存放多個值。
HashMap的put()操作的源碼:
1 public V put(K key, V value) {
2 if (table == EMPTY_TABLE) {
3 inflateTable(threshold);
4 }
5 if (key == null)
6 return putForNullKey(value);
7 int hash = hash(key);
8 int i = indexFor(hash, table.length);
9 for (Entry<K,V> e = table[i]; e != null; e = e.next) {
10 Object k;
11 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
12 V oldValue = e.value;//取得舊值
13 e.value = value;
14 e.recordAccess(this);
15 return oldValue;//返回舊值
16 }
17 }
18
19 modCount++;
20 addEntry(hash, key, value, i);//添加當前表項到i位置
21 return null;
22 }
23 void addEntry(int hash, K key, V value, int bucketIndex) {
24 if ((size >= threshold) && (null != table[bucketIndex])) {
25 resize(2 * table.length);
26 hash = (null != key) ? hash(key) : 0;
27 bucketIndex = indexFor(hash, table.length);
28 }
29
30 createEntry(hash, key, value, bucketIndex);
31 }
32 void createEntry(int hash, K key, V value, int bucketIndex) {
33 Entry<K,V> e = table[bucketIndex];
34 table[bucketIndex] = new Entry<>(hash, key, value, e);//將新增元素放到i位置,並把它的next指向舊值
35 size++;
36 }
基於HashMap的這種實現,只要對hashCode()和hash()的方法實現的夠好,就能盡可能的減少沖突,那麼對HashMap的操作就等價於對數組隨機訪問的操作,具有很好的性能。但是,如果處理不好,在產生大量沖突的情況下,HashMap就退化為幾個鏈表,性能極差。
2、容量參數:
因為HashMap和Hashtable底層是基於數組實現的,當數組空間不足時,就會進行數組擴容,數組擴容就會進行數組復制,是十分影響性能的。
HashMap的構造函數:
1 public HashMap(int initialCapacity) 2 public HashMap(int initialCapacity, float loadFactor)
initialCapacity指定HashMap的初始容量,loadFactor是指負載因子(元素個數/元素總量),HashMap中還定義了一個阈值,它是當前數組容量和負載因子的乘積,當數組的實際容量超過阈值時,就會進行數組擴容。
另外,HashMap的性能一定程度上取決於hashCode()的實現,一個好的hashCode()的實現,可以盡可能減少沖突,提升hashMap的訪問速度。
3、LinkedHashMap
HashMap的一大缺點就是無序性,放入的數據,在遍歷取出時候是無序的。如果需要保證元素輸入時的順序,可以使用LinkedHashMap。
LinkedHashMap繼承自HashMap,因此,其性能是比較好。在HashMap的基礎上,LinkedHashMap內部又增加了一個鏈表,用於存放元素的順序。LinkedHashMap提供了兩種類型的順序,一種是元素插入時的順序,一種是最近訪問的順序。
1 public LinkedHashMap(int initialCapacity, 2 float loadFactor, 3 boolean accessOrder)
其中,accessOrder為true是,是按元素最後訪問時間排序,當accessOrder為false時,按插入順序排序。
4、TreeMap
TreeMap可以對元素進行排序,TreeMap是基於元素的固有順序而排序的(有Comparable或Comparator確定)。
TreeMap是根據key進行排序的,為了確定key的排序算法,可以使用兩種方法指定:
1:在TreeMap的構造函數中注入Comparator
TreeMap(Comparator<? super K> comparator);
2:使用一個實現了Comparable接口的key。
TreeMap是內部是基於紅黑樹實現,紅黑樹是一種平衡查找樹,其統計性能優於平衡二叉樹。
set集合中的元素是不能重復的,其中最主要的實現就是HashSet、LinkedHashSrt和TreeSet。查看Set接口實現類,可以發現所有的Set的一些實現都是相應Map的一種封裝。
set特性如圖所示:

1、分離循環中被重復調用的代碼。如:for(int i=0;i<list.size();i++),可以將list.size()分離出來。
2、省略相同的操作
3、減少方法的調用,方法調用時消耗系統堆棧的,會犧牲系統的性能。
RandomAccess接口是一個標識接口,本身沒有提供任何方法。主要的目的是為了標識出那些可以支持快速隨機訪問的List的實現。例如,根據是否實現RandomAccess接口在變量的時候選擇不同的遍歷實現,以提升性能。