Java集合
剛剛經歷過秋招,看了大量的面經,順便將常見的Java集合常考知識點總結了一下,並根據被問到的頻率大體作了一個標註。一顆星表示知識點須要瞭解,被問到的頻率不高,面試時起碼能說個差很少。兩顆星表示被問到的頻率較高或對理解Java有着重要的做用,建議熟練掌握。三顆星表示被問到的頻率很是高,建議深刻理解並熟練掌握其相關知識,方便麪試時拓展(方便裝逼),給面試官留下個好印象。html
推薦閱讀:一文搞懂全部Java基礎知識面試題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
:shellHashSet
:底層基於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) |