文章內容
容器總體結構
public interface Collection<E> extends Iterable<E> {}
public interface Map<K, V> {
...
Collection<V> values();
...
}
public interface List<E> extends Collection<E> {}
public abstract class AbstractCollection<E> implements Collection<E> {}
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {}
複製程式碼
2.Map
2.1 HashMap
關 注 點 | 結 論 |
---|---|
HashMap是否允許空 | Key和Value都允許為空,允許<null, null>的鍵值對 |
HashMap是否允許重複資料 | Key重複會覆蓋、Value允許重複 |
HashMap是否有序 | 無序,特別說明這個無序指的是遍歷HashMap的時候,得到的元素的順序基本不可能是put的順序 |
HashMap是否執行緒安全 | 非執行緒安全 |
2.1.1 HashMap的儲存結構
2.1.2 兩個重要的引數 Capacity 和 Load Factor
簡單的說,Capacity就是buckets的數目,容量都是2的冪。Load factor就是buckets填滿程度的最大比例。如果對迭代效能要求很高的話不要把capacity設定過大,也不要把load factor設定過小。當bucket填充的數目(即hashmap中元素的個數)大於capacity*load factor時就需要調整buckets的數目為當前的2倍。
2.1.3 put方法的實現
put函式大致的思路為:
- 對key的hashCode()做hash(呼叫了hash方法),然後再計算index;
- 如果沒碰撞直接放到bucket裡;
- 如果碰撞了,以連結串列的形式存在buckets後;
- 如果碰撞導致連結串列過長(大於等於TREEIFY_THRESHOLD),就把連結串列轉換成紅黑樹(Java 8中做的優化,但是在Android API25 中還沒有看到這樣的優化);
- 如果節點已經存在就替換old value(保證key的唯一性)
- 如果bucket滿了(超過load factor*current capacity),就要resize。
2.1.4 get方法的實現
在理解了put之後,get就很簡單了。大致思路如下:
- bucket裡的第一個節點,直接命中;
- 如果有衝突,則通過key.equals(k)去查詢對應的entry
- 若為樹,則在樹中通過key.equals(k)查詢,O(logn);
- 若為連結串列,則在連結串列中通過key.equals(k)查詢,O(n)。
2.1.5 hash方法的實現
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製程式碼
2.1.6 resize
當put時,如果發現目前的bucket佔用程度已經超過了Load Factor所希望的比例,那麼就會發生resize。在resize的過程,簡單的說就是把bucket擴充為2倍,之後重新計算index,把節點再放到新的bucket中。
2.1.7 紅黑樹和連結串列的實現
我們在樹中確實儲存了比連結串列更多的資料。
根據繼承原則,內部表中可以包含Node(連結串列)或者TreeNode(紅黑樹)。Oracle決定根據下面的規則來使用這兩種資料結構:
-
對於內部表中的指定索引(桶),如果node的數目多於8個(TREEIFY_THRESHOLD = 8),那麼連結串列就會被轉換成紅黑樹。
-
對於內部表中的指定索引(桶),如果node的數目小於6個(UNTREEIFY_THRESHOLD = 6),那麼紅黑樹就會被轉換成連結串列。
參考:
- Java HashMap工作原理及實現(主要參考這一篇,寫的很清晰)
- 深入分析hashmap
- Java HashMap工作原理
2.2 Hashtable
關 注 點 | 結 論 |
---|---|
Hashtable是否允許空 | key和value都不允許null |
Hashtable是否允許重複資料 | key不能重複,value允許 |
Hashtable是否有序 | 不保證有序 |
Hashtable是否執行緒安全 | 執行緒安全 |
Hashtable和HashMap區別
-
第一,繼承不同。
public class Hashtable extends Dictionary implements Map
public class HashMap extends AbstractMap implements Map
-
第二,Hashtable中的方法是同步的(通過synchronized實現),而HashMap中的方法在預設情況下是非同步的。在多執行緒併發的環境下,可以直接使用Hashtable,但是要使用HashMap的話就要自己增加同步處理了。
-
第三,Hashtable中,key和value都不允許出現null值。在HashMap中,null可以作為鍵,這樣的鍵只有一個;可以有一個或多個鍵所對應的值為null。
當get()方法返回null值時,既可以表示HashMap中沒有該鍵,也可以表示該鍵所對應的值為null。因此,**在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵,**而應該用containsKey()方法來判斷。
-
第四,兩個遍歷方式的內部實現上不同。Hashtable,HashMap都使用了Iterator。而由於歷史原因,Hashtable還使用了Enumeration的方式。並且HashMap的迭代器是fail-fast機制,Hashtable的Enumeration迭代器不是fail-fast的
-
第五,雜湊值的使用不同,Hashtable直接使用物件的hashCode。而HashMap重新計算hash值。
-
第六,Hashtable和HashMap它們兩個內部實現方式的陣列的初始大小和擴容的方式。Hashtable中hash陣列預設大小是11,增加的方式是 old*2+1。HashMap中hash陣列的預設大小是16,而且一定是2的指數。
參考
2.3 LinkedHashMap
關 注 點 | 結 論 |
---|---|
LinkedHashMap是否允許空 | Key和Value都允許空 |
LinkedHashMap是否允許重複資料 | Key重複會覆蓋、Value允許重複 |
LinkedHashMap是否有序 | 有序 |
LinkedHashMap是否執行緒安全 | 非執行緒安全 |
Entry增加了兩個變數,after和befor用來維護雙向連結串列
LruCache使用LinkedHashMap作為快取
對節點進行訪問後會呼叫afterNodeAccess方法,更新列表,將最近訪問的元素放在最後,afterNodeAccess 會呼叫afterNodeInsertion方法,在afterNodeInsertion方法中會呼叫removeNode方法,而在LruCache中對重寫removeNode方法,當size超過LruCache的容量的時候就會刪除。(不同的JDK和Android API中實現的方式有所不同)
參考
2.4 TreeMap
關 注 點 | 結 論 |
---|---|
TreeMap是否允許空 | Key不能為null,Value允許空 |
TreeMap是否允許重複資料 | Key重複會覆蓋、Value允許重複 |
TreeMap是否有序 | 不能保證按插入的順序有序,而是根據Key值進行排序 |
TreeMap是否執行緒安全 | 非執行緒安全 |
之前已經學習過HashMap和LinkedHashMap了,HashMap不保證資料有序,LinkedHashMap保證資料可以保持插入順序,而如果我們希望Map可以保持key的大小順序的時候,我們就需要利用TreeMap了。
另外需要參考紅黑樹的筆記
參考文章
2.5 WeakHashMap
WeakHashMap是一種改進的HashMap,它對key實行“弱引用”.
private static class Entry<K, V> extends WeakReference<Object> implements java.util.Map.Entry<K, V>
複製程式碼
3.Collection
不要將Collection誤認為Collections
- 1.
java.util.Collection
是一個集合介面。它提供了對集合物件進行基本操作的通用介面方法。Collection介面在Java 類庫中有很多具體的實現。Collection介面的意義是為各種具體的集合提供了最大化的統一操作方式。 - 2.
java.util.Collections
是一個包裝類。它包含有各種有關集合操作的靜態多型方法。此類不能例項化,就像一個工具類,服務於Java的Collection框架。
3.1 List
3.1.1 ArrayList
關 注 點 | 結 論 |
---|---|
ArrayList是否允許空 | 允許 |
ArrayList是否允許重複資料 | 允許 |
ArrayList是否有序 | 有序 |
ArrayList是否執行緒安全 | 非執行緒安全 |
(是否有序,有序的意思是讀取資料的順序和存放資料的順序是否一致)
ArrayList比較適合順序新增.隨機訪問的場景。
增加元素進行擴容
內部維護了一個int型別的size和Object[]陣列,預設大小是10,如果add之後大小不夠的話會呼叫ensureCapacityInternal
方法進行動態擴容,擴容後的大小是原大小的1.5倍,並呼叫Arrays.copyOf進行一次陣列的複製。(查閱原始碼發現預設情況下ArrayList的最大長度是Integer.MAX_VALUE - 8,當超過這個數值時會擴充套件到Integer.MAX_VALUE,如果超過這個限制會拋OOM)
刪除元素
不論按下標刪除還是按元素刪除,總的來說做了兩件事
-
1.把指定元素後面位置的所有元素,利用System.arraycopy方法整體向前移動一個位置
-
2.最後一個位置的元素指定為null,這樣讓gc可以去回收它
優點
-
1.ArrayList底層以陣列實現,是一種隨機訪問模式,再加上它實現了RandomAccess介面,因此查詢也就是get的時候非常快
-
2.ArrayList在順序新增一個元素的時候非常方便,只是往陣列裡面新增了一個元素而已
缺點
-
1.刪除元素的時候,涉及到一次元素複製,如果要複製的元素很多,那麼就會比較耗費效能
-
2.插入元素的時候,涉及到一次元素複製,如果要複製的元素很多,那麼就會比較耗費效能
3.1.2 LinkedList
關 注 點 | 結 論 |
---|---|
LinkedList是否允許空 | 允許 |
LinkedList是否允許重複資料 | 允許 |
LinkedList是否有序 | 有序 |
LinkedList是否執行緒安全 | 非執行緒安全 |
使用雙向連結串列實現,set和get方法時間複雜度是O(n/2),同時實現了Deque介面,可以將LinkedList作為雙端佇列使用
private static class Entry<E> {
E element;
Entry<E> next;
Entry<E> previous;
...
}
複製程式碼
set和get的時間複雜度為什麼是O(n/2)?
public E get(int index) {
return entry(index).element;
}
private Entry<E> entry(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
Entry<E> e = header;
if (index < (size >> 1)) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
複製程式碼
與ArrayList的對比
-
1、順序插入速度ArrayList會比較快
-
2、因為LinkedList裡面不僅維護了待插入的元素,還維護了Entry的前置Entry和後繼Entry,如果一個LinkedList中的Entry非常多,那麼LinkedList將比ArrayList更耗費一些記憶體
-
3、使用各自遍歷效率最高的方式,ArrayList的遍歷效率會比LinkedList的遍歷效率高一些。因為使用普通for比for each快一些。
-
4、有些說法認為LinkedList做插入和刪除更快,這種說法其實是不準確的:
-(1)LinkedList做插入、刪除的時候,慢在定址,快在只需要改變前後Entry的引用地址 -(2)ArrayList做插入、刪除的時候,慢在陣列元素的批量copy,快在定址
3.1.3 Vector
關 注 點 | 結 論 |
---|---|
Vector是否允許空 | 允許 |
Vector是否允許重複資料 | 允許 |
Vector是否有序 | 有序 |
Vector是否執行緒安全 | 執行緒安全 |
類似ArrayList,內部實現的原理形同,也是通過Object[]陣列實現的。但是是執行緒安全的,但是為了同步,儘量少使用vector,因為vector的方法都是通過synchronized實現的,代價很大
3.2 Set
不包含重複元素的Collection,最多有一個null元素
3.2.1 HashSet
關 注 點 | 結 論 |
---|---|
HashSet是否允許空 | 允許,但最多一個 |
HashSet是否允許重複資料 | 不允許 |
HashSet是否有序 | 不保證有序 |
HashSet是否執行緒安全 | 非執行緒安全 |
HashSet是基於HashMap來實現的,操作很簡單,更像是對HashMap做了一次“封裝”,而且只使用了HashMap的key來實現各種特性。內部有多個不同的構造方法,對應初始化HashMap的操作也不同。
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
}
複製程式碼
實際上HashSet儲存的物件是HashMap的key,只不過HashMap的value是一個Object物件。
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
public boolean contains(Object o) {
return map.containsKey(o);
}
public int size() {
return map.size();
}
複製程式碼
如果想獲得執行緒安全的HashSet可以使用如下方法:
Collections.synchronizedSet(new HashSet<String>());
3.2.2 TreeSet
關 注 點 | 結 論 |
---|---|
TreeSet是否允許空 | 不允許 |
TreeSet是否允許重複資料 | 不允許 |
TreeSet是否有序 | 不保證有序 |
TreeSet是否執行緒安全 | 非執行緒安全 |
TreeSet是基於TreeMap實現的,也非常簡單,同樣的只是用key及其操作,然後把value置為dummy的object。
利用TreeMap的特性,實現了set的有序性(通過紅黑樹實現,這裡的有序性指的是排序後的順序,對於某些型別的元素,需要傳遞自定義的Comparable)。
TreeSet新增null時,如果再新增不是null的元素,就會報NullPointerException異常
3.2.3 LinkedHashSet
關 注 點 | 結 論 |
---|---|
LinkedHashSet是否允許空 | 允許,但最多一個 |
LinkedHashSet是否允許重複資料 | 不允許 |
LinkedHashSet是否有序 | 不保證有序 |
LinkedHashSet是否執行緒安全 | 非執行緒安全 |
繼承自HashSet,內部使用LinkedHashMap維持雙向的連結串列。
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable
複製程式碼
3.3 Queue與Deque
3.3.1 ArrayDeque
基於陣列實現,實現了一個邏輯上的迴圈陣列。
ArrayDeque的高效來源於head和tail這兩個變數,它們使得物理上簡單的從頭到尾的陣列變為了一個邏輯上迴圈的陣列,避免了在頭尾操作時的移動。我們來解釋下迴圈陣列的概念。
對於一般陣列,比如arr,第一個元素為arr[0],最後一個為arr[arr.length-1]。但對於ArrayDeque中的陣列,它是一個邏輯上的迴圈陣列,所謂迴圈是指元素到陣列尾之後可以接著從陣列頭開始,陣列的長度.第一個和最後一個元素都與head和tail這兩個變數有關,具體來說:
- 如果head和tail相同,則陣列為空,長度為0。
- 如果tail大於head,則第一個元素為elements[head],最後一個為elements[tail-1],長度為tail-head,元素索引從head到tail-1。
- 如果tail小於head,且為0,則第一個元素為elements[head],最後一個為elements[elements.length-1],元素索引從head到elements.length-1。
- 如果tail小於head,且大於0,則會形成迴圈,第一個元素為elements[head],最後一個是elements[tail-1],元素索引從head到elements.length-1,然後再從0到tail-1。
參考: