集合接口
Map接口
定義解釋
- 爲什麼要有集合框架?
早在 Java 2 中之前,Java 就提供了特設類。比如:Dictionary, Vector, Stack, 和 Properties 這些類用來存儲和操作對象組。
雖然這些類都非常有用,但是它們缺少一個核心的,統一的主題。由於這個原因,使用 Vector 類的方式和使用 Properties 類的方式有着很大不同。
集合框架被設計成要滿足以下幾個目標。
-
該框架必須是高性能的。基本集合(動態數組,鏈表,樹,哈希表)的實現也必須是高效的。
-
該框架允許不同類型的集合,以類似的方式工作,具有高度的互操作性。
-
對一個集合的擴展和適應必須是簡單的。
爲此,整個集合框架就圍繞一組標準接口而設計。你可以直接使用這些接口的標準實現,諸如: LinkedList, HashSet, 和 TreeSet 等,除此之外你也可以通過這些接口實現自己的集合。
- 集合框架有哪些東西?
主要是Collection和Map接口,和其子類 組成了完整的集合框架。
Java集合框架爲程序員提供了預先包裝的數據結構和算法來操縱他們。
集合是一個對象,可容納其他對象的引用。集合接口聲明對每一種類型的集合可以執行的操作。
集合框架的類和接口均在java.util包中。
任何對象加入集合類後,自動轉變爲Object類型,所以在取出的時候,需要進行強制類型轉換。
如何使用
想怎麼用就怎麼用,不過要根據實際情況,選擇合適的集合類。
源碼原理
源碼只能去各個實現類去看
總結
Collection 和 Map 是Java集合框架的根接口。 Set, List, Queue接口是Collection的子接口。 HashMap, Hashtable,SortedMap等都是Map接口的實現。
-
Java集合分爲 Set, List, Queue, Map四種體系。
-
Set : 無序,不可重複的集合;
-
List: 有序,可以重複的集合;
-
Queue: 隊列集合;
-
Map: 有映射關係的集合。
-
Collections和Arrays是2個工具類用於查找和排序等
所到底集合框架 考驗的就是 數據結構基礎。如果不熟悉 數組,鏈表,隊列,棧,樹,查找,排序等 就很難 看懂源碼。
List接口
定義解釋
- 什麼是list接口?
List接口是Collection的子類。主要有以下方法:
- list接口有哪些特點?
有序-數據存儲的順序就是你添加的順序
可重複
每個元素都有其對應的順序索引
可以添加Null元素
- list接口有哪些實現類?
有2個抽象子類:AbstractList和AbstractSequentialList。實現子類有:ArrayList LinkedList Stack Vector CopyOnWriteArrayList。
- list接口常用的有哪些?
ArrayList
LinkedList
Vector
Stack
CopyOnWriteArrayList
如何使用
public static <T> void println(T msg) {
System.out.println("MainCollection=" + msg.toString());
}
public static void main(String[] args) {
ArrayList<String> arrayList=new ArrayList<String>();
arrayList.add("我");
arrayList.add("是");
arrayList.add("白");
arrayList.add("嫖");
println(arrayList.toString());
}
使用非常的簡單,基本沒什麼難度。不會的看下文檔就知道他們那些方法是什麼意思了。
源碼原理
- ArrayList
使用數組實現數據的存儲
第一個構造函數
第二個構造函數,無參數,默認數組大小其實是零
第三個 傳入集合參數,則把數據拷貝過來
最普通的添加元素方法put(Object obj):
首先檢測當前容量能否再加1,不能則擴容 。
如果當前的數組大小爲0,傳入進來的值小於默認的10,則取 10.
增加容量也很簡單
首先獲取一個新的容量值:原來的+原來的/2. 可以說是1.5倍。
然後判斷新容量值是否合法。如果超過最大值,則取最大值
clear方法直接把大小至爲0,同時會把每個元素也置空,防止內存泄漏。
add(int index,E element)方法也很簡單,首先判斷index的範圍是否合法。 然後保證正確的容量。然後用System.arraycopy的方法,把目標的位置給空出來 放新元素。
remove方法有2個參數爲index和object。先看index
很簡單,先檢測參數的合法性,然後獲取目標位置的元素,最後返回出去。還是利用System.arraycopy方法把目標位置給填了,再把最後一個重複的元素置空。object一樣很簡單就不看了。
總的來說ArrayList的源碼還是很簡單的,從源碼來看 數據量很大的時候就不要使用了。因爲它不會縮小容量。如果知道數據大概的大小,建議傳入準確值,提高效率。
- LinkedList
底層使用鏈表實現:
是雙向鏈表 :
添加元素:
很簡單,就鏈表的尾部插入,注意是雙向。
- Vector
這個就是 AarrayList的 同步版本,但是由於它是直接在方法上增加synchronized關鍵字去同步所以效率不高,目前已經很少使用
- Stack
是Vector的子類,不過它可以作爲數據結構棧使用,就是可以入棧和出棧。但是效率也不高。
總結
- 查找比較多,添加刪除少 時使用ArrayList
- 有重複元素添加時使用List
- ArrayList底層是通過數組實現,LinkedList是雙向鏈表實現
- 要注意線程同步問題
Set接口
定義解釋
Set 接口實例存儲的是無序的,不重複的數據
Set檢索效率低下,刪除和插入效率高,插入和刪除不會引起元素位置改變
接口方法有:
主要實現類:HashSet,TreeSet,LinkedHashSet,EnumSet
如何使用
public static void main(String[] args) {
HashSet<String> hashSet=new HashSet<>();
hashSet.add("我");
hashSet.add("愛");
hashSet.add("太");
hashSet.add("陽");
println(hashSet.toString());
}
使用 大同小異。這裏的運行結果是:我愛陽太。和加入的順序是不同的。但是其實順序也是固定的。這個和其內部的數據結構有關。
源碼原理
- HashSet
這個基本是個皮包類,裏面是通過 關聯了一個HashMap實現所有的Set接口功能
HashSet按照Hash算法存儲集合中的元素。 HashSet有以下特點:
不能保證元素的排列順序 HashSet不是同步的,多個線程訪問時,如果有多線程同時修改,需要代碼來保證同步。 集合元素值可以是null 向HashSet 添加元素時,會調用該對象的 hashCode()得到該對象的hashCode值,根據hashCode值決定 HashSet的存儲位置。 判斷兩個元素相等的標準是 通過 equals()方法比較相等,並且hashCode()返回的值也相等。
- TreeSet
這個基本也是個皮包類,裏面是通過 關聯了一個TreeMap實現所有的Set接口功能
SortedSet 是 Set的子接口。 TreeSet是 SortedSet接口的實現類,保證集合元素處於排序狀態。
TreeSet支持兩種排序規則: 自然排序和定製排序。 TreeSet會調用集合元素的 compareTo()方法比較元素的大小關係,然後按照升序排列, 這種爲自然排序。 定製排序需要創建TreeSet時,提供一個 Comparator對象, 實現排序邏輯。
- LinkedHashSet
LinkedHashSet是 HashSet的子類,LinkedHashSet根據元素的hashCode值決定元素的存儲位置,同時使用鏈表維護元素的次序。使元素看起來是按照插入順序保存的。 由於要維護插入順序,性能略低於HashSet性能,但迭代訪問時有很好的性能。
- EnumSet
EnumSet是專門給枚舉類設計的集合類。 所有元素必須是枚舉類型的枚舉值
總結
- Set底層都是通過HashMap實現。
- 不能存儲重複元素
- 無序,因爲是用的哈希表。
- 我們可以利用特點 來過濾相同的元素。
queue接口
定義
Queue用於模擬隊列數據結構。 通常指先進先出(FIFO)的容器。 新元素插入到隊尾, 獲取元素會返回隊頭的元素, 通常,不允許隨機訪問隊列中的元素。
Queue 是繼承於 Collection接口。
Queue接口中定義瞭如下的方法:
add() :指定元素加入到隊列的尾部
element(): 獲取頭部元素,但不刪除該元素
offer():指定元素加入到隊列尾部,如果發現隊列已滿無法添加的話,會直接返回false。
peek(): 獲取頭部元素, 但是不刪除, 隊列爲空,返回 null
poll(): 獲取頭部元素,並刪除該元素, 隊列爲空,返回Null
remove():獲取頭部元素,並刪除該元素, 隊列爲空時拋出異常NoSuchElementException
主要的實現類:ArrayBlockQueue,LinkedBlockingQueue,DelayQueue,PriorityQueue等。
Deque集合
Deque接口 是 Queue 接口的子接口。 代表雙端隊列。
Deque 還可當做棧來使用, 包含了 pop() 出棧 和 push()入棧兩個方法。 主要包括以下方法:
addFirst() :插入開頭
addLast(): 插入末尾
descendingIterator(): 返回對應的迭代器,以逆向順序來迭代隊列
getFirst(): 獲取不刪除第一個元素
getLast(): 獲取不刪除最後一個元素
offerFirst(): 插入開頭
offerLast(): 插入末尾
peekFirst():獲取不刪除第一個元素
peekLast(): 獲取不刪除最後一個元素
pollFirst(): 獲取,刪除第一個
pollLast(): 獲取, 刪除最後一個
removeFirst(): 獲取, 刪除一個
removeFirstOccurrence(): 刪除第一次出現的元素
removeLast():獲取,刪除最後一個
removeLastOccurrence(): 刪除最後一個出現的元素
ArrayDeque 是 Deque的實現類。是基於數組實現的雙端隊列。 採用動態,可重分配的Object[]數組來存儲元素。 可以指定Object[]數組長度,默認爲16.
主要的實現類有:ArrayDeque,LinkedBlockingDeque,LinkedList。
當然後還有阻塞隊列。
整個Queue接口的結構如下:
使用
和使用 列表類似
比如使用PriorityQueue:
PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.offer(4);
priorityQueue.offer(-2);
priorityQueue.offer(8);
priorityQueue.offer(6);
System.out.println(priorityQueue);
System.out.println(priorityQueue.poll());
PriorityQueue 不是絕對標準的隊列實現。 PriorityQueue 保存元素的順序不是按照加入隊列的順序, 而是按照元素的大小重新排序。
上面輸出並沒有完全按照大小排序輸出,這只是受到toString()方法返回值的影響, 實際上多次 poll(),元素是按照從小到大移出隊列。 PriorityQueue 不允許插入 null元素,需要對元素進行排序, 排序有 自然排序 和 定製排序 兩種, 和 TreeSet 基本一致。
比如使用ArrayDeque :
ArrayDeque<String> stack = new ArrayDeque<>();
stack.push("A");
stack.push("B");
stack.push("C");
System.out.println(stack);
System.out.println(stack.peek());
System.out.println(stack.pop());
System.out.println(stack);
推薦使用 ArrayDeque代替 Stack,Stack是古老的集合, 性能較差。
ArrayDeque 也可以當做隊列使用, 先進先出。
ArrayDeque queue = new ArrayDeque();
queue.offer("A");
queue.offer("B");
queue.offer("C");
System.out.println(queue);
System.out.println(queue.peek());
System.out.println(queue);
System.out.println(queue.poll());
System.out.println(queue);
LinkedList 是 List接口的實現類,可以根據索引隨機訪問, LinkedList還是實現類 Deque的接口。 可以當做雙端隊列,即可以當 棧, 也可以當 隊列
原理
整體和上面的Arrylist差不多
總結
- 需要排隊的時候可以使用
- 有阻塞隊列和非阻塞隊列
- 生產者和消費者
Map接口
定義
Map集合用於保存映射關係的數據,Map集合中保存了兩組值,一組是 key, 一組是 value。 Map的key不能重複。 key和value之間存在單向一對一的關係, 通過key,能找到唯一的,確定的value。
map接口中定義的方法:
使用
public static void main(String[] args) {
HashMap<String, String> hashMap=new HashMap<>();
hashMap.put("a", "我");
hashMap.put("b", "愛");
hashMap.put("c", "太");
hashMap.put("d", "陽");
println(hashMap.toString());
Set<Entry<String, String>> entrySet = hashMap.entrySet();
Iterator<Entry<String, String>> iterator = entrySet.iterator();
while (iterator.hasNext()) {
Entry<String, String> entry=iterator.next();
println(entry.getKey()+"---"+entry.getValue());
}
}
運行結果:
使用也是非常簡單的,就是 put 和get。關鍵還是源碼。Key的存儲是根據hashcode來的。
原理
構造函數
默認的話。構造因子就是0.75,意思就是 數組的容量是100,如果當前已有75個數據時就要 擴容數組了。初始默認的數組容量是16.最大容量是2的30次方。10億。 整個結構類似:
怎麼實現存儲數據?
其實就是看Put方法的實現:
計算Hash值。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
這是整個Put方法。我來解釋下
- 首先判斷hash表是否爲空,是則resize()擴容,這個方法也比較複雜
- 根據i = (n - 1) & hash 獲取下標如果沒有元素則把新的節點插入完事
- 如果有元素了,那就是去解決衝突問題了。首先判斷key是否相等,如果相等則直接替換完事
- 如果當前節點是樹節點 就是已經是紅黑樹結構則插入樹對應的位置。
- 如果是還是鏈表節點則插入末尾
- 插入後如果節點數大於等於了7 則轉化爲樹
- 最後如果元素數量達到了臨界值,則調用resize()擴容
怎麼擴容?
擴容就是resize()方法:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
這個方法也很長,需要解釋下:
- 判斷原來的表如果大於0,並且已經大於最大值2的30次方,10億,MAXIMUM_CAPACITY,則把容量賦值爲2147483647 2的31次方,20億。
- 否則左移一位,翻倍擴容。
- 重新複製臨界值大小
- 接着就要把原表的數據 拷貝過來。
- 循環遍歷舊錶,如果元素不爲空,則把原表元素置空。
- 如果此節點沒有下一個節點那好辦用 e.hash & (newCap - 1) 獲取新表的下標,把元素放入就好了。
- 如果是紅黑樹節點了,則分割紅黑樹節點到新表
- 則分解 鏈表節點到新表
總的來說還是很複雜的。
怎麼解決哈希衝突?
使用的是 鏈地址法。通過一個鏈表維護。如果節點數量超過7個則轉化爲紅黑樹 提高性能。轉化過程也是相當複雜,涉及了紅黑的所有知識。旋轉,平衡等
get的實現?
get方法相對 remove方法就簡單點了。
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
- 首先獲取傳入進來的key的hash值。
- 判斷當前的表 不爲空,有元素,且根據hash獲取的第一個節點不爲空
- 如果此節點的hash值和傳入的是一樣的。則返回此節點
- 如果不是,則判斷此節點是屬於鏈表,還是紅黑樹。
- 則在鏈表中查詢返回,後者是紅黑樹中查詢返回。
- 紅黑樹是根據hash大小來排序的。
怎麼遍歷?
使用迭代器模式進行遍歷
最終實現:HashIterator類中
總結
- 哈希表
- 紅黑樹
- 單鏈表
- 代碼實現的還是有難度,暫時只能掌握思想。