00、故事的起源
“二哥,上一篇《泛型》的反響效果怎麼樣啊?”三妹對她提議的《教妹學 Java》專欄很是關心。
“有人評論說,‘二哥你敲程式碼都敲出幻想了啊。’”
“呵呵,這句話充斥著滿滿的諷刺意味啊。”三妹有點難過了起來。
“不過,也有人評論說,‘建議這個系列的文章多寫啊,因為我花了半個月都沒看懂《 Java 程式設計思想》中關於泛型的講解,但再看完這篇文章後終於融會貫通了,比心。’”
“二哥,你能不能先說好訊息啊?真是的。我也要給這位暖心的讀者比心了。”三妹說完這句話就在我面前比了一個心,我瞅了她一眼,發現她之前的愁容也無影無蹤了。
“那接下來,二哥還要繼續寫嗎?”我看到了三妹深情的目光。
“嗯,我想該寫集合了。”
“那就讓我繼續來提問吧,二哥你繼續來回答。”三妹已經躍躍欲試了。
01、二哥,什麼是集合啊?
三妹,聽哥慢慢給你講啊。
JDK 1.2 的時候引入了集合的概念,用來包含一組資料結構。與陣列不同的是,這些資料結構的儲存空間會隨著元素增加而動態增加。其中,有一些集合類支援新增重複元素,而另一些不支援;有一些支援新增 null
元素,而另一些不支援。
可以根據繼承體系將集合分為兩大類,一類實現了 Collection
介面(見圖 1),另一類實現了 Map
介面(見圖 2)。
介紹一下圖 1:
1)Collection
是所有集合類的根介面。
2)Set
介面的實現類不允許重複的元素,例如 HashSet
、LinkedHashSet
。
3)List
介面的實現類允許重複元素,可通過 index
訪問對應位置上的元素,例如 LinkedList
、ArrayList
。
4)Queue
介面的實現類允許在佇列的尾部或者頭部增加或者刪除元素,例如 PriorityQueue
。
介紹一下圖 2:
1)HashMap
是最常用的 Map
,可以根據鍵直接獲取對應的值,它根據鍵的 hashCode
值儲存資料,所以訪問速度非常快。HashMap
最多隻允許一條記錄的鍵為 null
(多條會覆蓋);但允許多條記錄的值為 null
。
2)TreeMap
能夠把它儲存的記錄根據鍵(不允許鍵的值為 null
)排序,預設是升序,也可以指定排序的比較器,當用迭代器(Iterator
)遍歷 TreeMap
時,得到的記錄是排過序的。
3)Hashtable
的鍵和值均不允許為 null
,是執行緒同步的,也就是說任一時刻只有一個執行緒能寫 Hashtable
,執行緒同步會消耗掉一些效能,因此 Hashtable
在寫入時花費的時間也會比較多。
4)LinkedHashMap
儲存了記錄的插入順序,當用迭代器(Iterator
)遍歷 LinkedHashMap
時,先得到的記錄肯定是先插入的。鍵和值均允許為 null
。
有了集合的幫助,程式設計師不再需要親自實現元素的排序、查詢等底層演算法了。另外,基於陣列實現的集合類在頻繁讀取時效能更佳,比如說 ArrayList
;基於佇列實現的集合類在頻繁增加、更新、刪除資料時效率更高,比如說 LinkedList
;程式設計師所要做的就是,根據業務需要選擇適當的集合類,至於效能調優嘛,可以微信找二哥。
02、二哥,LinkedList 和 ArrayList 有什麼區別啊?
三妹,剛提完問題就打盹啊,繼續聽哥給你慢慢講啊。
LinkedList
其實是一個雙向連結串列,來看原始碼。
public class LinkedList<E>
{
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
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;
}
}
}
複製程式碼
1)LinkedList
包含一個非常重要的內部類——Node
。Node
是節點所對應的資料結構,item
為當前節點的值,prev
為上一個節點,next
為下一個節點——這也正是“雙向”連結串列的原因。first
為 LinkedList
的第一個節點,last
為最後一個節點。
2)size
是 LinkedList
的節點個數。當往 LinkedList
新增一個元素時,size+1,刪除一個元素時,size-1。
ArrayList
其實是一個動態陣列,來看原始碼。
public class ArrayList<E>
{
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
}
複製程式碼
1)elementData
是 Object
型別的陣列,用來儲存新增到 ArrayList
中的元素。如果通過預設構造引數建立 ArrayList
物件時,elementData
的預設大小是 10。當 ArrayList
容量不足以容納全部元素時,就會重新設定容量,新的容量 = 原始容量 + (原始容量 >> 1)
(參照以下程式碼)。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
elementData = Arrays.copyOf(elementData, newCapacity);
}
複製程式碼
>>
運算子還沒有駕馭了。不過,通過程式碼測試後的結論是,當原始容量為 10 的時候,新的容量為 15;當原始容量為 20 的時候,新的容量為 30。
2) size
是 ArrayList
的元素個數。當往 ArrayList
新增一個元素時,size+1,刪除一個元素時,size-1。
由於 LinkedList
和 ArrayList
底層實現的不同(一個雙向連結串列,一個動態陣列),它們之間的區別也很一目瞭然。
關鍵點1 :LinkedList
在新增(add(E e)
)、插入(add(int index, E element)
)、刪除(remove(int index)
)元素的效能上遠超 ArrayList
。
為什麼呢?先來看 ArrayList
的相關原始碼。
// ensureCapacityInternal() 方法內部會呼叫 System.arraycopy()
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
public E remove(int index) {
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
的原始碼,就能夠發現,ArrayList
在新增、插入、刪除元素的時候,會有意或者無意(擴容)的呼叫 System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
方法,該方法對效能的損耗是非常嚴重的。
再來看 LinkedList
的相關原始碼。
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
}
/**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
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;
return element;
}
複製程式碼
LinkedList
不存在擴容的問題,也不需要對原有的元素進行復制;只需要改變節點的資料就好了。
關鍵點2:LinkedList
在查詢元素時要慢於 ArrayList
。
為什麼呢?先來看 LinkedList 的相關原始碼。
/**
* Returns the (non-null) Node at the specified element index.
*/
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;
}
}
複製程式碼
觀察 LinkedList
的原始碼,就能夠發現, LinkedList
在定位 index
的時候會先判斷位置(是在 1 / 2 的前面還是後面),再從前往後或者從後往前執行 for
迴圈依次找。
再來看 ArrayList
的相關原始碼。
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
複製程式碼
ArrayList
直接根據 index
從陣列中取出該位置上的元素,不需要 for
迴圈遍歷啊——這樣顯然更快!
03、二哥,HashMap 和 TreeMap 有什麼區別啊?
三妹,提問題越來越有藝術了啊?繼續聽哥給你慢慢講啊。
HashMap
儲存的是鍵值對,其鍵是一個雜湊碼(Hash 的直譯,也稱作雜湊)。來看原始碼。
public class HashMap<K,V>
{
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
public HashMap(int initialCapacity, float loadFactor) {
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
}
複製程式碼
1)table
是一個 Node
陣列,而 Node
是一個單向連結串列(只有 next)。HashMap
的鍵值對就儲存在 table
陣列中。
2)loadFactor
就是大名鼎鼎的載入因子,預設的載入因子是 0.75, 據說這是在時間和空間成本上尋求的一種折衷。
3)initialCapacity
就是初始容量,預設為 16。
4)threshold
是 HashMap
的閾值——判斷是否需要對 HashMap
進行擴容,threshold
的值 = 容量 * 載入因子,當 HashMap
中儲存的資料數量達到 threshold
時,就需要將 HashMap
的容量加倍。
“初始容量” 和 “載入因子”對 HashMap
的效能影響頗大。容量是 HashMap
中桶(見下圖)的數量,初始容量只是 HashMap
在建立時的容量。載入因子是 HashMap
在其容量自動增加之前可以達到多滿的一種尺度。
TreeMap
儲存的是有序的鍵值對,基於紅黑樹(Red-Black tree)實現。可以在初始化的時候指定鍵位的排序方式,如果沒有指定的話就根據鍵位的自然順序進行排序。來看原始碼。
public class TreeMap<K,V>
{
private final Comparator<? super K> comparator;
private transient Entry<K,V> root;
private static final boolean RED = false;
private static final boolean BLACK = true;
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
}
}
複製程式碼
1)root
是紅黑樹的根節點,是一個 Entry
型別(按照 key 進行排序),包含了 key(鍵)、value(值)、left(左邊的子節點)、right(右邊的子節點)、parent(父節點)、color(顏色)。
2)comparator
是紅黑樹的排序方式,是一個 Comparator
介面型別,該介面裡面有一個 compare
方法,有兩個引數 T o1
和 T o2
,是泛型的表示方式,表示待比較的兩個物件,該方法的返回值是一個整形, o1大於o2,返回正整數; o1等於o2,返回0;o1小於o3,返回負整數。
總結一下就是,HashMap
適用於在 Map
中插入、刪除和定位元素;TreeMap
適用於按自然順序或自定義順序遍歷鍵(key)。
04、二哥,再講講二分查詢唄!
三妹,沒有任何問題,包在我身上。不過,在講之前,你能先去給哥泡杯咖啡嗎?
通常,我們從陣列中查詢一個元素時,需要對整個陣列進行遍歷。但如果這個陣列是排序過的,就可以進行二分查詢了。
二分查詢的方式:
第一步,將陣列中間位置上的元素與要查詢的物件進行比較,如果兩者相等,則查詢成功;否則進行第二步。
第二步,利用中間位置將陣列分割成前、後兩個子集。
第三步,比較要查詢的物件與中間位置上的元素,如果前者大於後者,則在後面的子集中按照之前的方式進行查詢;否則,在前面的子集中按照之前的方式進行查詢。
這樣做可以將查詢範圍縮減一半,大大的減少了查詢的次數。
Collections
類的 binarySearch()
方法實現了二分查詢這個演算法,可以直接使用,前提是先要排序,否則將返回 -2。原始碼如下。
private static <T>
int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
int low = 0;
int high = list.size()-1;
while (low <= high) {
int mid = (low + high) >>> 1;
Comparable<? super T> midVal = list.get(mid);
int cmp = midVal.compareTo(key);
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found
}
複製程式碼
我們來測試一下。
List<String> list1 = new ArrayList<>();
list1.add("沉");
list1.add("默");
list1.add("王");
list1.add("二");
Collections.sort(list1); // 先要排序
System.out.println(Collections.binarySearch(list1, "王")); // 2
複製程式碼
05、故事的未完待續
“二哥,終於講完《集合》了,喝口咖啡吧!”三妹的態度很體貼。
“謝謝。”
“二哥,如果這篇文章繼續遭受到批評,你會不會氣餒啊?”三妹眨了眨眼睛,繼續問我,我看到她長長的睫毛,真的很美。
“嗯,對於作者來說,當然希望文章能夠得到正面的反饋,如果是負面的反饋,那也在我的意料之中。”
“為啥?”三妹很好奇。
“《教妹學 Java》是一種創新的寫作手法,市面上還沒有,新鮮、有趣的事物總需要一段時間才能被大眾接受,否則也就不叫創新了。”
“二哥,為你的勇氣點贊!”看到三妹很為我驕傲的樣子,我的心裡盛開了一朵牡丹花。