42 道Java集合經典面試題,陪伴學習,共同優(yōu)秀
本篇文章是Java集合經典面試題。
1、Java中常用的集合有哪些?
Java集合框架為不同類型的集合定義了大量接口。
Java類庫中的集合:
- ArrayList,可以動態(tài)增長和縮減的一個索引序列。
- LinkedList,可以在任意位置高效插入和刪除的一個有序序列。
- ArrayDeque,實現(xiàn)為循環(huán)數組的一個雙端隊列。
- HashSet,沒有重復元素的一個無序集合。
- TreeSet,有序集。
- EnumSet,包含枚舉類型值的集合。
- LinkedHashSet,可以記住元素插入次序的集合。
- PriorityQueue,允許高效刪除最小元素的集合。
- HashMap,存儲鍵值對的數據結構。
- TreeMap,鍵有序的鍵值對集合。
- EnumMap,鍵是枚舉類型的鍵值對集合。
- LinkedHashMap,可以記住添加次序的鍵值對集合。
- WeakHashMap,這個集合中的鍵如果不在使用,就會被垃圾回收。
2、Collection 和 Collections 有什么區(qū)別?
(1)Collection是最基本的集合接口,Collection派生了兩個子接口list和set,分別定義了兩種不同的存儲方式。
(2)Collections是一個包裝類,它包含各種有關集合操作的靜態(tài)方法(對集合的搜索、排序、線程安全化等)。
此類不能實例化,就像一個工具類,服務于Collection框架。
3、為什么集合類沒有實現(xiàn) Cloneable 和 Serializable 接口?
集合類通常包含多個元素,這些元素的類型和數量可能會非常復雜。實現(xiàn)Cloneable和Serializable接口需要對這些元素的狀態(tài)進行精確的控制和管理,這可能會導致代碼變得復雜且容易出錯。
序列化和克隆操作可能會消耗大量的計算資源,尤其是在處理大型集合時。不實現(xiàn)這些接口可以避免不必要的性能開銷。
序列化和克隆操作可能會引發(fā)安全問題,因為它們允許對對象的深拷貝。如果不小心使用,可能會導致敏感信息的泄露或不當訪問。
4、數組和集合有什么本質區(qū)別?
數組的大小是固定的,一旦創(chuàng)建后就不能改變,而集合的大小是動態(tài)的,可以根據需要擴展和縮減。
數組可以存儲基本數據類型和引用數據類型,而集合只能存儲引用數據類型(對象)。
數組通常用于存儲同一類型的數據,而集合可以存儲不同類型的數據。
數組的長度是不可變的,而集合的長度是可變的,這意味著可以在運行時向集合中添加或刪除元素。
5、數組和集合如何選擇?
如果數據的大小是固定的,那么數組可能是一個更好的選擇,因為它提供了固定大小的存儲空間。相反,如果數據的大小可能會發(fā)生變化,那么集合可能更合適。
如果需要存儲基本數據類型,那么只能使用數組,如果需要存儲不同類型的數據,集合可能更適合。
數組在訪問速度上通常比集合更快,因為它們可以通過索引直接訪問元素。
集合提供了許多有用的方法,如add、remove、contains等,這些方法使得數據的操作更加方便。如果需要使用這些方法,那么集合可能是更好的選擇。
6、list與Set區(qū)別
(1)List簡介
實際上有兩種List:一種是基本的ArrayList,其優(yōu)點在于隨機訪問元素,另一種是LinkedList,它并不是為快速隨機訪問設計的,而是快速的插入或刪除。
- ArrayList:由數組實現(xiàn)的List。允許對元素進行快速隨機訪問,但是向List中間插入與移除元素的速度很慢。
- LinkedList :對順序訪問進行了優(yōu)化,向List中間插入與刪除的開銷并不大。隨機訪問則相對較慢。
還具有下列方 法:addFirst(), addLast(), getFirst(), getLast(), removeFirst() 和 removeLast(), 這些方法 (沒有在任何接口或基類中定義過)使得LinkedList可以當作堆棧、隊列和雙向隊列使用。
(2)Set簡介
Set具有與Collection完全一樣的接口,因此沒有任何額外的功能。實際上Set就是Collection,只是行為不同。這是繼承與多態(tài)思想的典型應用:表現(xiàn)不同的行為。Set不保存重復的元素(至于如何判斷元素相同則較為負責)
- Set : 存入Set的每個元素都必須是唯一的,因為Set不保存重復元素。加入Set的元素必須定義equals()方法以確保對象的唯一性。Set與Collection有完全一樣的接口。Set接口不保證維護元素的次序。
- HashSet:為快速查找設計的Set。存入HashSet的對象必須定義hashCode()。
- TreeSet:保存次序的Set, 底層為樹結構。使用它可以從Set中提取有序的序列。
(3)list與Set區(qū)別
List,Set都是繼承自Collection接口。
List特點:元素有放入順序,元素可重復 ,Set特點:元素無放入順序,元素不可重復,重復元素會覆蓋掉,(元素雖然無放入順序,但是元素在set中的位置是有該元素的HashCode決定的,其位置其實是固定的,加入Set 的Object必須定義equals()方法 ,另外list支持for循環(huán),也就是通過下標來遍歷,也可以用迭代器,但是set只能用迭代,因為他無序,無法用下標來取得想要的值。)
Set和List對比:
- Set:檢索元素效率低下,刪除和插入效率高,插入和刪除不會引起元素位置改變。
- List:和數組類似,List可以動態(tài)增長,查找元素效率高,插入刪除元素效率低,因為會引起其他元素位置改變。
7、HashMap 和 Hashtable 有什么區(qū)別?
- Hashtable不允許鍵或值為null,否則會拋出NullPointerException異常。而HashMap可以存儲key和value為null的元素;
- Hashtable繼承自Dictionary類,HashMap繼承自AbstractMap類并實現(xiàn)了Map接口;
- Hashtable在創(chuàng)建時必須指定容量大小,且默認大小為11。而HashMap可以在創(chuàng)建時不指定容量大小,系統(tǒng)會自動分配初始容量,并采用2倍擴容機制;
- 迭代器 Iterator 對 Hashtable 是安全的,而 Iterator 對 HashMap 不是安全的,因為迭代器被設計為工作于一個快照上,如果在迭代過程中其他線程修改了 HashMap,則會拋出并發(fā)修改異常;
- Hashtable是線程安全的,而HashMap是非線程安全的。Hashtable通過在每個方法前加上synchronized關鍵字來保證線程安全性,而HashMap則沒有實現(xiàn)這種機制。
8、concurrentHashMap和HashTable有什么區(qū)別
concurrentHashMap融合了hashmap和hashtable的優(yōu)勢,hashmap是不同步的,但是單線程情況下效率高,hashtable是同步的同步情況下保證程序執(zhí)行的正確性。
concurrentHashMap鎖的方式是細粒度的。concurrentHashMap將hash分為16個桶(默認值),諸如get、put、remove等常用操作只鎖住當前需要用到的桶。
concurrentHashMap的讀取并發(fā),因為讀取的大多數時候都沒有鎖定,所以讀取操作幾乎是完全的并發(fā)操作,只是在求size時才需要鎖定整個hash。
而且在迭代時,concurrentHashMap使用了不同于傳統(tǒng)集合的快速失敗迭代器的另一種迭代方式,弱一致迭代器。在這種方式中,當iterator被創(chuàng)建后集合再發(fā)生改變就不會拋出ConcurrentModificationException,取而代之的是在改變時new新的數據而不是影響原來的數據,iterator完成后再講頭指針替代為新的數據,這樣iterator時使用的是原來的數據。
9、HashMap 的工作原理是什么?
(1)存儲
當向HashMap中添加一個鍵值對時,首先會計算鍵(Key)的哈希值,這個哈希值將決定該鍵值對在內部數組中的索引位置。然后,該鍵值對會被存儲在對應索引的鏈表中。如果兩個不同的鍵擁有相同的哈希值,它們會被存儲在同一個索引位置,這種現(xiàn)象稱為哈希沖突。為了解決沖突,HashMap會在鏈表中維護這些具有相同哈希值的鍵值對。
(2)查找
當需要獲取某個特定鍵對應的值時,HashMap會再次計算該鍵的哈希值,并沿著對應索引的鏈表查找匹配的鍵。一旦找到,就返回對應的值。
(3)擴容
隨著HashMap中元素的增加,為了防止性能下降,當鏈表的長度超過一定閾值時,HashMap會進行自動擴容。這個過程涉及到創(chuàng)建一個新的、更大的數組,并將舊數組中的所有元素重新映射到新數組的索引上。這個過程也被稱為rehashing。
(4)數據結構
HashMap的內部結構是一個Entry數組,每個Entry包含一個key-value鍵值對。這樣設計的目的是為了高效地存儲和檢索數據。
10、Hashmap什么時候進行擴容?
在初始化HashMap時,需要指定其初始容量和負載因子。負載因子是一個介于0到1之間的浮點數,默認值為0.75。
當HashMap中的元素數量達到當前容量乘以負載因子時,即滿足capacity * loadFactor條件時,就會觸發(fā)擴容操作。
在擴容過程中,HashMap會創(chuàng)建一個新的數組,這個新數組的容量是原來容量的兩倍。然后,它會重新計算每個鍵值對的哈希值,并將這些鍵值對重新映射到新數組的對應位置上。這個過程可能會涉及到一些性能開銷,因為它需要重新計算哈希值和重新分配元素。
由于每次擴容都需要重新計算哈希值并重新分配元素,這會帶來一定的性能開銷。因此,我們應該盡量避免讓HashMap頻繁地進行擴容,以提高性能。
11、說一下 HashMap 的實現(xiàn)原理?
HashMap實現(xiàn)了Map接口,Map接口對鍵值對進行映射。Map中不允許重復的鍵。Map接口有兩個基本的實現(xiàn),HashMap和TreeMap。TreeMap保存了對象的排列次序,而HashMap則不能。HashMap允許鍵和值為null。HashMap是非synchronized的,但collection框架提供方法能保證HashMap synchronized,這樣多個線程同時訪問HashMap時,能保證只有一個線程更改Map。
HashMap在JDK1.7中采用數組+鏈表的存儲結構。
HashMap采取Entry數組來存儲key-value,每一個鍵值對組成了一個Entry實體,Entry類時機上是一個單向的鏈表結構,它具有next指針,指向下一個Entry實體,以此來解決Hash沖突的問題。
HashMap實現(xiàn)一個內部類Entry,重要的屬性有hash、key、value、next。
JDK1.8中采用數據+鏈表+紅黑樹的存儲形式。當鏈表長度超過閾值(8)時,將鏈表轉換為紅黑樹。在性能上進一步得到提升。
12、為什么HashMap使用紅黑樹而不使用AVL樹
- AVL樹是更加嚴格的平衡,因此可以提供更快的查找速度,一般讀取查找密集型任務,適用AVL樹;
- 紅黑樹更適合于插入修改密集型任務,即更適合HashMap;
- 通常,AVL樹的旋轉比紅黑樹的旋轉更加難以平衡和調試。
深入理解紅黑樹與AVL樹:
- AVL以及紅黑樹是高度平衡的樹數據結構。它們非常相似,真正的區(qū)別在于在任何添加/刪除操作時完成的旋轉操作次數。
- 兩種實現(xiàn)都縮放為a O(lg N),其中N是葉子的數量,但實際上AVL樹在查找密集型任務上更快:利用更好的平衡,樹遍歷平均更短。另一方面,插入和刪除方面,AVL樹速度較慢:需要更高的旋轉次數才能在修改時正確地重新平衡數據結構。
- 在AVL樹中,從根到任何葉子的最短路徑和最長路徑之間的差異最多為1。在紅黑樹中,差異可以是2倍。
- 兩個都給O(log n)查找,但平衡AVL樹可能需要O(log n)旋轉,而紅黑樹將需要最多兩次旋轉使其達到平衡(盡管可能需要檢查O(log n)節(jié)點以確定旋轉的位置)。旋轉本身是O(1)操作,因為你只是移動指針。
13、Java中的ConcurrentHashMap中為什么不能存儲null?
與HashMap一樣,ConcurrentHashMap也是一個基于散列的Map,但它使用了一種完全不同的加鎖策略來提供更高的并發(fā)性和伸縮性。ConcurrentHashMap并不是將每個方法都在同一個鎖上同步并使得每次只能有一個線程訪問容器,而是使用一種更細的加鎖機制來實現(xiàn)更大程度的共享,這種機制成為分段鎖。在這種機制中,任意數量的讀取線程可以并發(fā)地訪問Map,執(zhí)行讀取操作的線程和執(zhí)行寫入操作的線程可以并發(fā)地訪問Map,并且一定數量的寫入線程可以并發(fā)地修改Map。ConcurrentHashMap帶來的結果是,在并發(fā)訪問環(huán)境下將實現(xiàn)更高的吞吐量,而在單線程環(huán)境中只損失非常小的性能。
ConcurrentHashMap返回的迭代器具有弱一致性,而并非“及時失敗”。弱一致性的迭代器可以容忍并發(fā)的修改,當創(chuàng)建迭代器時會遍歷已有的元素,并可以在迭代器被構造后將修改操作反映給容器。ConcurrentHashMap返回的迭代器具有弱一致性,而并非“及時失敗”。弱一致性的迭代器可以容忍并發(fā)的修改,當創(chuàng)建迭代器時會遍歷已有的元素,并可以在迭代器被構造后將修改操作反映給容器。
14、Java8開始ConcurrentHashMap,為什么舍棄分段鎖?
ConcurrentHashMap的原理是引用了內部的 Segment ( ReentrantLock ) 分段鎖,保證在操作不同段 map 的時候, 可以并發(fā)執(zhí)行, 操作同段 map 的時候,進行鎖的競爭和等待。從而達到線程安全, 且效率大于 synchronized。
但是在 Java 8 之后, JDK 卻棄用了這個策略,重新使用了 synchronized+CAS。
Java 8 中的 ConcurrentHashMap 放棄了分段鎖,而是引入了 CAS 操作,即 Compare and Swap,利用原子性的操作和無鎖編程的思想,來實現(xiàn)并發(fā)寫入:采用一種樂觀鎖的方式,通過比較當前值與期望值是否相等,來決定是否更新。這種方式避免了對整個數據結構加鎖,提高了并發(fā)寫入時的性能和效率。
15、ConcurrentHashMap(JDK1.8)為什么要使用synchronized而不是如ReentranLock這樣的可重入鎖?
我想從下面幾個角度討論這個問題:
(1)鎖的粒度
首先鎖的粒度并沒有變粗,甚至變得更細了。每當擴容一次,ConcurrentHashMap的并發(fā)度就擴大一倍。
(2)Hash沖突
JDK1.7中,ConcurrentHashMap從過二次hash的方式(Segment -> HashEntry)能夠快速的找到查找的元素。在1.8中通過鏈表加紅黑樹的形式彌補了put、get時的性能差距。JDK1.8中,在ConcurrentHashmap進行擴容時,其他線程可以通過檢測數組中的節(jié)點決定是否對這條鏈表(紅黑樹)進行擴容,減小了擴容的粒度,提高了擴容的效率。
16、set有哪些實現(xiàn)類?
其實面試官想聽到的不只是有哪些實現(xiàn)類,而是觸類旁通,比如它們的原理、對比、使用場景等。
(1)HashSet
- 基于散列表實現(xiàn)的Set集合,內部的存儲結構是哈希表,是線程不安全的
- 它的底層數據結構是HashMap,因此它擁有快速的存取速度,是用的最多的實現(xiàn)類;
- HashSet不保證元素的迭代順序,也不保證該順序會隨著時間的推移保持不變;
- 允許使用null元素;
(2)TreeSet
- 基于紅黑樹(Red-Black tree)或者AVL樹等自平衡二叉查找樹實現(xiàn)的Set集合;
- TreeSet可以確保集合元素處于排序狀態(tài);
- 不允許插入null元素
TreeSet對元素進行排序的方式:
- 元素自身具備比較功能,需要實現(xiàn)Comparable接口,并覆蓋compareTo方法;
- 元素自身不具備比較功能,需要實現(xiàn)Comparator接口,并覆蓋compare方法。
(3)鏈接散列集LinkedHashSet
- LinkedHashSet結合了哈希表和鏈表兩種數據結構,具有可預知的迭代順序;
- 它繼承自HashSet,但是又添加了一個雙向鏈表來維持插入的順序;
- LinkedHashSet的元素迭代順序是它們被插入的順序,或者最近訪問的順序。
HashSet和LinkedHashSet內部使用哈希表來存儲元素,當多個元素經過哈希函數計算后產生同一個索引位置時,就會產生哈希沖突。為了解決哈希沖突,HashSet和LinkedHashSet使用鏈式散列技術,即在哈希表每個索引位置上維護一個鏈表,將所有哈希值相同的元素存放在同一個鏈表中,從而實現(xiàn)快速查找和添加元素。
LinkedHashMap的常用方法包括:
- put(K key, V value):將一個鍵值對添加到鏈接散列集中;
- get(K key):返回一個鍵值對,如果鍵不存在則返回null;
- remove(K key):從鏈接散列集中刪除一個鍵值對;
- containsKey(K key):檢查一個鍵是否存在于鏈接散列集中;
- size():返回鏈接散列集中鍵值對的數量;
這些方法都是基于鏈表實現(xiàn)的,因此它們的時間復雜度都是O(1),其中n是Map中元素的數量。
(4)AbstractSet
這是一個抽象類,它為創(chuàng)建新的set實現(xiàn)提供了一個框架。它本身不能直接實例化,但可以通過繼承并實現(xiàn)其抽象方法來創(chuàng)建自定義的set實現(xiàn)類。
面試中能說出AbstractSet的肯定不多,面試加分項。
17、說一下HashSet的實現(xiàn)原理
HashSet底層使用的是數組加鏈表或者紅黑樹的數據結構。在JDK1.8之前,主要是數組加鏈表的方式,而在JDK1.8及以后的版本中,為了優(yōu)化性能,引入了紅黑樹。
當我們向HashSet中添加一個元素時,首先會計算該元素的哈希值,然后根據哈希值確定元素在內部HashMap中的存儲位置。如果該位置為空,則直接存儲;如果不為空,則需要通過鏈表或紅黑樹來處理沖突。
在查找元素時,也是通過計算哈希值來確定位置,然后在對應的鏈表或紅黑樹中進行搜索。
HashSet的性能可以通過合理設置初始容量和負載因子來提高。一個較大的初始容量可以減少擴容操作的頻率,而合適的負載因子可以平衡空間利用率和查找效率。
18、Set是如何保證元素不重復的?
HashSet內部實際上是通過HashMap來實現(xiàn)的。當向HashSet中添加一個元素時,它會調用該元素的hashCode()方法來計算其哈希值,然后根據這個哈希值決定元素在HashMap中的存儲位置。
如果有兩個元素具有相同的哈希值,那么它們會被視為同一個位置的候選者。為了區(qū)分這些具有相同哈希值的元素,HashSet還會使用equals()方法來比較它們是否相等。只有當元素在HashMap中不存在時,它才會被添加到集合中。如果已經存在,則不會重復添加,從而保證了Set集合中元素的唯一性。
19、HashMap和HashSet的區(qū)別
(1)先了解一下HashCode
Java中的集合有兩類,一類是List,一類是Set。
List:元素有序,可以重復。
Set:元素無序,不可重復。
要想保證元素的不重復,拿什么來判斷呢?這就是Object.equals方法了。如果元素有很多,增加一個元素,就要判斷n次嗎?
顯然不現(xiàn)實,于是,Java采用了哈希表的原理。哈希算法也稱為散列算法,是將數據依特定算法直接指定到一根地址上,初學者可以簡單的理解為,HashCode方法返回的就是對象存儲的物理位置(實際上并不是)。
這樣一來,當集合添加新的元素時,先調用這個元素的hashcode()方法,就一下子能定位到他應該放置的物理位置上。如果這個位置上沒有元素,他就可以直接存儲在這個位置上,不用再進行任何比較了。如果這個位置上有元素,就調用它的equals方法與新元素進行比較,想同的話就不存了,不相同就散列其它的地址。所以這里存在一個沖突解決的問題。這樣一來實際上調用equals方法的次數就大大降低了,幾乎只需要一兩次。
簡而言之,在集合查找時,hashcode能大大降低對象比較次數,提高查找效率。
Java對象的equals方法和hashCode方法時這樣規(guī)定的:
相等的對象就必須具有相等的hashcode。
- 如果兩個對象的hashcode相同,他們并不一定相同。
- 如果兩個對象的hashcode相同,他們并不一定相同。
如果兩個Java對象A和B,A和B不相等,但是A和B的哈希碼相等,將A和B都存入HashMap時會發(fā)生哈希沖突,也就是A和B存放在HashMap內部數組的位置索引相同,這時HashMap會在該位置建立一個鏈接表,將A和B串起來放在該位置,顯然,該情況不違反HashMap的使用規(guī)則,是允許的。當然,哈希沖突越少越好,盡量采用好的哈希算法避免哈希沖突。
equals()相等的兩個對象,hashcode()一定相等;equals()不相等的兩個對象,卻并不能證明他們的hashcode()不相等。
(2)HashMap和HashSet的區(qū)別
20、TreeSet常用方法有哪些?
- add(Object obj):將一個對象添加到TreeSet中。
- remove(Object obj):從TreeSet中移除一個對象。
- pollFirst():返回TreeSet中的第一個對象,如果TreeSet為空則返回null。
- pollLast():返回TreeSet中的最后一個對象,如果TreeSet為空則返回null。
- size():返回TreeSet中元素的個數。
- isEmpty():判斷TreeSet是否為空。
- contains(Object obj):判斷一個對象是否在TreeSet中。
- addAll(Collection<? extends E> c):將一個Collection對象中的元素添加到TreeSet中。
- removeAll(Collection<? extends E> c):從TreeSet中移除一個Collection對象中的元素。
- retainAll(Collection<? extends E> c):保留一個Collection對象中的元素,并將它們添加到TreeSet中。
21、TreeMap 和 TreeSet 在排序時如何比較元素?
TreeMap 和 TreeSet 都是基于紅黑樹實現(xiàn)的,它們在排序時會使用元素的自然順序(如果元素實現(xiàn)了 Comparable 接口)或者比較器(如果構造時提供了 Comparator)。
當插入一個元素到 TreeSet 中時,它會按照指定的比較方式(自然順序或比較器)找到正確的位置來插入該元素,以保證集合中的元素是有序的。如果元素沒有實現(xiàn) Comparable 接口且沒有提供比較器,則無法進行排序,編譯時會報錯。
TreeMap 會根據鍵來對元素進行排序。當插入一個鍵值對到 TreeMap 中時,它會根據鍵的自然順序或者提供的比較器來確定鍵值對在映射中的正確位置。同樣,如果鍵的類型沒有實現(xiàn) Comparable 接口且沒有提供比較器,則無法進行排序。
22、ArrayList 和 LinkedList 的區(qū)別是什么?
ArrayList和LinkedList都是Java集合框架中的一部分,它們實現(xiàn)了List接口,用于存儲元素的動態(tài)數組。然而,它們在內部實現(xiàn)、效率、以及操作特點上有一些顯著的區(qū)別。
(1)內部實現(xiàn)
ArrayList是基于數組實現(xiàn)的,其內部維護了一個動態(tài)數組來存儲元素。數組是一塊連續(xù)的內存空間,因此ArrayList在隨機訪問元素時非常高效,時間復雜度為O(1)。然而,當添加或刪除元素時,如果數組已滿或需要移動元素,可能需要進行擴容或數據移動操作,這可能會降低效率。
LinkedList則是基于鏈表實現(xiàn)的,其內部元素是通過節(jié)點(Node)連接在一起的。每個節(jié)點包含實際的數據以及指向下一個和上一個節(jié)點的指針。因此,LinkedList在內存中的存儲不是連續(xù)的。這種結構使得LinkedList在添加或刪除元素時效率較高,因為只需要修改相關節(jié)點的指針,而不需要移動大量數據。然而,隨機訪問元素時,LinkedList需要從頭或尾開始遍歷,效率較低。
(2)效率
當需要頻繁地進行隨機訪問元素(如通過索引獲取元素)時,ArrayList通常比LinkedList更高效,因為ArrayList可以直接通過索引定位到元素。
而在添加或刪除元素時,LinkedList通常比ArrayList更高效,因為LinkedList只需要修改相關節(jié)點的指針,而不需要移動其他元素。
(3)控件開銷
ArrayList的主要控件開銷在于需要在其內部數組中預留一定的空間以存儲元素。當元素數量超出當前容量時,ArrayList會自動進行擴容,以適應更多的元素。這種擴容操作可能會導致一定的性能開銷。
LinkedList的主要控件開銷在于需要存儲每個節(jié)點的信息以及節(jié)點之間的指針信息。這增加了額外的內存開銷,但使得在鏈表中間添加或刪除元素的操作變得高效。
(4)線程安全
ArrayList和LinkedList都不是線程安全的。如果在多線程環(huán)境下使用,需要手動進行同步處理或者使用線程安全的集合類,如Collections.synchronizedList()或CopyOnWriteArrayList。
綜上所述,ArrayList和LinkedList各有其優(yōu)勢和適用場景。在選擇使用哪種集合時,應根據具體的應用需求和性能要求來做出決策。如果需要頻繁地進行隨機訪問元素,ArrayList可能更合適;而如果需要頻繁地進行添加或刪除元素的操作,LinkedList可能更合適。
23、ArrayList 和 Vector 的區(qū)別?
ArrayList和Vector都是基于數組實現(xiàn)的List集合類,它們提供了動態(tài)數組的功能,可以根據需要自動調整大小以存儲對象。
Vector的公共方法大多帶有synchronized關鍵字,確保了方法是同步的,因此Vector是線程安全的。而ArrayList沒有這樣的同步措施,所以它是線程不安全的。
由于Vector的方法是同步的,因此在多線程環(huán)境下會涉及鎖的獲取和釋放,這可能導致性能上的開銷。相比之下,ArrayList由于沒有額外的同步開銷,通常運行得更快。
當底層數組容量不足以容納新元素時,ArrayList會在原有基礎上擴展約0.5倍的容量,而Vector則擴展一倍的容量。這意味著在頻繁進行大量添加操作的情況下,ArrayList可能會有更高的效率。
24、隊列和棧是什么?有什么區(qū)別?
隊列先進先出,棧先進后出。
遍歷數據速度不同。
棧只能從頭部取數據 也就最先放入的需要遍歷整個棧最后才能取出來,而且在遍歷數據的時候還得為數據開辟臨時空間,保持數據在遍歷前的一致性;
隊列則不同,他基于地址指針進行遍歷,而且可以從頭或尾部開始遍歷,但不能同時遍歷,無需開辟臨時空間,因為在遍歷的過程中不影像數據結構,速度要快的多。
25、Queue和Deque的區(qū)別是什么?
Queue以及Deque都是繼承于Collection,Deque是Queue的子接口。
Queue是FIFO的單向隊列,Deque是雙向隊列。
Queue有一個直接子類PriorityQueue,而Deque中直接子類有兩個:LinkedList以及ArrayDeque。
PriorityQueue的底層數據結構是數組,而無邊界的形容,那么指明了PriorityQueue是自帶擴容機制的。
ArrayDeque是無初始容量的雙端隊列,LinkedList則是雙向鏈表。
PriorityQueue可以作為堆使用,而且可以根據傳入的Comparator實現(xiàn)大小的調整,會是一個很好的選擇。ArrayDeque通常作為?;蜿犃惺褂茫菞5男什蝗鏛inkedList高。LinkedList通常作為棧或隊列使用,但是隊列的效率不如ArrayQueue高。
26、在 Queue 中 poll()和 remove()有什么區(qū)別?
offer()和add()區(qū)別:
增加新項時,如果隊列滿了,add會拋出異常,offer返回false。
poll()和remove()區(qū)別:
poll()和remove()都是從隊列中刪除第一個元素,remove拋出異常,poll返回null。
peek()和element()區(qū)別:
peek()和element()用于查詢隊列頭部元素,為空時element拋出異常,peek返回null。
27、說說你對優(yōu)先隊列的理解
優(yōu)先隊列中的元素可以按照任意的順序插入,但會按照有序的順序獲取。
優(yōu)先隊列常用結構是PriorityQueue和ArrayDeque。
也就是在調用remove時,總是刪除隊列中最小的元素。
優(yōu)先隊列使用堆作為存儲數據結構,堆是一個自組織的二叉樹,其添加和刪除操作會讓最小的元素移動到根,而不必花費時間對元素進行排序。
優(yōu)先隊列的主要用途是調度。每個任務有一個優(yōu)先級,任務以隨機順序插入到隊列中,每當啟動一個新的任務時,將從隊列中刪除優(yōu)先級最高的任務。
28、說說你對雙端隊列的理解
雙端隊列是一種特殊的隊列,它的兩端都可以進行插入和刪除操作。這種隊列的實現(xiàn)方式是使用兩個指針,一個指針指向隊列的頭部,另一個指針指向隊列的尾部。當需要插入或刪除元素時,只需要移動指針即可。
雙端隊列的主要優(yōu)點是可以在隊列的兩端進行操作,因此具有較高的效率。此外,雙端隊列還具有一些其他的優(yōu)點,例如可以在隊列的兩端進行查詢操作,因此具有較高的查詢效率。
雙端隊列的缺點是插入和刪除操作的時間復雜度都是O(1),因此在處理大量數據時可能會導致性能問題。此外,雙端隊列的空間復雜度也是O(1),因此在插入和刪除元素時需要使用額外的空間。
29、CopyOnWriteArrayList是什么,有哪些應用場景?
CopyOnWriteArrayList是Java并發(fā)包java.util.concurrent下提供的一個線程安全的ArrayList實現(xiàn),它是寫時復制(Copy-On-Write)的容器。
CopyOnWriteArrayList的核心特性在于,當修改容器(例如添加、刪除元素)時,不是直接修改當前容器,而是先復制當前容器的副本,然后在副本上進行修改。修改完成后,再將原容器的引用指向新的容器。這種策略使得讀操作可以完全不用加鎖,因此讀取性能極高。同時,寫入操作也不會阻塞讀取操作,只有寫入和寫入之間需要進行同步等待。
由于CopyOnWriteArrayList的這些特性,它特別適用于以下場景:
- 讀多寫少的場景:當對數據的讀操作次數遠遠高于寫操作時,使用CopyOnWriteArrayList可以有效提升系統(tǒng)性能。因為每次修改操作都會創(chuàng)建底層數組的副本,從而避免了讀取操作受到寫入操作的干擾。
- 數據更新要求不頻繁的場景:由于每次添加、修改或刪除列表中的元素時,CopyOnWriteArrayList都需要重新創(chuàng)建一個新的底層數組,因此在實現(xiàn)上會消耗更多的內存空間。因此,它更適用于數據更新不頻繁的場景。
- 互斥訪問數據不方便的場景:在多線程環(huán)境下,如果需要對一個ArrayList實例進行訪問,通常需要加鎖以保證數據一致性。但在某些場景下,加鎖可能會給程序帶來額外的復雜度和延遲。此時,可以考慮使用CopyOnWriteArrayList。
- 需要保證數據一致性的場景:由于每個線程都在自己的副本上進行操作,因此不存在讀取過程中數據被其他線程修改的問題,從而保證了數據的一致性。
30、使用CopyOnWriteArrayList時需要注意哪些問題?
(1)內存占用問題
由于CopyOnWriteArrayList的寫時復制機制,當進行寫操作時,內存中會同時駐扎兩個對象的內存,舊的對象和新寫入的對象。在復制時只復制容器里的引用,在寫時才會創(chuàng)建新對象添加到新容器里,而舊容器的對象還在使用,所以有兩份對象內存。
(2)數據一致性問題
CopyOnWriteArrayList只能保證數據的最終一致性,不能保證數據的實時一致性。因為復制和操作元素需要一些時間,所以會有延遲。如果希望寫入的數據馬上能讀到,要求數據強一致性的話,建議不要使用CopyOnWriteArrayList。
(3)線程安全
CopyOnWriteArrayList是寫同步,讀非同步的。多個線程對CopyOnWriteArrayList進行寫操作是線程安全的,但是在讀操作時是非線程安全的。如果在for循環(huán)中使用下標的方式去讀取數據,可能會報錯ArrayIndexOutOfBoundsException。
(4)不支持add()、set()、remove()方法
CopyOnWriteArrayList的迭代器實現(xiàn)了ListIterator接口,但是add()、set()、remove()方法都直接拋出了UnsupportedOperationException異常,所以應該避免使用迭代器的這幾個方法。
31、說一下鏈表的實現(xiàn)原理
從數組中間刪除一個元素開銷很大,其原因是向數組中插入元素時,此元素之后的所有元素都要向后端移動,刪除時也是,數組中位于被刪除元素之后的所有元素都要向數組的前端移動。
此時,在Java中,可以通過鏈表解決這個問題。
數組是在連續(xù)的存儲位置上存放對象引用,而鏈表則是將每個對象存放在單獨的鏈接link中。每個鏈接還存放著序列中下一個鏈接的引用。在Java中,所有的鏈表都是雙向鏈接,即每個鏈接還存儲前驅的引用。
在鏈表中新增、刪除一個元素是很輕松的操作,只需要更新鎖刪除元素前后對應的鏈接即可。
有的同學可能覺得上面兩個圖,沒啥區(qū)別,其實就是前后鏈接指向的問題,so easy。
在Java中,可以使用雙指針法來向鏈表中間添加元素。
ListNode newNode = new ListNode(val);
if (head == null) {
head = newNode;
} else {
ListNode curr = head;
while (curr.next != null && curr.next.next != null) {
curr = curr.next;
}
curr.next = newNode;
}
在上面的代碼中,我們首先創(chuàng)建一個新的節(jié)點newNode,并將其插入到鏈表的中間。如果鏈表為空,則將新節(jié)點設置為頭部節(jié)點。否則,我們遍歷鏈表,找到最后一個節(jié)點,并將新節(jié)點插入到該節(jié)點的后面。
32、說一下散列表的實現(xiàn)原理
如果想要查找某個元素,但又不知道它的存儲位置,此時,就需要遍歷所有元素,直到找到匹配的元素為止。如果集合中包含的元素很多,就需要耗費很長時間時間。
此時,散列表閃亮登場。
散列表可以快速的查找對象,散列表為每個元素計算一個整數,稱為散列碼,散列碼是以某種方式由對象的實例字段得出的一個整數,可以保證不同的數據對象擁有不同的散列碼。
在Java中,刪列表實現(xiàn)為鏈表數組,每個列表被稱為桶bucket,可以通過:先計算散列碼,再與桶的總數取余,所得到的數就是保存這個元素的那個桶的索引。
可以通過初始化桶數的方式,快速的進行元素插入。
如果裝載因子是0.75,當表中已經填到75%就會進行自動再散列,新的桶數就是原來的兩倍。對大多數情況而言,裝載因子為0.75是比較合理的。
33、說說你對映射視圖的理解
Java中的映射視圖是指一種將Map轉換為Set或Collection的機制,以便于更方便地操作Map中的元素。
- keySet():返回包含Map中所有鍵的Set;
- values():返回包含Map中所有值的Collection;
- entrySet():返回包含Map中所有鍵值對的Set;
通過這些映射視圖,可以方便地遍歷Map中的元素、檢查某個鍵是否存在、刪除指定鍵等操作。
在使用時需要注意,映射視圖只是一個視圖,即對原Map進行的修改會反映到相應的映射視圖中,反之亦然,因此要謹慎使用。
另外,由于映射視圖是基于Map實現(xiàn)的,因此對映射視圖的修改也可能影響到原Map中的元素。
34、說說你對弱散列映射WeakHashMap的理解
Java中的弱散列映射指的是一種特殊的Map實現(xiàn),即WeakHashMap類。和普通HashMap不同,WeakHashMap中的鍵是弱引用,即當某個鍵不再被外部對象引用時,該鍵及其對應的值會被自動清除掉,以避免內存泄漏問題。
通過使用WeakHashMap,可以將某些對象與其他應用邏輯分離開來,使得它們的生命周期僅由其它對象的引用決定,當沒有任何對象引用時,這些對象會被自動清除,從而釋放系統(tǒng)資源。在Java中,常用WeakHashMap來實現(xiàn)緩存、事件通知等場景。需要注意的是,由于弱引用的存在,WeakHashMap無法保證元素的順序,因此在遍歷時應該謹慎。
WeakHashMap是一種基于紅黑樹實現(xiàn)的有序映射,它的常用方法包括:
- put(K key, V value):將一個鍵值對添加到弱散列映射中;
- get(K key):返回一個鍵值對,如果鍵不存在則返回null;
- remove(K key):從弱散列映射中刪除一個鍵值對;
- containsKey(K key):檢查一個鍵是否存在于弱散列映射中;
- size():返回弱散列映射中鍵值對的數量;
這些方法都是基于紅黑樹實現(xiàn)的,因此它們的時間復雜度都是O(log n),其中n是Map中元素的數量。
35、說說你對鏈接散列映射LinkedHashMap的理解
Java中的鏈接散列映射指的是HashMap和LinkedHashMap這兩個鍵值對映射集合實現(xiàn)類。它們都是基于哈希表實現(xiàn)的,鏈式散列是解決哈希沖突的一種方法。
具體來說,HashMap和LinkedHashMap內部使用哈希表來存儲鍵值對,當多個鍵經過哈希函數計算后產生同一個索引位置時,就會產生哈希沖突。為了解決哈希沖突,HashMap和LinkedHashMap使用鏈式散列技術,即在哈希表每個索引位置上維護一個鏈表,將所有哈希值相同的鍵值對存放在同一個鏈表中,從而實現(xiàn)快速查找和添加元素。
HashMap和LinkedHashMap的區(qū)別在于,前者是無序鍵值對集合,而后者是有序鍵值對集合。具體來說,LinkedHashMap內部使用一個雙向鏈表來維護鍵值對的插入順序,因此遍歷LinkedHashMap時可以按照鍵值對插入的順序進行。需要注意的是,在使用HashMap和LinkedHashMap時,應根據具體的業(yè)務需求和性能要求選擇合適的實現(xiàn)類。
36、說說你對LinkedHashSet的理解
Java中的鏈接散列集指的是HashSet和LinkedHashSet這兩個集合實現(xiàn)類。它們都是基于哈希表(Hash Table)實現(xiàn)的,鏈式散列是解決哈希沖突的一種方法。
HashSet和LinkedHashSet內部使用哈希表來存儲元素,當多個元素經過哈希函數計算后產生同一個索引位置時,就會產生哈希沖突。為了解決哈希沖突,HashSet和LinkedHashSet使用鏈式散列技術,即在哈希表每個索引位置上維護一個鏈表,將所有哈希值相同的元素存放在同一個鏈表中,從而實現(xiàn)快速查找和添加元素。
HashSet和LinkedHashSet的區(qū)別在于,前者是無序集合,而后者是有序集合。具體來說,LinkedHashSet內部使用一個雙向鏈表來維護元素的插入順序,因此遍歷LinkedHashSet時可以按照元素插入的順序進行。需要注意的是,在使用HashSet和LinkedHashSet時,應根據具體的業(yè)務需求和性能要求選擇合適的實現(xiàn)類。
LinkedHashMap的常用方法包括:
- put(K key, V value):將一個鍵值對添加到鏈接散列集中;
- get(K key):返回一個鍵值對,如果鍵不存在則返回null;
- remove(K key):從鏈接散列集中刪除一個鍵值對;
- containsKey(K key):檢查一個鍵是否存在于鏈接散列集中;
- size():返回鏈接散列集中鍵值對的數量;
這些方法都是基于鏈表實現(xiàn)的,因此它們的時間復雜度都是O(1),其中n是Map中元素的數量。
37、說說你對枚舉集EnumSet的理解
Java中的枚舉集指的是基于枚舉類型實現(xiàn)的集合類,即EnumSet。
它是一個專門用于存儲枚舉類型值的高效集合實現(xiàn)類,可以實現(xiàn)基本操作(如添加、刪除、查找等)和集合運算(如交、并、補等),同時還提供了高性能的迭代器,可以按照枚舉類型常量在內存中出現(xiàn)的順序進行遍歷。
EnumSet使用位向量(bit vector)實現(xiàn),即將每個枚舉類型常量映射到一個二進制位上,從而快速進行集合運算。由于EnumSet只能存儲枚舉類型值,因此它具有類型安全性、性能高效、空間利用率高等優(yōu)點。
EnumSet是一個抽象類,不能直接實例化,但可以通過EnumSet的靜態(tài)工廠方法創(chuàng)建實例,例如EnumSet.of()、EnumSet.range()等。此外,EnumSet也支持各種集合轉換操作,可以與其他集合實現(xiàn)類進行互相轉換。
38、說說你對EnumMap的理解
Java中的枚舉映射指的是基于枚舉類型實現(xiàn)的鍵值對集合類,即EnumMap。它是一個專門用于存儲枚舉類型作為鍵的鍵值對集合實現(xiàn)類,可以實現(xiàn)基本操作(如添加、刪除、查找等)和集合運算(如交、并、補等),同時還提供了高性能的迭代器,可以按照枚舉類型常量在內存中出現(xiàn)的順序進行遍歷。
EnumMap使用數組實現(xiàn),數組的長度等于枚舉類型常量數目,每個位置上存儲的是該枚舉類型常量所對應的值。由于EnumMap只能存儲枚舉類型作為鍵,因此它具有類型安全性、性能高效、空間利用率高等優(yōu)點。
需要注意的是,EnumMap也是一個抽象類,不能直接實例化,但可以通過EnumMap的構造方法或靜態(tài)工廠方法創(chuàng)建實例,例如new EnumMap<>(MyEnum.class)、EnumMap.copyOf()等。此外,EnumMap也支持各種集合轉換操作,可以與其他集合實現(xiàn)類進行互相轉換。
39、Comparator與Comparable有什么區(qū)別
Comparator與Comparable在Java中都是用于比較對象大小的接口。
首先,從功能角度來看,Comparable是一個排序接口,當一個類實現(xiàn)了Comparable接口時,它表示這個類的對象之間可以相互比較大小,即這個類支持排序。而Comparator則是一個比較器接口,它允許我們實現(xiàn)該接口,自定義比較算法,從而創(chuàng)建一個特定類的比較器來進行排序。可以說,Comparable相當于“內部比較器”,而Comparator相當于“外部比較器”。
其次,從使用靈活性來看,Comparable接口的耦合性相對較強,通常用作類的默認排序方法。這意味著如果我們需要對一個類的實例進行排序,而該類的定義又沒有發(fā)生變化,我們通常會讓該類實現(xiàn)Comparable接口,并提供比較的邏輯。而Comparator接口的靈活性和擴展性更優(yōu),它主要用于當默認排序不滿足需求時,提供自定義排序。例如,我們可能需要對一個已經存在的類進行排序,而這個類并沒有實現(xiàn)Comparable接口,或者我們想要對這個類進行不同的排序,這時就可以使用Comparator接口。
最后,從方法定義上來看,Comparable接口定義了一個抽象方法compareTo(T o),用于比較當前對象與另一個對象的大小。而Comparator接口定義了一個抽象方法compare(T o1, T o2),用于比較兩個對象的大小。
40、Iterator 怎么使用?有什么特點?
為了方便的處理集合中的元素,Java中出現(xiàn)了一個對象,該對象提供了一些方法專門處理集合中的元素.例如刪除和獲取集合中的元素.該對象就叫做迭代器(Iterator)。
Iterator 接口源碼中的方法:
- java.lang.Iterable 接口被 java.util.Collection 接口繼承,java.util.Collection 接口的 iterator() 方法返回一個 Iterator 對象
- next() 方法獲得集合中的下一個元素
- hasNext() 檢查集合中是否還有元素
- remove() 方法將迭代器新返回的元素刪除
41、Iterator 和 ListIterator 有什么區(qū)別?
ListIterator 繼承 Iterator。
ListIterator 比 Iterator多方法:
- add(E e) 將指定的元素插入列表,插入位置為迭代器當前位置之前。
- set(E e) 迭代器返回的最后一個元素替換參數e。
- hasPrevious() 迭代器當前位置,反向遍歷集合是否含有元素。
- previous() 迭代器當前位置,反向遍歷集合,下一個元素。
- previousIndex() 迭代器當前位置,反向遍歷集合,返回下一個元素的下標。
- nextIndex() 迭代器當前位置,返回下一個元素的下標。
使用范圍不同,Iterator可以迭代所有集合;ListIterator 只能用于List及其子類。
- ListIterator 有 add 方法,可以向 List 中添加對象;Iterator 不能。
- ListIterator 有 hasPrevious() 和 previous() 方法,可以實現(xiàn)逆向遍歷;Iterator不可以。
- ListIterator 有 nextIndex() 和previousIndex() 方法,可定位當前索引的位置;Iterator不可以。
- ListIterator 有 set()方法,可以實現(xiàn)對 List 的修改;Iterator 僅能遍歷,不能修改。
42、快速失敗 (fail-fast) 和安全失敗 (fail-safe) 的區(qū)別是什么?
快速失?。╢ail-fast)策略的核心在于一旦發(fā)現(xiàn)數據結構在迭代過程中被修改,系統(tǒng)會立即停止迭代并通過拋出ConcurrentModificationException異常來報告錯誤。這樣做的目的是盡早地發(fā)現(xiàn)錯誤,防止錯誤的擴散,從而保證軟件的穩(wěn)定性和可靠性。例如,在使用ArrayList或HashMap等集合類進行迭代時,如果嘗試在迭代過程中修改集合內容,就會觸發(fā)這種快速失敗的行為。
安全失?。╢ail-safe)策略則相對寬容,它允許在迭代過程中對數據結構進行修改,而不會立即拋出異常。這種策略通常通過使用額外的措施,如迭代器復制、鎖或其他同步機制,來確保即使在多線程環(huán)境下也能安全地進行操作。這樣可以減少因并發(fā)修改導致的問題,提高程序的容錯性。