本篇內(nèi)容主要講解“有哪些Java集合面試題”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“有哪些Java集合面試題”吧!
10年積累的網(wǎng)站設(shè)計(jì)制作、成都網(wǎng)站設(shè)計(jì)經(jīng)驗(yàn),可以快速應(yīng)對(duì)客戶對(duì)網(wǎng)站的新想法和需求。提供各種問題對(duì)應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認(rèn)識(shí)你,你也不認(rèn)識(shí)我。但先制作網(wǎng)站后付款的網(wǎng)站建設(shè)流程,更有富拉爾基免費(fèi)網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
Map接口和Collection接口是所有集合框架的父接口。下圖中的實(shí)線和虛線看著有些亂,其中接口與接口之間如果有聯(lián)系為繼承關(guān)系,類與類之間如果有聯(lián)系為繼承關(guān)系,類與接口之間則是類實(shí)現(xiàn)接口。重點(diǎn)掌握的抽象類有HashMap
,LinkedList
,HashTable
,ArrayList
,HashSet
,Stack
,TreeSet
,TreeMap
。注意:Collection
接口不是Map
的父接口。
List
:有序集合(有序指存入的順序和取出的順序相同,不是按照元素的某些特性排序),可存儲(chǔ)重復(fù)元素,可存儲(chǔ)多個(gè)null
。
Set
:無序集合(元素存入和取出順序不一定相同),不可存儲(chǔ)重復(fù)元素,只能存儲(chǔ)一個(gè)null
。
Map
:使用鍵值對(duì)的方式對(duì)元素進(jìn)行存儲(chǔ),key
是無序的,且是唯一的。value
值不唯一。不同的key
值可以對(duì)應(yīng)相同的value
值。
Lis
t:
ArrayList
:數(shù)組
LinkedList
:雙線鏈表
Set
:
HashSet
:底層基于HashMap
實(shí)現(xiàn),HashSet
存入讀取元素的方式和HashMap
中的Key
是一致的。
TreeSet
:紅黑樹
Map
:
HashMap
: JDK1.8之前HashMap
由數(shù)組+鏈表組成的, JDK1.8之后有數(shù)組+鏈表/紅黑樹組成,當(dāng)鏈表長度大于8時(shí),鏈表轉(zhuǎn)化為紅黑樹,當(dāng)長度小于6時(shí),從紅黑樹轉(zhuǎn)化為鏈表。這樣做的目的是能提高HashMap
的性能,因?yàn)榧t黑樹的查找元素的時(shí)間復(fù)雜度遠(yuǎn)小于鏈表。
HashTable
:數(shù)組+鏈表
TreeMap
:紅黑樹
Vector
:相當(dāng)于有同步機(jī)制的ArrayList
Stack
:棧
HashTable
enumeration
:枚舉
Iterator
是 Java 迭代器最簡單的實(shí)現(xiàn),它不是一個(gè)集合,它是一種用于訪問集合的方法,Iterator
接口提供遍歷任何Collection
的接口。
快速失敗
Java的快速失敗機(jī)制是Java集合框架中的一種錯(cuò)誤檢測(cè)機(jī)制,當(dāng)多個(gè)線程同時(shí)對(duì)集合中的內(nèi)容進(jìn)行修改時(shí)可能就會(huì)拋出ConcurrentModificationException
異常。其實(shí)不僅僅是在多線程狀態(tài)下,在單線程中用增強(qiáng)for
循環(huán)中一邊遍歷集合一邊修改集合的元素也會(huì)拋出ConcurrentModificationException
異常??聪旅娲a
public class Main{ public static void main(String[] args) { List<Integer> list = new ArrayList<>(); for(Integer i : list){ list.remove(i); //運(yùn)行時(shí)拋出ConcurrentModificationException異常 } } }
正確的做法是用迭代器的remove()
方法,便可正常運(yùn)行。
public class Main{ public static void main(String[] args) { List<Integer> list = new ArrayList<>(); Iterator<Integer> it = list.iterator(); while(it.hasNext()){ it.remove(); } } }
造成這種情況的原因是什么?細(xì)心的同學(xué)可能已經(jīng)發(fā)現(xiàn)兩次調(diào)用的remove()
方法不同,一個(gè)帶參數(shù)據(jù),一個(gè)不帶參數(shù),這個(gè)后面再說,經(jīng)過查看ArrayList源碼,找到了拋出異常的代碼
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
從上面代碼中可以看到如果modCount
和expectedModCount
這兩個(gè)變量不相等就會(huì)拋出ConcurrentModificationException
異常。那這兩個(gè)變量又是什么呢?繼續(xù)看源碼
protected transient int modCount = 0; //在AbstractList中定義的變量
int expectedModCount = modCount;//在ArrayList中的內(nèi)部類Itr中定義的變量
從上面代碼可以看到,modCount
初始值為0,而expectedModCount
初始值等于modCount
。也就是說在遍歷的時(shí)候直接調(diào)用集合的remove()
方法會(huì)導(dǎo)致modCount
不等于expectedModCount
進(jìn)而拋出ConcurrentModificationException
異常,而使用迭代器的remove()
方法則不會(huì)出現(xiàn)這種問題。那么只能在看看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
沒有操作,當(dāng)每一次迭代時(shí),迭代器會(huì)比較expectedModCount
和modCount
的值是否相等,所以在調(diào)用remove()
方法后,modCount
不等于expectedModCount
了,這時(shí)就了報(bào)ConcurrentModificationException
異常。但用迭代器中remove()
的方法為什么不拋異常呢?原來**迭代器調(diào)用的remove()
方法和上面的remove()
方法不是同一個(gè)!**迭代器調(diào)用的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
是相等的,進(jìn)而保證了在增強(qiáng)for
循環(huán)中修改集合內(nèi)容不會(huì)報(bào)ConcurrentModificationException
異常。
上面介紹的只是單線程的情況,用迭代器調(diào)用remove()
方法即可正常運(yùn)行,但如果是多線程會(huì)怎么樣呢?
答案是在多線程的情況下即使用了迭代器調(diào)用remove()
方法,還是會(huì)報(bào)ConcurrentModificationException
異常。這又是為什么呢?還是要從expectedModCount
和modCount
這兩個(gè)變量入手分析,剛剛說了modCount
在AbstractList
類中定義,而expectedModCount
在ArrayList
內(nèi)部類中定義,所以modCount
是個(gè)共享變量而expectedModCount
是屬于線程各自的。簡單說,線程1更新了modCount
和屬于自己的expectedModCount
,而在線程2看來只有modCount
更新了,expectedModCount
并未更新,所以會(huì)拋出ConcurrentModificationException
異常。
安全失敗
采用安全失敗機(jī)制的集合容器,在遍歷時(shí)不是直接在集合內(nèi)容上訪問的,而是先復(fù)制原有集合內(nèi)容,在拷貝的集合上進(jìn)行遍歷。所以在遍歷過程中對(duì)原集合所作的修改并不能被迭代器檢測(cè)到,所以不會(huì)拋出ConcurrentModificationException
異常。缺點(diǎn)是迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發(fā)生了修改,迭代器是無法訪問到修改后的內(nèi)容。java.util.concurrent
包下的容器都是安全失敗,可以在多線程下并發(fā)使用。
從上文**“快速失敗機(jī)制”**可知在遍歷集合時(shí)如果直接調(diào)用remove()
方法會(huì)拋出ConcurrentModificationException
異常,所以使用迭代器中調(diào)用remove()
方法。
Array
可以包含基本類型和對(duì)象類型,ArrayList
只能包含對(duì)象類型。
Array
大小是固定的,ArrayList
的大小是動(dòng)態(tài)變化的。(ArrayList
的擴(kuò)容是個(gè)常見面試題)
相比于Array
,ArrayList
有著更多的內(nèi)置方法,如addAll()
,removeAll()
。
對(duì)于基本類型數(shù)據(jù),ArrayList
使用自動(dòng)裝箱來減少編碼工作量;而當(dāng)處理固定大小的基本數(shù)據(jù)類型的時(shí)候,這種方式相對(duì)比較慢,這時(shí)候應(yīng)該使用Array
。
comparable
接口出自java.lang
包,可以理解為一個(gè)內(nèi)比較器,因?yàn)閷?shí)現(xiàn)了Comparable
接口的類可以和自己比較,要和其他實(shí)現(xiàn)了Comparable
接口類比較,可以使用compareTo(Object obj)
方法。compareTo
方法的返回值是int
,有三種情況:
返回正整數(shù)(比較者大于被比較者)
返回0(比較者等于被比較者)
返回負(fù)整數(shù)(比較者小于被比較者)
comparator
接口出自 java.util
包,它有一個(gè)compare(Object obj1, Object obj2)
方法用來排序,返回值同樣是int
,有三種情況,和compareTo
類似。
它們之間的區(qū)別:很多包裝類都實(shí)現(xiàn)了comparable
接口,像Integer
、String
等,所以直接調(diào)用Collections.sort()
直接可以使用。如果對(duì)類里面自帶的自然排序不滿意,而又不能修改其源代碼的情況下,使用Comparator
就比較合適。此外使用Comparator
可以避免添加額外的代碼與我們的目標(biāo)類耦合,同時(shí)可以定義多種排序規(guī)則,這一點(diǎn)是Comparable
接口沒法做到的,從靈活性和擴(kuò)展性講Comparator更優(yōu),故在面對(duì)自定義排序的需求時(shí),可以優(yōu)先考慮使用Comparator
接口。
Collection
是一個(gè)集合接口。它提供了對(duì)集合對(duì)象進(jìn)行基本操作的通用接口方法。
Collections
是一個(gè)包裝類。它包含有各種有關(guān)集合操作的靜態(tài)多態(tài)方法,例如常用的sort()
方法。此類不能實(shí)例化,就像一個(gè)工具類,服務(wù)于Java的Collection
框架。
先說一下常見的元素在內(nèi)存中的存儲(chǔ)方式,主要有兩種:
順序存儲(chǔ)(Random Access):相鄰的數(shù)據(jù)元素在內(nèi)存中的位置也是相鄰的,可以根據(jù)元素的位置(如ArrayList
中的下表)讀取元素。
鏈?zhǔn)酱鎯?chǔ)(Sequential Access):每個(gè)數(shù)據(jù)元素包含它下一個(gè)元素的內(nèi)存地址,在內(nèi)存中不要求相鄰。例如LinkedList
。
主要的遍歷方式主要有三種:
for
循環(huán)遍歷:遍歷者自己在集合外部維護(hù)一個(gè)計(jì)數(shù)器,依次讀取每一個(gè)位置的元素。
Iterator
遍歷:基于順序存儲(chǔ)集合的Iterator
可以直接按位置訪問數(shù)據(jù)?;阪?zhǔn)酱鎯?chǔ)集合的Iterator
,需要保存當(dāng)前遍歷的位置,然后根據(jù)當(dāng)前位置來向前或者向后移動(dòng)指針。
foreach
遍歷:foreach
內(nèi)部也是采用了Iterator
的方式實(shí)現(xiàn),但使用時(shí)不需要顯示地聲明Iterator
。
那么對(duì)于以上三種遍歷方式應(yīng)該如何選取呢?
在Java集合框架中,提供了一個(gè)RandomAccess
接口,該接口沒有方法,只是一個(gè)標(biāo)記。通常用來標(biāo)記List
的實(shí)現(xiàn)是否支持RandomAccess
。所以在遍歷時(shí),可以先判斷是否支持RandomAccess
(list instanceof RandomAccess
),如果支持可用 for
循環(huán)遍歷,否則建議用Iterator
或 foreach
遍歷。
先說下結(jié)論,一般面試時(shí)需要記住,
ArrayList
的初始容量為10,擴(kuò)容時(shí)對(duì)是舊的容量值加上舊的容量數(shù)值進(jìn)行右移一位(位運(yùn)算,相當(dāng)于除以2,位運(yùn)算的效率更高),所以每次擴(kuò)容都是舊的容量的1.5倍。
具體的實(shí)現(xiàn)大家可查看下ArrayList
的源碼。
是否線程安全:ArrayList
和LinkedList
都是不保證線程安全的
底層實(shí)現(xiàn):ArrayList
的底層實(shí)現(xiàn)是數(shù)組,LinkedList
的底層是雙向鏈表。
內(nèi)存占用:ArrayList
會(huì)存在一定的空間浪費(fèi),因?yàn)槊看螖U(kuò)容都是之前的1.5倍,而LinkedList
中的每個(gè)元素要存放直接后繼和直接前驅(qū)以及數(shù)據(jù),所以對(duì)于每個(gè)元素的存儲(chǔ)都要比ArrayList
花費(fèi)更多的空間。
應(yīng)用場(chǎng)景:ArrayList
的底層數(shù)據(jù)結(jié)構(gòu)是數(shù)組,所以在插入和刪除元素時(shí)的時(shí)間復(fù)雜度都會(huì)收到位置的影響,平均時(shí)間復(fù)雜度為o(n),在讀取元素的時(shí)候可以根據(jù)下標(biāo)直接查找到元素,不受位置的影響,平均時(shí)間復(fù)雜度為o(1),所以ArrayList
更加適用于多讀,少增刪的場(chǎng)景。LinkedList
的底層數(shù)據(jù)結(jié)構(gòu)是雙向鏈表,所以插入和刪除元素不受位置的影響,平均時(shí)間復(fù)雜度為o(1),如果是在指定位置插入則是o(n),因?yàn)樵诓迦胫靶枰日业皆撐恢?,讀取元素的平均時(shí)間復(fù)雜度為o(n)。所以LinkedList
更加適用于多增刪,少讀寫的場(chǎng)景。
相同點(diǎn)
都實(shí)現(xiàn)了List
接口
底層數(shù)據(jù)結(jié)構(gòu)都是數(shù)組
不同點(diǎn)
線程安全:Vector
使用了Synchronized
來實(shí)現(xiàn)線程同步,所以是線程安全的,而ArrayList
是線程不安全的。
性能:由于Vector
使用了Synchronized
進(jìn)行加鎖,所以性能不如ArrayList
。
擴(kuò)容:ArrayList
和Vector
都會(huì)根據(jù)需要?jiǎng)討B(tài)的調(diào)整容量,但是ArrayList
每次擴(kuò)容為舊容量的1.5倍,而Vector
每次擴(kuò)容為舊容量的2倍。
ArrayList
底層數(shù)據(jù)結(jié)構(gòu)為數(shù)組,對(duì)元素的讀取速度快,而增刪數(shù)據(jù)慢,線程不安全。
LinkedList
底層為雙向鏈表,對(duì)元素的增刪數(shù)據(jù)快,讀取慢,線程不安全。
Vector
的底層數(shù)據(jù)結(jié)構(gòu)為數(shù)組,用Synchronized
來保證線程安全,性能較差,但線程安全。
HashSet
的底層是HashMap
,默認(rèn)構(gòu)造函數(shù)是構(gòu)建一個(gè)初始容量為16,負(fù)載因子為0.75 的HashMap
。HashSet
的值存放于HashMap
的key
上,HashMap
的value
統(tǒng)一為PRESENT
。
這里面涉及到了HasCode()
和equals()
兩個(gè)方法。
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
方法首先比較的是內(nèi)存地址,如果內(nèi)存地址相同,直接返回true
;如果內(nèi)存地址不同,再比較對(duì)象的類型,類型不同直接返回false
;類型相同,再比較值是否相同;值相同返回true
,值不同返回false
??偨Y(jié)一下,equals
會(huì)比較內(nèi)存地址、對(duì)象類型、以及值,內(nèi)存地址相同,equals
一定返回true
;對(duì)象類型和值相同,equals
方法一定返回true
。
如果沒有重寫equals
方法,那么equals
和==
的作用相同,比較的是對(duì)象的地址值。
hashCode
hashCode
方法返回對(duì)象的散列碼,返回值是int
類型的散列碼。散列碼的作用是確定該對(duì)象在哈希表中的索引位置。
關(guān)于hashCode
有一些約定:
兩個(gè)對(duì)象相等,則hashCode
一定相同。
兩個(gè)對(duì)象有相同的hashCode
值,它們不一定相等。
hashCode()
方法默認(rèn)是對(duì)堆上的對(duì)象產(chǎn)生獨(dú)特值,如果沒有重寫hashCode()
方法,則該類的兩個(gè)對(duì)象的hashCode
值肯定不同
介紹完equals()方法和hashCode()方法,繼續(xù)說下HashSet是如何檢查重復(fù)的。
HashSet
的特點(diǎn)是存儲(chǔ)元素時(shí)無序且唯一,在向HashSet
中添加對(duì)象時(shí),首相會(huì)計(jì)算對(duì)象的HashCode
值來確定對(duì)象的存儲(chǔ)位置,如果該位置沒有其他對(duì)象,直接將該對(duì)象添加到該位置;如果該存儲(chǔ)位置有存儲(chǔ)其他對(duì)象(新添加的對(duì)象和該存儲(chǔ)位置的對(duì)象的HashCode
值相同),調(diào)用equals
方法判斷兩個(gè)對(duì)象是否相同,如果相同,則添加對(duì)象失敗,如果不相同,則會(huì)將該對(duì)象重新散列到其他位置。
HashMap | HashSet |
---|---|
實(shí)現(xiàn)了Map 接口 | 實(shí)現(xiàn)了Set 接口 |
存儲(chǔ)鍵值對(duì) | 存儲(chǔ)對(duì)象 |
key 唯一,value 不唯一 | 存儲(chǔ)對(duì)象唯一 |
HashMap 使用鍵(Key )計(jì)算Hashcode | HashSet 使用成員對(duì)象來計(jì)算hashcode 值 |
速度相對(duì)較快 | 速度相對(duì)較慢 |
JDK1.7的底層數(shù)據(jù)結(jié)構(gòu)(數(shù)組+鏈表)
JDK1.8的底層數(shù)據(jù)結(jié)構(gòu)(數(shù)組+鏈表)
JDK1.7的Hash函數(shù)
static final int hash(int h){ h ^= (h >>> 20) ^ (h >>>12); return h^(h >>> 7) ^ (h >>> 4); }
JDK1.8的Hash函數(shù)
static final int hash(Onject key){ int h; return (key == null) ? 0 : (h = key.hashCode())^(h >>> 16); }
JDK1.8的函數(shù)經(jīng)過了一次異或一次位運(yùn)算一共兩次擾動(dòng),而JDK1.7經(jīng)過了四次位運(yùn)算五次異或一共九次擾動(dòng)。這里簡單解釋下JDK1.8的hash函數(shù),面試經(jīng)常問這個(gè),兩次擾動(dòng)分別是key.hashCode()
與key.hashCode()
右移16位進(jìn)行異或。這樣做的目的是,高16位不變,低16位與高16位進(jìn)行異或操作,進(jìn)而減少碰撞的發(fā)生,高低Bit都參與到Hash的計(jì)算。如何不進(jìn)行擾動(dòng)處理,因?yàn)閔ash值有32位,直接對(duì)數(shù)組的長度求余,起作用只是hash值的幾個(gè)低位。
HashMap在JDK1.7和JDK1.8中有哪些不同點(diǎn):
JDK1.7 | JDK1.8 | JDK1.8的優(yōu)勢(shì) | |
---|---|---|---|
底層結(jié)構(gòu) | 數(shù)組+鏈表 | 數(shù)組+鏈表/紅黑樹(鏈表大于8) | 避免單條鏈表過長而影響查詢效率,提高查詢效率 |
hash值計(jì)算方式 | 9次擾動(dòng) = 4次位運(yùn)算 + 5次異或運(yùn)算 | 2次擾動(dòng) = 1次位運(yùn)算 + 1次異或運(yùn)算 | 可以均勻地把之前的沖突的節(jié)點(diǎn)分散到新的桶(具體細(xì)節(jié)見下面擴(kuò)容部分) |
插入數(shù)據(jù)方式 | 頭插法(先講原位置的數(shù)據(jù)移到后1位,再插入數(shù)據(jù)到該位置) | 尾插法(直接插入到鏈表尾部/紅黑樹) | 解決多線程造成死循環(huán)地問題 |
擴(kuò)容后存儲(chǔ)位置的計(jì)算方式 | 重新進(jìn)行hash計(jì)算 | 原位置或原位置+舊容量 | 省去了重新計(jì)算hash值的時(shí)間 |
因?yàn)?code>HashMap是通過key
的hash值來確定存儲(chǔ)的位置,但Hash值的范圍是-2147483648到2147483647,不可能建立一個(gè)這么大的數(shù)組來覆蓋所有hash值。所以在計(jì)算完hash值后會(huì)對(duì)數(shù)組的長度進(jìn)行取余操作,如果數(shù)組的長度是2的冪次方,(length - 1)&hash
等同于hash%length
,可以用(length - 1)&hash
這種位運(yùn)算來代替%取余的操作進(jìn)而提高性能。
HashMap的主要流程可以看下面這個(gè)流程圖,邏輯非常清晰。
初始值為16,負(fù)載因子為0.75,閾值為負(fù)載因子*容量
resize()
方法是在hashmap
中的鍵值對(duì)大于閥值時(shí)或者初始化時(shí),就調(diào)用resize()
方法進(jìn)行擴(kuò)容。
每次擴(kuò)容,容量都是之前的兩倍
擴(kuò)容時(shí)有個(gè)判斷e.hash & oldCap
是否為零,也就是相當(dāng)于hash值對(duì)數(shù)組長度的取余操作,若等于0,則位置不變,若等于1,位置變?yōu)樵恢眉优f容量。
源碼如下:
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) { //如果舊容量已經(jīng)超過最大值,閾值為整數(shù)最大值 threshold = Integer.MAX_VALUE; return oldTab; }else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; //沒有超過最大值就變?yōu)樵瓉淼?倍 } 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 代表擴(kuò)容后在原位置 Node<K,V> hiHead = null, hiTail = null;//hiHead,hiTail 代表擴(kuò)容后在原位置+舊容量 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; }
這個(gè)主要是考慮空間利用率和查詢成本的一個(gè)折中。如果加載因子過高,空間利用率提高,但是會(huì)使得哈希沖突的概率增加;如果加載因子過低,會(huì)頻繁擴(kuò)容,哈希沖突概率降低,但是會(huì)使得空間利用率變低。具體為什么是0.75,不是0.74或0.76,這是一個(gè)基于數(shù)學(xué)分析(泊松分布)和行業(yè)規(guī)定一起得到的一個(gè)結(jié)論。
可能有很多人會(huì)問,既然紅黑樹性能這么好,為什么不一開始直接使用紅黑樹,而是先用鏈表,鏈表長度大于8時(shí),才轉(zhuǎn)換為紅紅黑樹。
因?yàn)榧t黑樹的節(jié)點(diǎn)所占的空間是普通鏈表節(jié)點(diǎn)的兩倍,但查找的時(shí)間復(fù)雜度低,所以只有當(dāng)節(jié)點(diǎn)特別多時(shí),紅黑樹的優(yōu)點(diǎn)才能體現(xiàn)出來。至于為什么是8,是通過數(shù)據(jù)分析統(tǒng)計(jì)出來的一個(gè)結(jié)果,鏈表長度到達(dá)8的概率是很低的,綜合鏈表和紅黑樹的性能優(yōu)缺點(diǎn)考慮將大于8的鏈表轉(zhuǎn)化為紅黑樹。
鏈表轉(zhuǎn)化為紅黑樹除了鏈表長度大于8,還要HashMap
中的數(shù)組長度大于64。也就是如果HashMap
長度小于64,鏈表長度大于8是不會(huì)轉(zhuǎn)化為紅黑樹的,而是直接擴(kuò)容。
哈希沖突:hashMap
在存儲(chǔ)元素時(shí)會(huì)先計(jì)算key
的hash值來確定存儲(chǔ)位置,因?yàn)?code>key的hash值計(jì)算最后有個(gè)對(duì)數(shù)組長度取余的操作,所以即使不同的key
也可能計(jì)算出相同的hash值,這樣就引起了hash沖突。hashMap
的底層結(jié)構(gòu)中的鏈表/紅黑樹就是用來解決這個(gè)問題的。
HashMap
中的哈希沖突解決方式可以主要從三方面考慮(以JDK1.8為背景)
拉鏈法
HasMap
中的數(shù)據(jù)結(jié)構(gòu)為數(shù)組+鏈表/紅黑樹,當(dāng)不同的key
計(jì)算出的hash值相同時(shí),就用鏈表的形式將Node結(jié)點(diǎn)(沖突的key
及key
對(duì)應(yīng)的value
)掛在數(shù)組后面。
hash函數(shù)
key
的hash值經(jīng)過兩次擾動(dòng),key
的hashCode
值與key
的hashCode
值的右移16位進(jìn)行異或,然后對(duì)數(shù)組的長度取余(實(shí)際為了提高性能用的是位運(yùn)算,但目的和取余一樣),這樣做可以讓hashCode
取值出的高位也參與運(yùn)算,進(jìn)一步降低hash沖突的概率,使得數(shù)據(jù)分布更平均。
紅黑樹
在拉鏈法中,如果hash沖突特別嚴(yán)重,則會(huì)導(dǎo)致數(shù)組上掛的鏈表長度過長,性能變差,因此在鏈表長度大于8時(shí),將鏈表轉(zhuǎn)化為紅黑樹,可以提高遍歷鏈表的速度。
hashCode()
處理后的哈希值范圍太大,不可能在內(nèi)存建立這么大的數(shù)組。
可以,但要注意以下兩點(diǎn):
如果類重寫了 equals()
方法,也應(yīng)該重寫hashCode()
方法。
最好定義key
類是不可變的,這樣key
對(duì)應(yīng)的hashCode()
值可以被緩存起來,性能更好,這也是為什么String
特別適合作為HashMap
的key
。
這些包裝類都是final
修飾,是不可變性的, 保證了key
的不可更改性,不會(huì)出現(xiàn)放入和獲取時(shí)哈希值不同的情況。
它們內(nèi)部已經(jīng)重寫過hashcode()
,equal()
等方法。
重寫hashCode()
方法,因?yàn)樾枰?jì)算hash值確定存儲(chǔ)位置
重寫equals()
方法,因?yàn)樾枰WCkey
的唯一性。
由于JDK1.7的
hashMap
遇到hash沖突采用的是頭插法,在多線程情況下會(huì)存在死循環(huán)問題,但JDK1.8已經(jīng)改成了尾插法,不存在這個(gè)問題了。但需要注意的是JDK1.8中的HashMap
仍然是不安全的,在多線程情況下使用仍然會(huì)出現(xiàn)線程安全問題?;旧厦嬖嚂r(shí)說到這里既可以了,具體流程用口述是很難說清的,感興趣的可以看這篇文章。HASHMAP的死循環(huán)
JDK1.7
在JDK1.7中,ConcurrentHashMap
采用Segment
數(shù)組 + HashEntry
數(shù)組的方式進(jìn)行實(shí)現(xiàn)。Segment
實(shí)現(xiàn)了ReentrantLock
,所以Segment
有鎖的性質(zhì),HashEntry
用于存儲(chǔ)鍵值對(duì)。一個(gè)ConcurrentHashMap
包含著一個(gè)Segment
數(shù)組,一個(gè)Segment
包含著一個(gè)HashEntry
數(shù)組,HashEntry
是一個(gè)鏈表結(jié)構(gòu),如果要獲取HashEntry
中的元素,要先獲得Segment
的鎖。
JDK1.8
在JDK1.8中,不在是Segment
+HashEntry
的結(jié)構(gòu)了,而是和HashMap
類似的結(jié)構(gòu),Node數(shù)組+鏈表/紅黑樹,采用CAS
+synchronized
來保證線程安全。當(dāng)鏈表長度大于8,鏈表轉(zhuǎn)化為紅黑樹。在JDK1.8中synchronized
只鎖鏈表或紅黑樹的頭節(jié)點(diǎn),是一種相比于segment
更為細(xì)粒度的鎖,鎖的競爭變小,所以效率更高。
總結(jié)一下:
JDK1.7底層是ReentrantLock
+Segment
+HashEntry
,JDK1.8底層是synchronized
+CAS
+鏈表/紅黑樹
JDK1.7采用的是分段鎖,同時(shí)鎖住幾個(gè)HashEntry
,JDK1.8鎖的是Node節(jié)點(diǎn),只要沒有發(fā)生哈希沖突,就不會(huì)產(chǎn)生鎖的競爭。所以JDK1.8相比于JDK1.7提供了一種粒度更小的鎖,減少了鎖的競爭,提高了ConcurrentHashMap
的并發(fā)能力。
HashTable
的底層數(shù)據(jù)結(jié)構(gòu)是數(shù)組+鏈表,鏈表主要是為了解決哈希沖突,并且整個(gè)數(shù)組都是synchronized
修飾的,所以HashTable
是線程安全的,但鎖的粒度太大,鎖的競爭非常激烈,效率很低。
HashMap(JDK1.8) | ConcurrentHashMap(JDK1.8) | Hashtable | |
---|---|---|---|
底層實(shí)現(xiàn) | 數(shù)組+鏈表/紅黑樹 | 數(shù)組+鏈表/紅黑樹 | 數(shù)組+鏈表 |
線程安全 | 不安全 | 安全(Synchronized 修飾Node節(jié)點(diǎn)) | 安全(Synchronized 修飾整個(gè)表) |
效率 | 高 | 較高 | 低 |
擴(kuò)容 | 初始16,每次擴(kuò)容成2n | 初始16,每次擴(kuò)容成2n | 初始11,每次擴(kuò)容成2n+1 |
是否支持Null key和Null Value | 可以有一個(gè)Null key,Null Value多個(gè) | 不支持 | 不支持 |
這些常用方法是需要背下來的,雖然面試用不上,但是筆試或者面試寫算法題時(shí)會(huì)經(jīng)常用到。
方法 | |
---|---|
booean add(E e) | 在集合末尾添加元素 |
boolean remove(Object o) | 若本類集中有值與o的值相等的元素,移除該元素并返回true |
void clear() | 清除本類中所有元素 |
boolean contains(Object o) | 判斷集合中是否包含該元素 |
boolean isEmpty() | 判斷集合是否為空 |
int size() | 返回集合中元素的個(gè)數(shù) |
boolean addAll(Collection c) | 將一個(gè)集合中c中的所有元素添加到另一個(gè)集合中 |
Object[] toArray() | 返回一個(gè)包含本集所有元素的數(shù)組,數(shù)組類型為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) | 返回指定元素在集合中第一次出現(xiàn)的索引 |
Object get(int index) | 返回指定位置的元素 |
List subList(int fromIndex,int toIndex) | 截取集合(左閉右開) |
方法 | |
---|---|
addFirst() | 在頭部添加元素 |
addLast() | 在尾部添加元素 |
removeFirst() | 在頭部刪除元素 |
removeLat() | 在尾部刪除元素 |
getFirst() | 獲取頭部元素 |
getLast() | 獲取尾部元素 |
方法 | |
---|---|
void clear() | 清除集合內(nèi)的元素 |
boolean containsKey(Object key) | 查詢Map中是否包含指定key,如果包含則返回true |
Set entrySet() | 返回Map中所包含的鍵值對(duì)所組成的Set集合,每個(gè)集合元素都是Map.Entry的對(duì)象 |
Object get(Object key) | 返回key指定的value,若Map中不包含key返回null |
boolean isEmpty() | 查詢Map是否為空,若為空返回true |
Set keySet() | 返回Map中所有key所組成的集合 |
Object put(Object key,Object value) | 添加一個(gè)鍵值對(duì),如果已有一個(gè)相同的key,則新的鍵值對(duì)會(huì)覆蓋舊的鍵值對(duì),返回值為覆蓋前的value值,否則為null |
void putAll(Map m) | 將制定Map中的鍵值對(duì)復(fù)制到Map中 |
Object remove(Object key) | 刪除指定key所對(duì)應(yīng)的鍵值對(duì),返回所關(guān)聯(lián)的value,如果key不存在返回null |
int size() | 返回Map里面的鍵值對(duì)的個(gè)數(shù) |
Collection values() | 返回Map里所有values所組成的Collection |
boolean containsValue ( Object value) | 判斷映像中是否存在值 value |
方法 | |
---|---|
boolean empty() | 測(cè)試堆棧是否為空。 |
E peek() | 查看堆棧頂部的對(duì)象,但不從堆棧中移除它。 |
E pop() | 移除堆棧頂部的對(duì)象,并作為此函數(shù)的值返回該對(duì)象。 |
E push(E item) | 把項(xiàng)壓入堆棧頂部。 |
int search(Object o) | 返回對(duì)象在堆棧中的位置,以 1 為基數(shù)。 |
方法 | |
---|---|
boolean add(E e) | 將指定元素插入到隊(duì)列的尾部(隊(duì)列滿了話,會(huì)拋出異常) |
boolean offer(E e) | 將指定元素插入此隊(duì)列的尾部(隊(duì)列滿了話,會(huì)返回false) |
E remove() | 返回取隊(duì)列頭部的元素,并刪除該元素(如果隊(duì)列為空,則拋出異常) |
E poll() | 返回隊(duì)列頭部的元素,并刪除該元素(如果隊(duì)列為空,則返回null) |
E element() | 返回隊(duì)列頭部的元素,不刪除該元素(如果隊(duì)列為空,則拋出異常) |
E peek() | 返回隊(duì)列頭部的元素,不刪除該元素(如果隊(duì)列為空,則返回null) |
到此,相信大家對(duì)“有哪些Java集合面試題”有了更深的了解,不妨來實(shí)際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!
分享名稱:有哪些Java集合面試題
網(wǎng)站路徑:http://sd-ha.com/article28/popsjp.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站改版、網(wǎng)站維護(hù)、企業(yè)建站、域名注冊(cè)、全網(wǎng)營銷推廣、微信小程序
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)