主題 | 鏈接 |
---|---|
Java基礎知識 | 面試題 |
Java集合容器 | 面試題 |
Java併發編程 | 面試題 |
Java底層知識 | 面試題 |
Java常用框架 | 面試題 |
計算機網絡 | 面試題 |
數據庫 | 面試題 |
RabbitMQ | 面試題 |
Redis | 面試題 |
文章目錄
- 面試套路
- Java常用集合類有哪些?
- HashMap與HashTable的區別?
- JDK1.8以後HashMap的put方法的具體流程?
- ArrayList、LinkList、Vetor的區別?
- HashMap、HashTable的區別?
- HashMap 和 ConcurrentHashMap 的區別?
- 不同版本JDK的HashMap的實現的區別以及原因
- Collection和Collections的區別
- Arrays.asList獲得的List使用時需要注意什麼
- Fail-fast和Fail-safe
- CopyOnWriteArrayList、ConcurrentSkipListMap
- Hashmap 什麼時候進行擴容呢?
- HashMap的默認初始化長度是多少?爲什麼?
- 哈希表如何解決Hash衝突?
- 爲什麼 HashMap 中 String、Integer 這樣的包裝類適合作爲 key 鍵
- HashMap 中的 key若爲 Object類型, 則需實現哪些方法?
- List、Map、Set 三個接口,存取元素時,各有什麼特點?
- Set 裏的元素是不能重複的,那麼用什麼方法來區分重複與否呢? 是用 == 還是equals()? 它們有何區別?
- Java 集合類框架的最佳實踐有哪些?
面試套路
- Java中常用的集合類哪些?
- 你最常用的是哪些?
- 說一說實現原理
- 怎麼擴容、怎麼處理hash衝突、不同jdk版本的區別,有沒有線程安全的集合類
Java常用集合類有哪些?
- Collection接口的子接口包括:Set接口和List接口
- Map接口的實現類主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
- Set接口的實現類主要有:HashSet、TreeSet、LinkedHashSet等
- List接口的實現類主要有:ArrayList、LinkedList、Stack以及Vector等
HashMap與HashTable的區別?
- HashMap沒有考慮同步,是線程不安全的;HashTable使用了synchronized關鍵字,是線程安全的;
- HashMap允許K/V都爲null;後者K/V都不允許爲null;
- HashMap繼承自AbstractMap類;而Hashtable繼承自Dictionary類;
JDK1.8以後HashMap的put方法的具體流程?
當 HashMap 中有大量的元素都存放到同一個桶中時,這個桶下有一條長長的鏈表,這個時候 HashMap 就相當於一個單鏈表,假如單鏈表有 n 個元素,遍歷的時間複雜度就是 O(n),完全失去了它的優勢。
針對這種情況,JDK 1.8 中引入了 紅黑樹(查找時間複雜度爲 O(logn))來優化這個問題。
ArrayList、LinkList、Vetor的區別?
List主要有ArrayList、LinkedList與Vector幾種實現。
- ArrayList
是一個可改變大小的數組.其大小可以動態地增長.內部的元素可以直接通過get與set方法進行訪問,因爲ArrayList本質上就是一個數組. - LinkedList
是一個雙鏈表,在添加和刪除元素時具有比ArrayList更好的性能.但在get與set方面弱於ArrayList.這些都是指數據量很大或者操作很頻繁的情況下的對比 - Vector
和ArrayList類似,但屬於強同步類。如果你的程序本身是線程安全的(thread-safe,沒有在多個線程之間共享同一個集合/對象),那麼使用ArrayList是更好的選擇。 - Vector和ArrayList在更多元素添加進來時會請求更大的空間。Vector每次請求其大小的雙倍空間,而ArrayList每次對size增長50%.而 LinkedList 還實現了 Queue 接口,該接口比List提供了更多的方法,包offer(),peek(),poll()等. 注意:默認情況下ArrayList的初始容量非常小,所以如果可以預估數據量的話,分配一個較大的初始值屬於最佳實踐,這樣可以減少調整大小的開銷。
HashMap、HashTable的區別?
- 線程安全: HashTable 中的方法是同步的,而HashMap中的方法在默認情況下是非同步的。在多線程併發的環境下,可以直接使用HashTable,但是要使用HashMap的話就要自己增加同步處理了。
- 繼承關係: HashTable是基於陳舊的Dictionary類繼承來的。HashMap繼承的抽象類AbstractMap實現了Map接口。
- 允不允許null值:HashTable中,key和value都不允許出現null值,否則會拋出NullPointerException異常。HashMap中,null可以作爲鍵,這樣的鍵只有一個;可以有一個或多個鍵所對應的值爲null。
- 默認初始容量和擴容機制: HashTable中的hash數組初始大小是11,增加的方式是old*2+1。HashMap中hash數組的默認大小是16,而且一定是2的指數。
- 哈希值的使用不同 : HashTable直接使用對象的hashCode。 HashMap重新計算hash值。
- 遍歷方式的內部實現上不同 : Hashtable、HashMap都使用了 Iterator。而由於歷史原因,Hashtable還使用了Enumeration的方式 。 HashMap 實現Iterator,支持fast-fail,Hashtable的 Iterator 遍歷支持fast-fail,用 Enumeration不支持 fast-fail
HashMap 和 ConcurrentHashMap 的區別?
- ConcurrentHashMap和HashMap的實現方式不一樣,雖然都是使用桶數組實現的,但是還是有區別,ConcurrentHashMap對桶數組進行了分段,而HashMap並沒有。
- ConcurrentHashMap在每一個分段上都用鎖進行了保護。HashMap沒有鎖機制。所以,前者線程安全的,後者不是線程安全的。
PS:以上區別基於jdk1.8以前的版本。
不同版本JDK的HashMap的實現的區別以及原因
(1)鏈表元素的插入方式不一樣
- JDK1.7用的是頭插法,而JDK1.8及之後使用的都是尾插法
- 因爲JDK1.7是用單鏈表進行的縱向延伸,當採用頭插法時會容易出現逆序且環形鏈表死循環問題。
- 在JDK1.8之後是因爲加入了紅黑樹使用尾插法,能夠避免出現逆序且鏈表死循環的問題。
(2)擴容後數據存儲位置的計算方式不一樣
- 在JDK1.7的時候是直接用hash值和需要擴容的二進制數進行&(這裏就是爲什麼擴容的時候爲啥一定必須是2的多少次冪的原因所在,因爲如果只有2的n次冪的情況時最後一位二進制數才一定是1,這樣能最大程度減少hash碰撞)(hash值 & length-1)
- 在JDK1.8的時候直接用了JDK1.7的時候計算的規律,也就是擴容前的原始位置+擴容的大小值=JDK1.8的計算方式,而不再是JDK1.7的那種異或的方法。但是這種方式就相當於只需要判斷Hash值的新增參與運算的位是0還是1就直接迅速計算出了擴容後的儲存方式。
(3)數據結構不一樣
- JDK1.7的時候使用的是數組+ 單鏈表的數據結構。
- 在JDK1.8及之後時,使用的是數組+鏈表+紅黑樹的數據結構(當鏈表的深度達到8的時候,也就是默認閾值,就會自動擴容把鏈表轉成紅黑樹的數據結構來把時間複雜度從O(n)變成O(logN)提高了效率)
(4)爲什麼在JDK1.8中HashMap把鏈表轉化爲紅黑樹的閾值是8?
- 由於treenodes的大小大約是常規節點的兩倍,因此我們僅在容器包含足夠的節點以保證使用時才使用它們,當它們變得太小(由於移除或調整大小)時,它們會被轉換回普通的node節點,容器中節點分佈在hash桶中的頻率遵循泊松分佈,桶的長度超過8的概率非常非常小。所以作者應該是根據概率統計而選擇了8作爲閥值(Java註釋中的解釋)
Collection和Collections的區別
- Collection:是集合類的上層接口。本身是一個Interface,裏面包含了一些集合的基本操作。Collection接口是Set接口和List接口的父接口
- Collections:Collections是一個集合框架的幫助類,裏面包含一些對集合的排序,搜索以及序列化的操作。
Arrays.asList獲得的List使用時需要注意什麼
Arrays.asList得到的List它的長度是不能改變的。當你向這個List添加或刪除一個元素時(例如 list.add(“d”);)程序就會拋出異常(java.lang.UnsupportedOperationException)。
public static List asList(T… a) {
return new ArrayList<>(a);
}
當你看到這段代碼時可能覺得沒啥問題啊,不就是返回了一個ArrayList對象嗎?問題就出在這裏。這個ArrayList不是java.util包下的,而是java.util.Arrays.ArrayList,顯然它是Arrays類自己定義的一個內部類!這個內部類沒有實現add()、remove()方法,而是直接使用它的父類AbstractList的相應方法。而AbstractList中的add()和remove()是直接拋出java.lang.UnsupportedOperationException異常的!
Fail-fast和Fail-safe
- 線程不安全的類,併發情況下可能會出現快速失敗;線程安全的類,可能會出現安全失敗
- 一個線程在遍歷,另一個線程在添加、刪除或修改,就會出現併發修改的問題
- 當遍歷時檢測到併發修改,就會拋出異常:concurrentmodificationException,這就是快速失敗
- ArrayList.iterator()返回一個迭代器對象,其中使用一個int類型的expectedModCount記錄狀態,當發生添加、刪除、修改操作時會更改這個值,當遍歷時調用next()會檢查這個值跟開始遍歷時是否一致,發現expectedModCount發生了變化,就意味着有併發修改,這時候就拋出異常iterator.remove()方法沒有進行modCount值的檢查,並且手動把expectedModCount值修改成了modCount值,這又保證了下一次迭代的正確。
- fail-safe是一個概念,併發容器的併發修改不會拋出異常,這和其實現有關。併發容器的iterate方法返回的iterator對象,內部都是保存了該集合對象的一個快照副本,並且沒有modCount等數值做檢查。這也造成了併發容器的iterator讀取的數據是某個時間點的快照版本。你可以併發讀取,不會拋出異常,但是不保證你遍歷讀取的值和當前集合對象的狀態是一致的!這就是安全失敗的含義。
CopyOnWriteArrayList、ConcurrentSkipListMap
- ConcurrentSkipListMap和ConcurrentSkipListSet是TreeMap和TreeSet的有序容器的併發版本
- ConcurrentSkipListMap的底層是通過跳錶來實現的。跳錶(Skiplist)是一個鏈表,但是通過使用“跳躍式”查找的方式使得插入、讀取數據時複雜度變成了O(logn),跳錶以空間換時間,是基於鏈表實現的一種類似“二分”的算法。
- CopyOnWriteArrayList使用了一種叫寫時複製的方法,當有新元素添加到CopyOnWriteArrayList時,先從原有的數組中拷貝一份出來,然後在新的數組做寫操作,寫完之後,再將原來的數組引用指向到新數組,合適讀多寫少的場景
ConcurrentSkipListMap與CopyOnWriteArrayList
Hashmap 什麼時候進行擴容呢?
- 默認大小爲16、負載因子爲0.75,即超過12就擴容,容量擴大一倍
- 新建hashmap時設置初始大小,假如有1000個元素,不能設置1000,因爲元素數量爲750時就會自動擴容,要避免自動擴容,要讓元素數量不超過初始容量的0.75
- 擴容時會重新計算元素在數組中的位置,儘量避免擴容
- Hash的公式—> index = HashCode(Key) & (Length - 1)
- 因爲resize的賦值方式,也就是使用了單鏈表的頭插入方式,同一位置上新元素總會被放在鏈表的頭部位置,在舊數組中同一條Entry鏈上的元素,通過重新計算索引位置後,有可能被放到了新數組的不同位置上。會形成環形列表。
- 使用頭插會改變鏈表的上的順序,但是如果使用尾插,在擴容時會保持鏈表元素原本的順序,就不會出現鏈表成環的問題了。Java8在同樣的前提下並不會引起死循環,原因是擴容轉移後前後鏈表順序不變,保持之前節點的引用關係。
HashMap的默認初始化長度是多少?爲什麼?
- 默認是16,可以是其他2的冪
- hash函數可以將任意長度的輸入經過變化以後得到固定長度的輸出,如果兩個元素不相同,但是hash函數的值相同,這兩個元素就是一個碰撞
- 爲了減少hash值的碰撞,需要實現一個儘量均勻分佈的hash函數,在HashMap中index = key的hashcode值 & length-1
- 長度16或者其他2的冪時,length - 1的值是所有二進制位全爲1,這種情況下index的結果等同於hashcode後幾位的值,只要輸入的hashcode本身分佈均勻,hash算法的結果就是均勻的
- 所以HashMap的默認長度爲16,是爲了降低hash碰撞的機率