您好,登錄后才能下訂單哦!
本篇內容主要講解“有哪些Java集合面試題”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“有哪些Java集合面試題”吧!
Map接口和Collection接口是所有集合框架的父接口。下圖中的實線和虛線看著有些亂,其中接口與接口之間如果有聯系為繼承關系,類與類之間如果有聯系為繼承關系,類與接口之間則是類實現接口。重點掌握的抽象類有HashMap
,LinkedList
,HashTable
,ArrayList
,HashSet
,Stack
,TreeSet
,TreeMap
。注意:Collection
接口不是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
是 Java 迭代器最簡單的實現,它不是一個集合,它是一種用于訪問集合的方法,Iterator
接口提供遍歷任何Collection
的接口。
快速失敗
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
包下的容器都是安全失敗,可以在多線程下并發使用。
從上文**“快速失敗機制”**可知在遍歷集合時如果直接調用remove()
方法會拋出ConcurrentModificationException
異常,所以使用迭代器中調用remove()
方法。
Array
可以包含基本類型和對象類型,ArrayList
只能包含對象類型。
Array
大小是固定的,ArrayList
的大小是動態變化的。(ArrayList
的擴容是個常見面試題)
相比于Array
,ArrayList
有著更多的內置方法,如addAll()
,removeAll()
。
對于基本類型數據,ArrayList
使用自動裝箱來減少編碼工作量;而當處理固定大小的基本數據類型的時候,這種方式相對比較慢,這時候應該使用Array
。
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
是一個包裝類。它包含有各種有關集合操作的靜態多態方法,例如常用的sort()
方法。此類不能實例化,就像一個工具類,服務于Java的Collection
框架。
先說一下常見的元素在內存中的存儲方式,主要有兩種:
順序存儲(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
的初始容量為10,擴容時對是舊的容量值加上舊的容量數值進行右移一位(位運算,相當于除以2,位運算的效率更高),所以每次擴容都是舊的容量的1.5倍。
具體的實現大家可查看下ArrayList
的源碼。
是否線程安全:ArrayList
和LinkedList
都是不保證線程安全的
底層實現:ArrayList
的底層實現是數組,LinkedList
的底層是雙向鏈表。
內存占用:ArrayList
會存在一定的空間浪費,因為每次擴容都是之前的1.5倍,而LinkedList
中的每個元素要存放直接后繼和直接前驅以及數據,所以對于每個元素的存儲都要比ArrayList
花費更多的空間。
應用場景:ArrayList
的底層數據結構是數組,所以在插入和刪除元素時的時間復雜度都會收到位置的影響,平均時間復雜度為o(n),在讀取元素的時候可以根據下標直接查找到元素,不受位置的影響,平均時間復雜度為o(1),所以ArrayList
更加適用于多讀,少增刪的場景。LinkedList
的底層數據結構是雙向鏈表,所以插入和刪除元素不受位置的影響,平均時間復雜度為o(1),如果是在指定位置插入則是o(n),因為在插入之前需要先找到該位置,讀取元素的平均時間復雜度為o(n)。所以LinkedList
更加適用于多增刪,少讀寫的場景。
相同點
都實現了List
接口
底層數據結構都是數組
不同點
線程安全:Vector
使用了Synchronized
來實現線程同步,所以是線程安全的,而ArrayList
是線程不安全的。
性能:由于Vector
使用了Synchronized
進行加鎖,所以性能不如ArrayList
。
擴容:ArrayList
和Vector
都會根據需要動態的調整容量,但是ArrayList
每次擴容為舊容量的1.5倍,而Vector
每次擴容為舊容量的2倍。
ArrayList
底層數據結構為數組,對元素的讀取速度快,而增刪數據慢,線程不安全。
LinkedList
底層為雙向鏈表,對元素的增刪數據快,讀取慢,線程不安全。
Vector
的底層數據結構為數組,用Synchronized
來保證線程安全,性能較差,但線程安全。
HashSet
的底層是HashMap
,默認構造函數是構建一個初始容量為16,負載因子為0.75 的HashMap
。HashSet
的值存放于HashMap
的key
上,HashMap
的value
統一為PRESENT
。
這里面涉及到了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
方法判斷兩個對象是否相同,如果相同,則添加對象失敗,如果不相同,則會將該對象重新散列到其他位置。
HashMap | HashSet |
---|---|
實現了Map 接口 | 實現了Set 接口 |
存儲鍵值對 | 存儲對象 |
key 唯一,value 不唯一 | 存儲對象唯一 |
HashMap 使用鍵(Key )計算Hashcode | HashSet 使用成員對象來計算hashcode 值 |
速度相對較快 | 速度相對較慢 |
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
是通過key
的hash值來確定存儲的位置,但Hash值的范圍是-2147483648到2147483647,不可能建立一個這么大的數組來覆蓋所有hash值。所以在計算完hash值后會對數組的長度進行取余操作,如果數組的長度是2的冪次方,(length - 1)&hash
等同于hash%length
,可以用(length - 1)&hash
這種位運算來代替%取余的操作進而提高性能。
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; }
這個主要是考慮空間利用率和查詢成本的一個折中。如果加載因子過高,空間利用率提高,但是會使得哈希沖突的概率增加;如果加載因子過低,會頻繁擴容,哈希沖突概率降低,但是會使得空間利用率變低。具體為什么是0.75,不是0.74或0.76,這是一個基于數學分析(泊松分布)和行業規定一起得到的一個結論。
可能有很多人會問,既然紅黑樹性能這么好,為什么不一開始直接使用紅黑樹,而是先用鏈表,鏈表長度大于8時,才轉換為紅紅黑樹。
因為紅黑樹的節點所占的空間是普通鏈表節點的兩倍,但查找的時間復雜度低,所以只有當節點特別多時,紅黑樹的優點才能體現出來。至于為什么是8,是通過數據分析統計出來的一個結果,鏈表長度到達8的概率是很低的,綜合鏈表和紅黑樹的性能優缺點考慮將大于8的鏈表轉化為紅黑樹。
鏈表轉化為紅黑樹除了鏈表長度大于8,還要HashMap
中的數組長度大于64。也就是如果HashMap
長度小于64,鏈表長度大于8是不會轉化為紅黑樹的,而是直接擴容。
哈希沖突: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時,將鏈表轉化為紅黑樹,可以提高遍歷鏈表的速度。
hashCode()
處理后的哈希值范圍太大,不可能在內存建立這么大的數組。
可以,但要注意以下兩點:
如果類重寫了 equals()
方法,也應該重寫hashCode()
方法。
最好定義key
類是不可變的,這樣key
對應的hashCode()
值可以被緩存起來,性能更好,這也是為什么String
特別適合作為HashMap
的key
。
這些包裝類都是final
修飾,是不可變性的, 保證了key
的不可更改性,不會出現放入和獲取時哈希值不同的情況。
它們內部已經重寫過hashcode()
,equal()
等方法。
重寫hashCode()
方法,因為需要計算hash值確定存儲位置
重寫equals()
方法,因為需要保證key
的唯一性。
由于JDK1.7的
hashMap
遇到hash沖突采用的是頭插法,在多線程情況下會存在死循環問題,但JDK1.8已經改成了尾插法,不存在這個問題了。但需要注意的是JDK1.8中的HashMap
仍然是不安全的,在多線程情況下使用仍然會出現線程安全問題。基本上面試時說到這里既可以了,具體流程用口述是很難說清的,感興趣的可以看這篇文章。HASHMAP的死循環
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
的底層數據結構是數組+鏈表,鏈表主要是為了解決哈希沖突,并且整個數組都是synchronized
修飾的,所以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多個 | 不支持 | 不支持 |
這些常用方法是需要背下來的,雖然面試用不上,但是筆試或者面試寫算法題時會經常用到。
方法 | |
---|---|
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值 |
方法 | |
---|---|
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) | 截取集合(左閉右開) |
方法 | |
---|---|
addFirst() | 在頭部添加元素 |
addLast() | 在尾部添加元素 |
removeFirst() | 在頭部刪除元素 |
removeLat() | 在尾部刪除元素 |
getFirst() | 獲取頭部元素 |
getLast() | 獲取尾部元素 |
方法 | |
---|---|
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 |
方法 | |
---|---|
boolean empty() | 測試堆棧是否為空。 |
E peek() | 查看堆棧頂部的對象,但不從堆棧中移除它。 |
E pop() | 移除堆棧頂部的對象,并作為此函數的值返回該對象。 |
E push(E item) | 把項壓入堆棧頂部。 |
int search(Object o) | 返回對象在堆棧中的位置,以 1 為基數。 |
方法 | |
---|---|
boolean add(E e) | 將指定元素插入到隊列的尾部(隊列滿了話,會拋出異常) |
boolean offer(E e) | 將指定元素插入此隊列的尾部(隊列滿了話,會返回false) |
E remove() | 返回取隊列頭部的元素,并刪除該元素(如果隊列為空,則拋出異常) |
E poll() | 返回隊列頭部的元素,并刪除該元素(如果隊列為空,則返回null) |
E element() | 返回隊列頭部的元素,不刪除該元素(如果隊列為空,則拋出異常) |
E peek() | 返回隊列頭部的元素,不刪除該元素(如果隊列為空,則返回null) |
到此,相信大家對“有哪些Java集合面試題”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。