前言:把這段時間複習的關於集合類的東西整理出來,特別是HashMap相關的一些東西,以前都沒有很注意1.7 ->> 1.8的變化問題,但後來發現這其實變化挺大的,並且不少整理的面試資料都沒有更新(包括我以前整理的…)html
答:Map接口和Collection接口是全部集合框架的父接口:java
Collection接口的子接口包括:Set接口和List接口git
Map接口的實現類主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等github
Set接口的實現類主要有:HashSet、TreeSet、LinkedHashSet等web
List接口的實現類主要有:ArrayList、LinkedList、Stack以及Vector等面試
答:算法
HashMap沒有考慮同步,是線程不安全的;Hashtable使用了synchronized關鍵字,是線程安全的;數組
HashMap容許K/V都爲null;後者K/V都不容許爲null;安全
HashMap繼承自AbstractMap類;而Hashtable繼承自Dictionary類;微信
圖引用自:https://blog.csdn.net/u011240877/article/details/53358305
答:下面先來分析一下源碼
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
// 1.若是table爲空或者長度爲0,即沒有元素,那麼使用resize()方法擴容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2.計算插入存儲的數組索引i,此處計算方法同 1.7 中的indexFor()方法
// 若是數組爲空,即不存在Hash衝突,則直接插入數組
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 3.插入時,若是發生Hash衝突,則依次往下判斷
else {
HashMap.Node<K,V> e; K k;
// a.判斷table[i]的元素的key是否與須要插入的key同樣,若相同則直接用新的value覆蓋掉舊的value
// 判斷原則equals() - 因此須要當key的對象重寫該方法
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// b.繼續判斷:須要插入的數據結構是紅黑樹仍是鏈表
// 若是是紅黑樹,則直接在樹中插入 or 更新鍵值對
else if (p instanceof HashMap.TreeNode)
e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 若是是鏈表,則在鏈表中插入 or 更新鍵值對
else {
// i .遍歷table[i],判斷key是否已存在:採用equals對比當前遍歷結點的key與須要插入數據的key
// 若是存在相同的,則直接覆蓋
// ii.遍歷完畢後任務發現上述狀況,則直接在鏈表尾部插入數據
// 插入完成後判斷鏈表長度是否 > 8:如果,則把鏈表轉換成紅黑樹
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;
}
}
// 對於i 狀況的後續操做:發現key已存在,直接用新value覆蓋舊value&返回舊value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 插入成功後,判斷實際存在的鍵值對數量size > 最大容量
// 若是大於則進行擴容
if (++size > threshold)
resize();
// 插入成功時會調用的方法(默認實現爲空)
afterNodeInsertion(evict);
return null;
}
圖片簡單總結爲:
答:經過分析源碼咱們知道了HashMap經過resize()
方法進行擴容或者初始化的操做,下面是對源碼進行的一些簡單分析:
/**
* 該函數有2中使用狀況:1.初始化哈希表;2.當前數組容量太小,須要擴容
*/
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) {
// 針對狀況2:若擴容前的數組容量超過最大值,則再也不擴容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 針對狀況2:若沒有超過最大值,就擴容爲原來的2倍(左移1位)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 針對狀況1:初始化哈希表(採用指定或者使用默認值的方式)
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);
}
// 計算新的resize上限
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) {
// 把每個bucket都移動到新的bucket中去
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;
}
參考資料:https://juejin.im/post/5ab99afff265da23a2291dee
答:在解決這個問題以前,咱們首先須要知道什麼是哈希衝突,而在瞭解哈希衝突以前咱們還要知道什麼是哈希才行;
Hash,通常翻譯爲「散列」,也有直接音譯爲「哈希」的,這就是把任意長度的輸入經過散列算法,變換成固定長度的輸出,該輸出就是散列值(哈希值);這種轉換是一種壓縮映射,也就是,散列值的空間一般遠小於輸入的空間,不一樣的輸入可能會散列成相同的輸出,因此不可能從散列值來惟一的肯定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。
全部散列函數都有以下一個基本特性:根據同一散列函數計算出的散列值若是不一樣,那麼輸入值確定也不一樣。可是,根據同一散列函數計算出的散列值若是相同,輸入值不必定相同。
當兩個不一樣的輸入值,根據同一散列函數計算出相同的散列值的現象,咱們就把它叫作碰撞(哈希碰撞)。
在Java中,保存數據有兩種比較簡單的數據結構:數組和鏈表。數組的特色是:尋址容易,插入和刪除困難;鏈表的特色是:尋址困難,但插入和刪除容易;因此咱們將數組和鏈表結合在一塊兒,發揮二者各自的優點,使用一種叫作鏈地址法的方式能夠解決哈希衝突:
這樣咱們就能夠將擁有相同哈希值的對象組織成一個鏈表放在hash值所對應的bucket下,但相比於hashCode返回的int類型,咱們HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4
(即2的四次方16)要遠小於int類型的範圍,因此咱們若是隻是單純的用hashCode取餘來獲取對應的bucket這將會大大增長哈希碰撞的機率,而且最壞狀況下還會將HashMap變成一個單鏈表,因此咱們還須要對hashCode做必定的優化
上面提到的問題,主要是由於若是使用hashCode取餘,那麼至關於參與運算的只有hashCode的低位,高位是沒有起到任何做用的,因此咱們的思路就是讓hashCode取值出的高位也參與運算,進一步下降hash碰撞的機率,使得數據分佈更平均,咱們把這樣的操做稱爲擾動,在JDK 1.8中的hash()函數以下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 與本身右移16位進行異或運算(高低位異或)
}
這比在JDK 1.7中,更爲簡潔,相比在1.7中的4次位運算,5次異或運算(9次擾動),在1.8中,只進行了1次位運算和1次異或運算(2次擾動);
經過上面的鏈地址法(使用散列表)和擾動函數咱們成功讓咱們的數據分佈更平均,哈希碰撞減小,可是當咱們的HashMap中存在大量數據時,加入咱們某個bucket下對應的鏈表有n個元素,那麼遍歷時間複雜度就爲O(n),爲了針對這個問題,JDK1.8在HashMap中新增了紅黑樹的數據結構,進一步使得遍歷複雜度下降至O(logn);
簡單總結一下HashMap是使用了哪些方法來有效解決哈希衝突的:
1. 使用鏈地址法(使用散列表)來連接擁有相同hash值的數據;
2. 使用2次擾動函數(hash函數)來下降哈希衝突的機率,使得數據分佈更平均;
3. 引入紅黑樹進一步下降遍歷的時間複雜度,使得遍歷更快;
答:hashCode()
方法返回的是int整數類型,其範圍爲-(2 ^ 31)~(2 ^ 31 - 1),約有40億個映射空間,而HashMap的容量範圍是在16(初始化默認值)~2 ^ 30,HashMap一般狀況下是取不到最大值的,而且設備上也難以提供這麼多的存儲空間,從而致使經過hashCode()
計算出的哈希值可能不在數組大小範圍內,進而沒法匹配存儲位置;
面試官:那怎麼解決呢?
答:
HashMap本身實現了本身的hash()
方法,經過兩次擾動使得它本身的哈希值高低位自行進行異或運算,下降哈希碰撞機率也使得數據分佈更平均;
在保證數組長度爲2的冪次方的時候,使用hash()
運算以後的值與運算(&)(數組長度 - 1)來獲取數組下標的方式進行存儲,這樣一來是比取餘操做更加有效率,二來也是由於只有當數組長度爲2的冪次方時,h&(length-1)纔等價於h%length,三來解決了「哈希值與數組大小範圍不匹配」的問題;
面試官:爲何數組長度要保證爲2的冪次方呢?
答:
只有當數組長度爲2的冪次方時,h&(length-1)纔等價於h%length,即實現了key的定位,2的冪次方也能夠減小衝突次數,提升HashMap的查詢效率;
若是 length 爲 2 的次冪 則 length-1 轉化爲二進制一定是 11111……的形式,在於 h 的二進制與操做效率會很是的快,並且空間不浪費;若是 length 不是 2 的次冪,好比 length 爲 15,則 length - 1 爲 14,對應的二進制爲 1110,在於 h 與操做,最後一位都爲 0 ,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠都不能存放元素了,空間浪費至關大,更糟的是這種狀況中,數組可使用的位置比數組長度小了不少,這意味着進一步增長了碰撞的概率,減慢了查詢的效率!這樣就會形成空間的浪費。
面試官:那爲何是兩次擾動呢?
答:這樣就是加大哈希值低位的隨機性,使得分佈更均勻,從而提升對應數組存儲下標位置的隨機性&均勻性,最終減小Hash衝突,兩次就夠了,已經達到了高位低位同時參與運算的目的;
答:
不一樣 | JDK 1.7 | JDK 1.8 |
---|---|---|
存儲結構 | 數組 + 鏈表 | 數組 + 鏈表 + 紅黑樹 |
初始化方式 | 單獨函數:inflateTable() |
直接集成到了擴容函數resize() 中 |
hash值計算方式 | 擾動處理 = 9次擾動 = 4次位運算 + 5次異或運算 | 擾動處理 = 2次擾動 = 1次位運算 + 1次異或運算 |
存放數據的規則 | 無衝突時,存放數組;衝突時,存放鏈表 | 無衝突時,存放數組;衝突 & 鏈表長度 < 8:存放單鏈表;衝突 & 鏈表長度 > 8:樹化並存放紅黑樹 |
插入數據方式 | 頭插法(先講原位置的數據移到後1位,再插入數據到該位置) | 尾插法(直接插入到鏈表尾部/紅黑樹) |
擴容後存儲位置的計算方式 | 所有按照原來方法進行計算(即hashCode ->> 擾動函數 ->> (h&length-1)) | 按照擴容後的規律計算(即擴容後的位置=原位置 or 原位置 + 舊容量) |
答:String、Integer等包裝類的特性可以保證Hash值的不可更改性和計算準確性,可以有效的減小Hash碰撞的概率
都是final類型,即不可變性,保證key的不可更改性,不會存在獲取hash值不一樣的狀況
內部已重寫了equals()
、hashCode()
等方法,遵照了HashMap內部的規範(不清楚能夠去上面看看putValue的過程),不容易出現Hash值計算錯誤的狀況;
面試官:若是我想要讓本身的Object做爲K應該怎麼辦呢?
答:重寫hashCode()
和equals()
方法
重寫hashCode()
是由於須要計算存儲數據的存儲位置,須要注意不要試圖從散列碼計算中排除掉一個對象的關鍵部分來提升性能,這樣雖然能更快但可能會致使更多的Hash碰撞;
重寫`equals()`方法,須要遵照自反性、對稱性、傳遞性、一致性以及對於任何非null的引用值x,x.equals(null)必須返回false的這幾個特性,目的是爲了保證key在哈希表中的惟一性;
答:ConcurrentHashMap 結合了 HashMap 和 HashTable 兩者的優點。HashMap 沒有考慮同步,HashTable 考慮了同步的問題。可是 HashTable 在每次同步執行時都要鎖住整個結構。 ConcurrentHashMap 鎖的方式是稍微細粒度的。
面試官:ConcurrentHashMap的具體實現知道嗎?
參考資料:http://www.importnew.com/23610.html
答:在JDK1.7中,ConcurrentHashMap採用Segment + HashEntry的方式進行實現,結構以下:
該類包含兩個靜態內部類 HashEntry 和 Segment ;前者用來封裝映射表的鍵值對,後者用來充當鎖的角色;
Segment 是一種可重入的鎖 ReentrantLock,每一個 Segment 守護一個HashEntry 數組裏得元素,當對 HashEntry 數組的數據進行修改時,必須首先得到對應的 Segment 鎖。
在JDK1.8中,放棄了Segment臃腫的設計,取而代之的是採用Node + CAS + Synchronized來保證併發安全進行實現,結構以下:
插入元素過程(建議去看看源碼):
若是相應位置的Node尚未初始化,則調用CAS插入相應的數據;
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
若是相應位置的Node不爲空,且當前該節點不處於移動狀態,則對該節點加synchronized鎖,若是該節點的hash不小於0,則遍歷鏈表更新節點或插入新節點;
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
若是該節點是TreeBin類型的節點,說明是紅黑樹結構,則經過putTreeVal方法往紅黑樹中插入節點;若是binCount不爲0,說明put操做對數據產生了影響,若是當前鏈表的個數達到8個,則經過treeifyBin方法轉化爲紅黑樹,若是oldVal不爲空,說明是一次更新操做,沒有對元素個數產生影響,則直接返回舊值;
若是插入的是一個新節點,則執行addCount()方法嘗試更新元素個數baseCount;
答:
是java集合的一種錯誤檢測機制,當多個線程對集合進行結構上的改變的操做時,有可能會產生 fail-fast 機制。
例如:假設存在兩個線程(線程一、線程2),線程1經過Iterator在遍歷集合A中的元素,在某個時候線程2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),那麼這個時候程序就會拋出 ConcurrentModificationException 異常,從而產生fail-fast機制。
緣由:迭代器在遍歷時直接訪問集合中的內容,而且在遍歷過程當中使用一個 modCount 變量。集合在被遍歷期間若是內容發生變化,就會改變modCount的值。每當迭代器使用hashNext()/next()遍歷下一個元素以前,都會檢測modCount變量是否爲expectedmodCount值,是的話就返回遍歷;不然拋出異常,終止遍歷。
解決辦法:
1. 在遍歷過程當中,全部涉及到改變modCount值得地方所有加上synchronized。
2. 使用CopyOnWriteArrayList來替換ArrayList
答:
這兩個類都實現了 List 接口(List 接口繼承了 Collection 接口),他們都是有序集合,即存儲在這兩個集合中的元素位置都是有順序的,至關於一種動態的數組,咱們之後能夠按位置索引來取出某個元素,而且其中的數據是容許重複的,這是與 HashSet 之類的集合的最大不一樣處,HashSet 之類的集合不能夠按索引號去檢索其中的元素,也不容許有重複的元素。
ArrayList 與 Vector 的區別主要包括兩個方面:
同步性:
Vector 是線程安全的,也就是說它的方法之間是線程同步(加了synchronized 關鍵字)的,而 ArrayList 是線程不安全的,它的方法之間是線程不一樣步的。若是隻有一個線程會訪問到集合,那最好是使用 ArrayList,由於它不考慮線程安全的問題,因此效率會高一些;若是有多個線程會訪問到集合,那最好是使用 Vector,由於不須要咱們本身再去考慮和編寫線程安全的代碼。
數據增加:
ArrayList 與 Vector 都有一個初始的容量大小,當存儲進它們裏面的元素的我的超過了容量時,就須要增長 ArrayList 和 Vector 的存儲空間,每次要增長存儲空間時,不是隻增長一個存儲單元,而是增長多個存儲單元,每次增長的存儲單元的個數在內存空間利用與程序效率之間要去的必定的平衡。Vector 在數據滿時(加載因子1)增加爲原來的兩倍(擴容增量:原容量的 2 倍),而 ArrayList 在數據量達到容量的一半時(加載因子 0.5)增加爲原容量的 (0.5 倍 + 1) 個空間。
答:
LinkedList 實現了 List 和 Deque 接口,通常稱爲雙向鏈表;ArrayList 實現了 List 接口,動態數組;
LinkedList 在插入和刪除數據時效率更高,ArrayList 在查找某個 index 的數據時效率更高;
LinkedList 比 ArrayList 須要更多的內存;
面試官:Array 和 ArrayList 有什麼區別?何時該應 Array 而不是 ArrayList 呢?
答:它們的區別是:
Array 能夠包含基本類型和對象類型,ArrayList 只能包含對象類型。
Array 大小是固定的,ArrayList 的大小是動態變化的。
ArrayList 提供了更多的方法和特性,好比:addAll(),removeAll(),iterator() 等等。
對於基本類型數據,集合使用自動裝箱來減小編碼工做量。可是,當處理固定大小的基本數據類型的時候,這種方式相對比較慢。
答:HashSet的底層其實就是HashMap,只不過咱們HashSet是實現了Set接口而且把數據做爲K值,而V值一直使用一個相同的虛值來保存,咱們能夠看到源碼:
public boolean add(E e) {
return map.put(e, PRESENT)==null;// 調用HashMap的put方法,PRESENT是一個至始至終都相同的虛值
}
因爲HashMap的K值自己就不容許重複,而且在HashMap中若是K/V相同時,會用新的V覆蓋掉舊的V,而後返回舊的V,那麼在HashSet中執行這一句話始終會返回一個false,致使插入失敗,這樣就保證了數據的不可重複性;
答:
Java.util.concurrent.BlockingQueue是一個隊列,在進行檢索或移除一個元素的時候,它會等待隊列變爲非空;當在添加一個元素時,它會等待隊列中的可用空間。BlockingQueue接口是Java集合框架的一部分,主要用於實現生產者-消費者模式。咱們不須要擔憂等待生產者有可用的空間,或消費者有可用的對象,由於它都在BlockingQueue的實現類中被處理了。Java提供了集中BlockingQueue的實現,好比ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。
歡迎轉載,轉載請註明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關注公衆微信號:wmyskxz_javaweb
分享本身的Java Web學習之路以及各類Java學習資料
想要交流的朋友也能夠加qq羣:3382693
我沒有三顆心臟