Java面試題-容器
1. 說說常見的集合有哪些?
- Collection
- List
ArrayList
LinkedList
Vector
Stack - Set
HashSet
LinkedHashSet
TreeSet - Map
HashMap
LinkedHashMap
TreeMap
ConcurrentHashMap
Hashtable
2. 哪些集合類可對元素隨機訪問?
ArrayList、HashMap、TreeMap、Hashtable 類提供對元素的隨機訪問。
3. Comparable 和 Comparator 介面的區別?
- Comparable 和 Comparator 介面用來對物件集合或者陣列進行排序;
- Comparable 介面用來提供物件的自然排序,我們可以使用它來提供基於單個邏輯的排序;
- Comparator 用來提供不同的排序演算法,我們可以選擇需要使用的 Comparator 來對給定的物件集合進行排序;
4. Collection 和 Collections的區別?
- Collection 是一個集合介面,它提供了對集合物件進行基本操作的通用介面方法,所有集合都是它的子類,比如 List、Set 等。
- Collections 是一個包裝類,包含了很多靜態方法,不能被例項化,就像一個工具類,比如提供的排序方法: Collections. sort(list)。
5. Enumeration 和 Iterator 介面的區別?
- Enumeration 只能讀取集合的資料,而不能對資料進行修改,Iterator除了能讀取集合的資料之外,還能刪除集合中的資料;
- 與 Enumeration 相比,Iterator更加安全,因為當一個集合正在遍歷的時候,它會阻止其他執行緒去修改集合(fail-fast機制);
6. 集合使用範型有什麼優點?
- 範型規定了一個集合中可以容納的物件型別,新增其他型別的物件將編譯失敗;
- 避免了執行時可能出現的 ClassCastException 異常;
- 範型也使得程式碼整潔,我們不需要使用顯式轉換和instanceOf操作符;
- 它也給執行時帶來了好處,因為不會產生型別檢查的位元組碼指令;
7. List、Set、Map 之間的區別是什麼?
8. 為什麼 Map 介面不繼承 Collection 介面?
儘管 Map 介面和它的實現也是集合框架的一部分,但 Map 不是集合,集合也不是 Map ,因此 Map 繼承 Collection 是毫無意義的。
如果 Map 繼承 Collection 介面,那麼元素去哪兒?Map 包含 key-value 對,它提供抽取 key 或 value 列表集合的方法,但是它不適合“一組物件”規範。
9. 常用的執行緒安全的 Map 有哪些?
- Hashtable
Hashtable的 get/put 方法都被 synchronized 修飾,說明他們是方法級別阻塞的,他們佔用共享資源鎖,效率低,不推薦使用; - SynchronizedMap
使用 Collections 工具類創建出來的同步集合,通過物件鎖實現,每次呼叫方法必須先獲取物件鎖,效率低,不推薦使用; - ConcurrentHashMap
JKD1.7 之前使用分段鎖方法,分成 16 個桶,每次只加鎖其中一個桶,而在 JDK1.8 中又加入了紅黑樹和 CAS 演算法,同步效率很高,推薦使用;
10. HashMap 與 Hashtable 的區別?
- 儲存:HashMap 允許一個 key 和 多個 value 為 null,而 Hashtable 不允許。
- 執行緒安全:Hashtable 是執行緒安全的,而 HashMap 是非執行緒安全的。
- 推薦使用:在 Hashtable 的類註釋可以看到,Hashtable 是保留類不建議使用,推薦在單執行緒環境下使用 HashMap 替代,如果需要多執行緒使用則用 ConcurrentHashMap 替代。
11. HashMap 與 TreeMap 怎麼選?
- 對於在 Map 中插入、刪除、定位一個元素這類操作,HashMap 是最好的 選擇,因為相對而言 HashMap 的插入會更快;
- 如果你要對一個 key 集合進行有序的遍歷,那 TreeMap 是更好的選擇;
12. HashMap 的資料結構是什麼?
- JDK 1.7:資料 + 連結串列
- JDK 1.8:資料 + 連結串列 + 紅黑樹(如果陣列的長度大於 64 並且連結串列的長度大於 8 將轉換為紅黑樹)
13. HashMap 在 JDK 8 中有哪些改變?
- 在 JDK 1.8 中,如果連結串列長度超過了 8,那麼連結串列將轉換為紅黑樹(桶陣列長度必須大於 64,小於 64 只會擴容);
- 發生 hash 碰撞時,JDK 1.7 會在連結串列的頭部插入,而 JDK 1.8 會在連結串列的尾部插入;
- 在 JDK 1.8 中,Entry 被 Node 替代;
14. HashMap 的 put 方法邏輯?
- 通過hash()函式對key進行hash運算得到當前key的hash值;
- 通過indexFor(hash, table.length)函式獲取在table中的實際位置;
- 如果當前位置為空,則建立新的Entry物件並插入實際位置;
- 如果當前位置不為空,則遍歷當前連結串列,如果對應資料已經存在則覆蓋如果對應資料不存在則建立新的Entry物件並將當前連結串列最後一個元素的next指標指向新建立的物件;
原始碼:
public V put(K key, V value) {
//如果table陣列為空陣列{},進行陣列填充(為table分配實際記憶體空間),入參為threshold,此時threshold為initialCapacity 預設是1<<4(24=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key為null,儲存位置為table[0]或table[0]的衝突鏈上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//對key的hashcode進一步計算,確保雜湊均勻
int i = indexFor(hash, table.length);//獲取在table中的實際位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果該對應資料已存在,執行覆蓋操作。用新value替換舊value,並返回舊value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;//保證併發訪問時,若HashMap內部結構發生變化,快速響應失敗
addEntry(hash, key, value, i);//新增一個entry
return null;
}
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);//位運算
}
put方法流程給圖:
15. HashMap 的 get 方法邏輯?
- 通過hash()函式對key進行hash運算得到當前key的hash值;
- 通過indexFor(hash, table.length)函式獲取在table中的實際位置;
- 比較引數key的hash值和當前位置元素的hash值及key的內容是否相等,如果相等則直接返回該元素,否則遍歷當前連結串列。
原始碼:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {//獲取的下標位置不為空
if (first.hash == hash && //總是檢查第一個元素是否相等
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {//如果該連結串列有下一個節點
if (first instanceof TreeNode)//如果是紅黑樹,則呼叫getTreeNode
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {//遍歷當前連結串列獲取元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
16. HashMap 是執行緒安全的嗎?
- 當用在方法內部的區域性變數時,區域性變數屬於當前執行緒級別的變數,其他執行緒訪問不了,所以不存線上程安全問題;
- 如果是成員變數,則不是執行緒安全的,兩條執行緒的 put 操作可能發生覆蓋(當兩個值的 hashCode 相同時);
17. HashMap 是怎麼解決 hash 衝突的?
HashMap 採用了一種連結串列資料結構來解決 hash 衝突的情況,當兩個物件的 hashCode 相同時,它們會放到當前陣列索引位置的連結串列中。
18. HashMap 是怎麼擴容的?
- table 陣列的大小是由 capacity 這個引數確定的,預設是 16,也可以構造傳入,最大限制是 1 << 30;
- loadFactor 是裝載因子,主要目的是用來確認 table 陣列是否需要動態擴充套件,預設值是 0.75,如 table 陣列大小為16,裝載因子為 0.75 時, threshould 就是 12,當 table 的實際大小超過 12 時,table 就需要動態擴容;
- 擴容時,呼叫 resize() 方法,將 table 長度變為原來的兩倍(注意是 table 長度,而不是 threshold);
- 如果資料很大的情況下,擴充套件時將會帶來效能的損失,在效能要求很高的地方,這種損失很可能是致命的;
原始碼:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//當size超過臨界閾值threshold,並且即將發生雜湊衝突時進行擴容
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {//for迴圈中的程式碼,逐個遍歷連結串列,重新計算索引位置,將老陣列資料複製到新陣列中去
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];//將當前entry的next鏈指向新的索引位置,newTable[i]有可能為空,有可能也是個entry鏈,如果是entry鏈,直接在連結串列頭部插入。
newTable[i] = e;
e = next;
}
}
}
19. 為何HashMap的陣列長度一定是2的次冪?
- 獲取元素下表位置的方式為:(n - 1) & hash,這個操作如果在 n 為 2 的 N 次冪的情況下是等同於 hash % n 取餘數的值;
- 至於為什麼要使用與(&)運算呢:因為與運算的效率要高於 hash % n 取餘的運算,這也就解釋了為什麼 HashMap 的陣列長度是 2 的 N 次冪;
20. HashMap 是如何實現同步的?
- 使用 Collections.synchronizedMap(...) 來同步 Map;
- 使用 ConcurrentHashMap;
21. Hashtable 為什麼不叫 HashTable?
Hashtable 實在 JDK 1.0 的時候建立的,而集合統一規範命名是在後來的 JDK 1.2 開始約定的,為了相容老版本所以就沒有改變;
22. ConcurrentHashMap 的資料結構?
- JDK 1.7 中,採用分段鎖機制,實現併發更新操作,底層採用 陣列 + 連結串列 的儲存結構,包括兩個核心靜態內部類 Segment 和 HashEntry。
1)Segment 繼承 ReentrantLock(可重入鎖)用來充當鎖的角色,每個Segment 物件守護每個散列表的若干個桶,Segment 陣列預設大小為 16,並且不會擴容。
2)HashEntry 用來封裝對映表的鍵-值對;
3)每個桶是由若干個 HashEntry 物件連結起來的連結串列; - JDK 1.8 中,採用 Node + CAS + Synchronized 來保證併發安全。取消類 Segment,直接用 table 陣列儲存鍵值對;當 Node 物件組成的連結串列長度超過 TREEIFY_THRESHOLD 時,連結串列轉換為紅黑樹,提升效能。底層變更為陣列 + 連結串列 + 紅黑樹。
23. ArrayList 是執行緒安全的嗎?
不是執行緒安全的,多執行緒操作時可能存在以下問題:
- 發生 ArrayIndexOutOfBoundsException異常;
- add 時程式正常執行,結果實際儲存的資料少於存入的資料;
24. 常用的執行緒安全的 List 集合有哪些?
- Vector
- SynchronizedList
- CopyOnWriteArrayList
CopyOnWriteArrayList 和 CopyOnWriteArraySet 是在 JDK 1.5 時加入的在 java.util.concurrent 包下。
25. 迴圈刪除 List 集合可能會發生什麼異常?
- ArrayIndexOutOfBoundsException:陣列下標越界異常。
- ConcurrentModificationException:使用增強 for 迴圈遍歷刪除時會報該異常,通過 Iterator 遍歷時則不會,因為取下個元素時會判斷要修改的數量和期待修改的數量是否一致,不一致會報錯,而迭代器的remove方法會同步該值。
26. ArrayList 和 LinkedList 的區別?
-
資料結構實現:ArrayList 是動態陣列的資料結構實現,而 LinkedList 是雙向連結串列的資料結構實現。
-
隨機訪問效率:ArrayList 比 LinkedList 在隨機訪問的時候效率要高,因為 LinkedList 是線性的資料儲存方式,所以需要移動指標從前往後依次查詢。
-
增加和刪除效率:在非首尾的增加和刪除操作,LinkedList 要比 ArrayList 效率要高,因為 ArrayList 增刪操作要影響陣列內的其他資料的下標。
綜合來說,在需要頻繁讀取集合中的元素時,更推薦使用 ArrayList,而在插入和刪除操作較多時,更推薦使用 LinkedList。
27. ArrayList 和 Vector 的區別?
- 執行緒安全:Vector 使用了 Synchronized 來實現執行緒同步,是執行緒安全的,而 ArrayList 是非執行緒安全的。
- 效能:ArrayList 在效能方面要優於 Vector。
- 擴容:ArrayList 和 Vector 都會根據實際的需要動態的調整容量,只不過在 Vector 擴容每次會增加 1 倍,而 ArrayList 只會增加 50%。
28.如何實現陣列和 List 之間的轉換?
- 陣列轉 List:使用 Arrays. asList(array) 進行轉換。
- List 轉陣列:使用 List 自帶的 toArray() 方法。
例如:
List<String> list = new ArrayList<>();
list.add("abc");
list.add("bcd");
Object[] objects = list.toArray();
for (Object object : objects) {
System.out.println(object);
}
List<String> list1 = Arrays.asList("abc", "bcd");
System.out.println(list1.toString());
29.Iterator 和 ListIterator 有什麼區別?
- Iterator 可以遍歷 Set 和 List 集合,而 ListIterator 只能遍歷 List。
- Iterator 只能單向遍歷,而 ListIterator 可以雙向遍歷(向前/後遍歷)。
- ListIterator 從 Iterator 介面繼承,然後添加了一些額外的功能,比如新增一個元素、替換一個元素、獲取前面或後面元素的索引位置。
30.怎麼確保一個集合不能被修改?
可以使用 Collections. unmodifiableCollection(Collection c) 方法來建立一個只讀集合,這樣改變集合的任何操作都會丟擲 Java. lang. UnsupportedOperationException 異常。
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 執行時此行報錯
System. out. println(list. size());