Java集合
剛剛經歷過秋招,看了大量的面經,順便將常見的Java集合常考知識點總結了一下,並根據被問到的頻率大致做了一個標註。一顆星表示知識點需要了解,被問到的頻率不高,面試時起碼能說個差不多。兩顆星表示被問到的頻率較高或對理解Java有著重要的作用,建議熟練掌握。三顆星表示被問到的頻率非常高,建議深入理解並熟練掌握其相關知識,方便麵試時擴充(方便裝逼),給面試官留下個好印象。
推薦閱讀:一文搞懂所有Java基礎知識面試題
- Java集合
- 常用的集合類有哪些? ***
- List,Set,Map三者的區別? ***
- 常用集合框架底層資料結構 ***
- 哪些集合類是執行緒安全的? ***
- 迭代器 Iterator 是什麼 *
- Java集合的快速失敗機制 “fail-fast”和安全失敗機制“fail-safe”是什麼? ***
- 如何邊遍歷邊移除 Collection 中的元素? ***
- Array 和 ArrayList 有何區別? ***
- comparable 和 comparator的區別? **
- Collection 和 Collections 有什麼區別? **
- List集合
- Set集合
- Map集合
- HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底層實現 ***
- HashMap 的長度為什麼是2的冪次方 ***
- HashMap的put方法的具體流程? **
- HashMap的擴容操作是怎麼實現的? ***
- HashMap預設載入因子為什麼選擇0.75?
- 為什麼要將連結串列中轉紅黑樹的閾值設為8?為什麼不一開始直接使用紅黑樹?
- HashMap是怎麼解決雜湊衝突的? ***
- HashMap為什麼不直接使用hashCode()處理後的雜湊值直接作為table的下標? ***
- 能否使用任何類作為 Map 的 key? ***
- 為什麼HashMap中String、Integer這樣的包裝類適合作為Key? ***
- 如果使用Object作為HashMap的Key,應該怎麼辦呢? **
- HashMap 多執行緒導致死迴圈問題 ***
- ConcurrentHashMap 底層具體實現知道嗎? **
- HashTable的底層實現知道嗎?
- HashMap、ConcurrentHashMap及Hashtable 的區別 ***
- Java集合的常用方法 **
常用的集合類有哪些? ***
Map介面和Collection介面是所有集合框架的父介面。下圖中的實線和虛線看著有些亂,其中介面與介面之間如果有聯絡為繼承關係,類與類之間如果有聯絡為繼承關係,類與介面之間則是類實現介面。重點掌握的抽象類有HashMap
,LinkedList
,HashTable
,ArrayList
,HashSet
,Stack
,TreeSet
,TreeMap
。注意:Collection
介面不是Map
的父介面。
List,Set,Map三者的區別? ***
List
:有序集合(有序指存入的順序和取出的順序相同,不是按照元素的某些特性排序),可儲存重複元素,可儲存多個null
。Set
:無序集合(元素存入和取出順序不一定相同),不可儲存重複元素,只能儲存一個null
。Map
:使用鍵值對的方式對元素進行儲存,key
是無序的,且是唯一的。value
值不唯一。不同的key
值可以對應相同的value
值。
常用集合框架底層資料結構 ***
-
Lis
t:ArrayList
:陣列LinkedList
:雙線連結串列
-
Set
:HashSet
:底層基於HashMap
實現,HashSet
存入讀取元素的方式和HashMap
中的Key
是一致的。TreeSet
:紅黑樹
-
Map
:HashMap
: JDK1.8之前HashMap
由陣列+連結串列組成的, JDK1.8之後有陣列+連結串列/紅黑樹組成,當連結串列長度大於8時,連結串列轉化為紅黑樹,當長度小於6時,從紅黑樹轉化為連結串列。這樣做的目的是能提高HashMap
的效能,因為紅黑樹的查詢元素的時間複雜度遠小於連結串列。HashTable
:陣列+連結串列TreeMap
:紅黑樹
哪些集合類是執行緒安全的? ***
Vector
:相當於有同步機制的ArrayList
Stack
:棧HashTable
enumeration
:列舉
迭代器 Iterator 是什麼 *
Iterator
是 Java 迭代器最簡單的實現,它不是一個集合,它是一種用於訪問集合的方法,Iterator
介面提供遍歷任何Collection
的介面。
Java集合的快速失敗機制 “fail-fast”和安全失敗機制“fail-safe”是什麼? ***
-
快速失敗
Java的快速失敗機制是Java集合框架中的一種錯誤檢測機制,當多個執行緒同時對集合中的內容進行修改時可能就會丟擲
ConcurrentModificationException
異常。其實不僅僅是在多執行緒狀態下,在單執行緒中用增強for
迴圈中一邊遍歷集合一邊修改集合的元素也會丟擲ConcurrentModificationException
異常。看下面程式碼public class Main{ public static void main(String[] args) { List<Integer> list = new ArrayList<>(); for(Integer i : list){ list.remove(i); //執行時丟擲ConcurrentModificationException異常 } } }
正確的做法是用迭代器的
remove()
方法,便可正常執行。public class Main{ public static void main(String[] args) { List<Integer> list = new ArrayList<>(); Iterator<Integer> it = list.iterator(); while(it.hasNext()){ it.remove(); } } }
造成這種情況的原因是什麼?細心的同學可能已經發現兩次呼叫的
remove()
方法不同,一個帶引數據,一個不帶引數,這個後面再說,經過檢視ArrayList原始碼,找到了丟擲異常的程式碼final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
從上面程式碼中可以看到如果
modCount
和expectedModCount
這兩個變數不相等就會丟擲ConcurrentModificationException
異常。那這兩個變數又是什麼呢?繼續看原始碼protected transient int modCount = 0; //在AbstractList中定義的變數
int expectedModCount = modCount;//在ArrayList中的內部類Itr中定義的變數
從上面程式碼可以看到,
modCount
初始值為0,而expectedModCount
初始值等於modCount
。也就是說在遍歷的時候直接呼叫集合的remove()
方法會導致modCount
不等於expectedModCount
進而丟擲ConcurrentModificationException
異常,而使用迭代器的remove()
方法則不會出現這種問題。那麼只能在看看remove()
方法的原始碼找找原因了public E remove(int index) { rangeCheck(index); modCount++; 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; }
從上面程式碼中可以看到只有
modCount++
了,而expectedModCount
沒有操作,當每一次迭代時,迭代器會比較expectedModCount
和modCount
的值是否相等,所以在呼叫remove()
方法後,modCount
不等於expectedModCount
了,這時就了報ConcurrentModificationException
異常。但用迭代器中remove()
的方法為什麼不拋異常呢?原來迭代器呼叫的remove()
方法和上面的remove()
方法不是同一個!迭代器呼叫的remove()
方法長這樣:public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; //這行程式碼保證了expectedModCount和modCount是相等的 } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
從上面程式碼可以看到
expectedModCount = modCount
,所以迭代器的remove()
方法保證了expectedModCount
和modCount
是相等的,進而保證了在增強for
迴圈中修改集合內容不會報ConcurrentModificationException
異常。上面介紹的只是單執行緒的情況,用迭代器呼叫
remove()
方法即可正常執行,但如果是多執行緒會怎麼樣呢?答案是在多執行緒的情況下即使用了迭代器呼叫
remove()
方法,還是會報ConcurrentModificationException
異常。這又是為什麼呢?還是要從expectedModCount
和modCount
這兩個變數入手分析,剛剛說了modCount
在AbstractList
類中定義,而expectedModCount
在ArrayList
內部類中定義,所以modCount
是個共享變數而expectedModCount
是屬於執行緒各自的。簡單說,執行緒1更新了modCount
和屬於自己的expectedModCount
,而線上程2看來只有modCount
更新了,expectedModCount
並未更新,所以會丟擲ConcurrentModificationException
異常。 -
安全失敗
採用安全失敗機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷。所以在遍歷過程中對原集合所作的修改並不能被迭代器檢測到,所以不會丟擲
ConcurrentModificationException
異常。缺點是迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發生了修改,迭代器是無法訪問到修改後的內容。java.util.concurrent
包下的容器都是安全失敗,可以在多執行緒下併發使用。
如何邊遍歷邊移除 Collection 中的元素? ***
從上文“快速失敗機制”可知在遍歷集合時如果直接呼叫remove()
方法會丟擲ConcurrentModificationException
異常,所以使用迭代器中呼叫remove()
方法。
Array 和 ArrayList 有何區別? ***
Array
可以包含基本型別和物件型別,ArrayList
只能包含物件型別。Array
大小是固定的,ArrayList
的大小是動態變化的。(ArrayList
的擴容是個常見面試題)- 相比於
Array
,ArrayList
有著更多的內建方法,如addAll()
,removeAll()
。 - 對於基本型別資料,
ArrayList
使用自動裝箱來減少編碼工作量;而當處理固定大小的基本資料型別的時候,這種方式相對比較慢,這時候應該使用Array
。
comparable 和 comparator的區別? **
comparable
介面出自java.lang
包,可以理解為一個內比較器,因為實現了Comparable
介面的類可以和自己比較,要和其他實現了Comparable
介面類比較,可以使用compareTo(Object obj)
方法。compareTo
方法的返回值是int
,有三種情況:- 返回正整數(比較者大於被比較者)
- 返回0(比較者等於被比較者)
- 返回負整數(比較者小於被比較者)
comparator
介面出自java.util
包,它有一個compare(Object obj1, Object obj2)
方法用來排序,返回值同樣是int
,有三種情況,和compareTo
類似。
它們之間的區別:很多包裝類都實現了comparable
介面,像Integer
、String
等,所以直接呼叫Collections.sort()
直接可以使用。如果對類裡面自帶的自然排序不滿意,而又不能修改其原始碼的情況下,使用Comparator
就比較合適。此外使用Comparator
可以避免新增額外的程式碼與我們的目標類耦合,同時可以定義多種排序規則,這一點是Comparable
介面沒法做到的,從靈活性和擴充套件性講Comparator更優,故在面對自定義排序的需求時,可以優先考慮使用Comparator
介面。
Collection 和 Collections 有什麼區別? **
Collection
是一個集合介面。它提供了對集合物件進行基本操作的通用介面方法。Collections
是一個包裝類。它包含有各種有關集合操作的靜態多型方法,例如常用的sort()
方法。此類不能例項化,就像一個工具類,服務於Java的Collection
框架。
List集合
遍歷一個 List 有哪些不同的方式? **
先說一下常見的元素在記憶體中的儲存方式,主要有兩種:
- 順序儲存(Random Access):相鄰的資料元素在記憶體中的位置也是相鄰的,可以根據元素的位置(如
ArrayList
中的下表)讀取元素。 - 鏈式儲存(Sequential Access):每個資料元素包含它下一個元素的記憶體地址,在記憶體中不要求相鄰。例如
LinkedList
。
主要的遍歷方式主要有三種:
for
迴圈遍歷:遍歷者自己在集合外部維護一個計數器,依次讀取每一個位置的元素。Iterator
遍歷:基於順序儲存集合的Iterator
可以直接按位置訪問資料。基於鏈式儲存集合的Iterator
,需要儲存當前遍歷的位置,然後根據當前位置來向前或者向後移動指標。foreach
遍歷:foreach
內部也是採用了Iterator
的方式實現,但使用時不需要顯示地宣告Iterator
。
那麼對於以上三種遍歷方式應該如何選取呢?
在Java集合框架中,提供了一個RandomAccess
介面,該介面沒有方法,只是一個標記。通常用來標記List
的實現是否支援RandomAccess
。所以在遍歷時,可以先判斷是否支援RandomAccess
(list instanceof RandomAccess
),如果支援可用 for
迴圈遍歷,否則建議用Iterator
或 foreach
遍歷。
ArrayList的擴容機制 ***
先說下結論,一般面試時需要記住,
ArrayList
的初始容量為10,擴容時對是舊的容量值加上舊的容量數值進行右移一位(位運算,相當於除以2,位運算的效率更高),所以每次擴容都是舊的容量的1.5倍。
具體的實現大家可檢視下ArrayList
的原始碼。
ArrayList 和 LinkedList 的區別是什麼? ***
- 是否執行緒安全:
ArrayList
和LinkedList
都是不保證執行緒安全的 - 底層實現:
ArrayList
的底層實現是陣列,LinkedList
的底層是雙向連結串列。 - 記憶體佔用:
ArrayList
會存在一定的空間浪費,因為每次擴容都是之前的1.5倍,而LinkedList
中的每個元素要存放直接後繼和直接前驅以及資料,所以對於每個元素的儲存都要比ArrayList
花費更多的空間。 - 應用場景:
ArrayList
的底層資料結構是陣列,所以在插入和刪除元素時的時間複雜度都會收到位置的影響,平均時間複雜度為o(n),在讀取元素的時候可以根據下標直接查詢到元素,不受位置的影響,平均時間複雜度為o(1),所以ArrayList
更加適用於多讀,少增刪的場景。LinkedList
的底層資料結構是雙向連結串列,所以插入和刪除元素不受位置的影響,平均時間複雜度為o(1),如果是在指定位置插入則是o(n),因為在插入之前需要先找到該位置,讀取元素的平均時間複雜度為o(n)。所以LinkedList
更加適用於多增刪,少讀寫的場景。
ArrayList 和 Vector 的區別是什麼? ***
-
相同點
- 都實現了
List
介面 - 底層資料結構都是陣列
- 都實現了
-
不同點
- 執行緒安全:
Vector
使用了Synchronized
來實現執行緒同步,所以是執行緒安全的,而ArrayList
是執行緒不安全的。 - 效能:由於
Vector
使用了Synchronized
進行加鎖,所以效能不如ArrayList
。 - 擴容:
ArrayList
和Vector
都會根據需要動態的調整容量,但是ArrayList
每次擴容為舊容量的1.5倍,而Vector
每次擴容為舊容量的2倍。
- 執行緒安全:
簡述 ArrayList、Vector、LinkedList 的儲存效能和特性? ***
ArrayList
底層資料結構為陣列,對元素的讀取速度快,而增刪資料慢,執行緒不安全。LinkedList
底層為雙向連結串列,對元素的增刪資料快,讀取慢,執行緒不安全。Vector
的底層資料結構為陣列,用Synchronized
來保證執行緒安全,效能較差,但執行緒安全。
Set集合
說一下 HashSet 的實現原理 ***
HashSet
的底層是HashMap
,預設建構函式是構建一個初始容量為16,負載因子為0.75 的HashMap
。HashSet
的值存放於HashMap
的key
上,HashMap
的value
統一為PRESENT
。
HashSet如何檢查重複?(HashSet是如何保證資料不可重複的?) ***
這裡面涉及到了HasCode()
和equals()
兩個方法。
-
equals()
先看下
String
類中重寫的equals
方法。public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
從原始碼中可以看到:
equals
方法首先比較的是記憶體地址,如果記憶體地址相同,直接返回true
;如果記憶體地址不同,再比較物件的型別,型別不同直接返回false
;型別相同,再比較值是否相同;值相同返回true
,值不同返回false
。總結一下,equals
會比較記憶體地址、物件型別、以及值,記憶體地址相同,equals
一定返回true
;物件型別和值相同,equals
方法一定返回true
。- 如果沒有重寫
equals
方法,那麼equals
和==
的作用相同,比較的是物件的地址值。
-
hashCode
hashCode
方法返回物件的雜湊碼,返回值是int
型別的雜湊碼。雜湊碼的作用是確定該物件在雜湊表中的索引位置。關於
hashCode
有一些約定:- 兩個物件相等,則
hashCode
一定相同。 - 兩個物件有相同的
hashCode
值,它們不一定相等。 hashCode()
方法預設是對堆上的物件產生獨特值,如果沒有重寫hashCode()
方法,則該類的兩個物件的hashCode
值肯定不同
- 兩個物件相等,則
介紹完equals()方法和hashCode()方法,繼續說下HashSet是如何檢查重複的。
HashSet
的特點是儲存元素時無序且唯一,在向HashSet
中新增物件時,首相會計算物件的HashCode
值來確定物件的儲存位置,如果該位置沒有其他物件,直接將該物件新增到該位置;如果該儲存位置有儲存其他物件(新新增的物件和該儲存位置的物件的HashCode
值相同),呼叫equals
方法判斷兩個物件是否相同,如果相同,則新增物件失敗,如果不相同,則會將該物件重新雜湊到其他位置。
HashSet與HashMap的區別 ***
HashMap | HashSet |
---|---|
實現了Map 介面 |
實現了Set 介面 |
儲存鍵值對 | 儲存物件 |
key 唯一,value 不唯一 |
儲存物件唯一 |
HashMap 使用鍵(Key )計算Hashcode |
HashSet 使用成員物件來計算hashcode 值 |
速度相對較快 | 速度相對較慢 |
Map集合
HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底層實現 ***
- JDK1.7的底層資料結構(陣列+連結串列)
- JDK1.8的底層資料結構(陣列+連結串列)
-
JDK1.7的Hash函式
static final int hash(int h){ h ^= (h >>> 20) ^ (h >>>12); return h^(h >>> 7) ^ (h >>> 4); }
-
JDK1.8的Hash函式
static final int hash(Onject key){ int h; return (key == null) ? 0 : (h = key.hashCode())^(h >>> 16); }
JDK1.8的函式經過了一次異或一次位運算一共兩次擾動,而JDK1.7經過了四次位運算五次異或一共九次擾動。這裡簡單解釋下JDK1.8的hash函式,面試經常問這個,兩次擾動分別是
key.hashCode()
與key.hashCode()
右移16位進行異或。這樣做的目的是,高16位不變,低16位與高16位進行異或操作,進而減少碰撞的發生,高低Bit都參與到Hash的計算。如何不進行擾動處理,因為hash值有32位,直接對陣列的長度求餘,起作用只是hash值的幾個低位。
HashMap在JDK1.7和JDK1.8中有哪些不同點:
JDK1.7 | JDK1.8 | JDK1.8的優勢 | |
---|---|---|---|
底層結構 | 陣列+連結串列 | 陣列+連結串列/紅黑樹(連結串列大於8) | 避免單條連結串列過長而影響查詢效率,提高查詢效率 |
hash值計算方式 | 9次擾動 = 4次位運算 + 5次異或運算 | 2次擾動 = 1次位運算 + 1次異或運算 | 可以均勻地把之前的衝突的節點分散到新的桶(具體細節見下面擴容部分) |
插入資料方式 | 頭插法(先講原位置的資料移到後1位,再插入資料到該位置) | 尾插法(直接插入到連結串列尾部/紅黑樹) | 解決多執行緒造成死迴圈地問題 |
擴容後儲存位置的計算方式 | 重新進行hash計算 | 原位置或原位置+舊容量 | 省去了重新計算hash值的時間 |
HashMap 的長度為什麼是2的冪次方 ***
因為HashMap
是通過key
的hash值來確定儲存的位置,但Hash值的範圍是-2147483648到2147483647,不可能建立一個這麼大的陣列來覆蓋所有hash值。所以在計算完hash值後會對陣列的長度進行取餘操作,如果陣列的長度是2的冪次方,(length - 1)&hash
等同於hash%length
,可以用(length - 1)&hash
這種位運算來代替%取餘的操作進而提高效能。
HashMap的put方法的具體流程? **
HashMap的主要流程可以看下面這個流程圖,邏輯非常清晰。
HashMap的擴容操作是怎麼實現的? ***
-
初始值為16,負載因子為0.75,閾值為負載因子*容量
-
resize()
方法是在hashmap
中的鍵值對大於閥值時或者初始化時,就呼叫resize()
方法進行擴容。 -
每次擴容,容量都是之前的兩倍
-
擴容時有個判斷
e.hash & oldCap
是否為零,也就是相當於hash值對陣列長度的取餘操作,若等於0,則位置不變,若等於1,位置變為原位置加舊容量。原始碼如下:
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; //沒有超過最大值就變為原來的2倍 } else if (oldThr > 0) newCap = oldThr; else { 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 { Node<K,V> loHead = null, loTail = null;//loHead,loTail 代表擴容後在原位置 Node<K,V> hiHead = null, hiTail = null;//hiHead,hiTail 代表擴容後在原位置+舊容量 Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { //判斷是否為零,為零賦值到loHead,不為零賦值到hiHead 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; //loHead放在原位置 } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; //hiHead放在原位置+舊容量 } } } } } return newTab; }
HashMap預設載入因子為什麼選擇0.75?
這個主要是考慮空間利用率和查詢成本的一個折中。如果載入因子過高,空間利用率提高,但是會使得雜湊衝突的概率增加;如果載入因子過低,會頻繁擴容,雜湊衝突概率降低,但是會使得空間利用率變低。具體為什麼是0.75,不是0.74或0.76,這是一個基於數學分析(泊松分佈)和行業規定一起得到的一個結論。
為什麼要將連結串列中轉紅黑樹的閾值設為8?為什麼不一開始直接使用紅黑樹?
可能有很多人會問,既然紅黑樹效能這麼好,為什麼不一開始直接使用紅黑樹,而是先用連結串列,連結串列長度大於8時,才轉換為紅紅黑樹。
- 因為紅黑樹的節點所佔的空間是普通連結串列節點的兩倍,但查詢的時間複雜度低,所以只有當節點特別多時,紅黑樹的優點才能體現出來。至於為什麼是8,是通過資料分析統計出來的一個結果,連結串列長度到達8的概率是很低的,綜合連結串列和紅黑樹的效能優缺點考慮將大於8的連結串列轉化為紅黑樹。
- 連結串列轉化為紅黑樹除了連結串列長度大於8,還要
HashMap
中的陣列長度大於64。也就是如果HashMap
長度小於64,連結串列長度大於8是不會轉化為紅黑樹的,而是直接擴容。
HashMap是怎麼解決雜湊衝突的? ***
雜湊衝突:hashMap
在儲存元素時會先計算key
的hash值來確定儲存位置,因為key
的hash值計算最後有個對陣列長度取餘的操作,所以即使不同的key
也可能計算出相同的hash值,這樣就引起了hash衝突。hashMap
的底層結構中的連結串列/紅黑樹就是用來解決這個問題的。
HashMap
中的雜湊衝突解決方式可以主要從三方面考慮(以JDK1.8為背景)
-
拉鍊法
HasMap
中的資料結構為陣列+連結串列/紅黑樹,當不同的key
計算出的hash值相同時,就用連結串列的形式將Node結點(衝突的key
及key
對應的value
)掛在陣列後面。 -
hash函式
key
的hash值經過兩次擾動,key
的hashCode
值與key
的hashCode
值的右移16位進行異或,然後對陣列的長度取餘(實際為了提高效能用的是位運算,但目的和取餘一樣),這樣做可以讓hashCode
取值出的高位也參與運算,進一步降低hash衝突的概率,使得資料分佈更平均。 -
紅黑樹
在拉鍊法中,如果hash衝突特別嚴重,則會導致陣列上掛的連結串列長度過長,效能變差,因此在連結串列長度大於8時,將連結串列轉化為紅黑樹,可以提高遍歷連結串列的速度。
HashMap為什麼不直接使用hashCode()處理後的雜湊值直接作為table的下標? ***
hashCode()
處理後的雜湊值範圍太大,不可能在記憶體建立這麼大的陣列。
能否使用任何類作為 Map 的 key? ***
可以,但要注意以下兩點:
- 如果類重寫了
equals()
方法,也應該重寫hashCode()
方法。 - 最好定義
key
類是不可變的,這樣key
對應的hashCode()
值可以被快取起來,效能更好,這也是為什麼String
特別適合作為HashMap
的key
。
為什麼HashMap中String、Integer這樣的包裝類適合作為Key? ***
- 這些包裝類都是
final
修飾,是不可變性的, 保證了key
的不可更改性,不會出現放入和獲取時雜湊值不同的情況。 - 它們內部已經重寫過
hashcode()
,equal()
等方法。
如果使用Object作為HashMap的Key,應該怎麼辦呢? **
- 重寫
hashCode()
方法,因為需要計算hash值確定儲存位置 - 重寫
equals()
方法,因為需要保證key
的唯一性。
HashMap 多執行緒導致死迴圈問題 ***
由於JDK1.7的
hashMap
遇到hash衝突採用的是頭插法,在多執行緒情況下會存在死迴圈問題,但JDK1.8已經改成了尾插法,不存在這個問題了。但需要注意的是JDK1.8中的HashMap
仍然是不安全的,在多執行緒情況下使用仍然會出現執行緒安全問題。基本上面試時說到這裡既可以了,具體流程用口述是很難說清的,感興趣的可以看這篇文章。HASHMAP的死迴圈
ConcurrentHashMap 底層具體實現知道嗎? **
-
JDK1.7
在JDK1.7中,
ConcurrentHashMap
採用Segment
陣列 +HashEntry
陣列的方式進行實現。Segment
實現了ReentrantLock
,所以Segment
有鎖的性質,HashEntry
用於儲存鍵值對。一個ConcurrentHashMap
包含著一個Segment
陣列,一個Segment
包含著一個HashEntry
陣列,HashEntry
是一個連結串列結構,如果要獲取HashEntry
中的元素,要先獲得Segment
的鎖。
-
JDK1.8
在JDK1.8中,不在是
Segment
+HashEntry
的結構了,而是和HashMap
類似的結構,Node陣列+連結串列/紅黑樹,採用CAS
+synchronized
來保證執行緒安全。當連結串列長度大於8,連結串列轉化為紅黑樹。在JDK1.8中synchronized
只鎖連結串列或紅黑樹的頭節點,是一種相比於segment
更為細粒度的鎖,鎖的競爭變小,所以效率更高。
總結一下:
- JDK1.7底層是
ReentrantLock
+Segment
+HashEntry
,JDK1.8底層是synchronized
+CAS
+連結串列/紅黑樹 - JDK1.7採用的是分段鎖,同時鎖住幾個
HashEntry
,JDK1.8鎖的是Node節點,只要沒有發生雜湊衝突,就不會產生鎖的競爭。所以JDK1.8相比於JDK1.7提供了一種粒度更小的鎖,減少了鎖的競爭,提高了ConcurrentHashMap
的併發能力。
HashTable的底層實現知道嗎?
HashTable
的底層資料結構是陣列+連結串列,連結串列主要是為了解決雜湊衝突,並且整個陣列都是synchronized
修飾的,所以HashTable
是執行緒安全的,但鎖的粒度太大,鎖的競爭非常激烈,效率很低。
HashMap、ConcurrentHashMap及Hashtable 的區別 ***
HashMap(JDK1.8) | ConcurrentHashMap(JDK1.8) | Hashtable | |
---|---|---|---|
底層實現 | 陣列+連結串列/紅黑樹 | 陣列+連結串列/紅黑樹 | 陣列+連結串列 |
執行緒安全 | 不安全 | 安全(Synchronized 修飾Node節點) |
安全(Synchronized 修飾整個表) |
效率 | 高 | 較高 | 低 |
擴容 | 初始16,每次擴容成2n | 初始16,每次擴容成2n | 初始11,每次擴容成2n+1 |
是否支援Null key和Null Value | 可以有一個Null key,Null Value多個 | 不支援 | 不支援 |
Java集合的常用方法 **
這些常用方法是需要背下來的,雖然面試用不上,但是筆試或者面試寫演算法題時會經常用到。
Collection常用方法
方法 | |
---|---|
booean add(E e) |
在集合末尾新增元素 |
boolean remove(Object o) |
若本類集中有值與o的值相等的元素,移除該元素並返回true |
void clear() |
清除本類中所有元素 |
boolean contains(Object o) |
判斷集合中是否包含該元素 |
boolean isEmpty() |
判斷集合是否為空 |
int size() |
返回集合中元素的個數 |
boolean addAll(Collection c) |
將一個集合中c中的所有元素新增到另一個集合中 |
Object[] toArray() |
返回一個包含本集所有元素的陣列,陣列型別為Object[] |
`boolean equals(Object c)`` | 判斷元素是否相等 |
int hashCode() |
返回元素的hash值 |
List特有方法
方法 | |
---|---|
void add(int index,Object obj) |
在指定位置新增元素 |
Object remove(int index) |
刪除指定元素並返回 |
Object set(int index,Object obj) |
把指定索引位置的元素更改為指定值並返回修改前的值 |
int indexOf(Object o) |
返回指定元素在集合中第一次出現的索引 |
Object get(int index) |
返回指定位置的元素 |
List subList(int fromIndex,int toIndex) |
擷取集合(左閉右開) |
LinkedList特有方法
方法 | |
---|---|
addFirst() |
在頭部新增元素 |
addLast() |
在尾部新增元素 |
removeFirst() |
在頭部刪除元素 |
removeLat() |
在尾部刪除元素 |
getFirst() |
獲取頭部元素 |
getLast() |
獲取尾部元素 |
Map
方法 | |
---|---|
void clear() |
清除集合內的元素 |
boolean containsKey(Object key) |
查詢Map中是否包含指定key,如果包含則返回true |
Set entrySet() |
返回Map中所包含的鍵值對所組成的Set集合,每個集合元素都是Map.Entry的物件 |
Object get(Object key) |
返回key指定的value,若Map中不包含key返回null |
boolean isEmpty() |
查詢Map是否為空,若為空返回true |
Set keySet() |
返回Map中所有key所組成的集合 |
Object put(Object key,Object value) |
新增一個鍵值對,如果已有一個相同的key,則新的鍵值對會覆蓋舊的鍵值對,返回值為覆蓋前的value值,否則為null |
void putAll(Map m) |
將制定Map中的鍵值對複製到Map中 |
Object remove(Object key) |
刪除指定key所對應的鍵值對,返回所關聯的value,如果key不存在返回null |
int size() |
返回Map裡面的鍵值對的個數 |
Collection values() |
返回Map裡所有values所組成的Collection |
boolean containsValue ( Object value) |
判斷映像中是否存在值 value |
Stack
方法 | |
---|---|
boolean empty() |
測試堆疊是否為空。 |
E peek() |
檢視堆疊頂部的物件,但不從堆疊中移除它。 |
E pop() |
移除堆疊頂部的物件,並作為此函式的值返回該物件。 |
E push(E item) |
把項壓入堆疊頂部。 |
int search(Object o) |
返回物件在堆疊中的位置,以 1 為基數。 |
Queue
方法 | |
---|---|
boolean add(E e) |
將指定元素插入到佇列的尾部(佇列滿了話,會丟擲異常) |
boolean offer(E e) |
將指定元素插入此佇列的尾部(佇列滿了話,會返回false) |
E remove() |
返回取佇列頭部的元素,並刪除該元素(如果佇列為空,則丟擲異常) |
E poll() |
返回佇列頭部的元素,並刪除該元素(如果佇列為空,則返回null) |
E element() |
返回佇列頭部的元素,不刪除該元素(如果佇列為空,則丟擲異常) |
E peek() |
返回佇列頭部的元素,不刪除該元素(如果佇列為空,則返回null) |