Java集合必會14問(精選面試題整理)


前言:把這段時間復習的關於集合類的東西整理出來,特別是HashMap相關的一些東西,之前都沒有很注意1.7 ->> 1.8的變化問題,但后來發現這其實變化挺大的,而且很多整理的面試資料都沒有更新(包括我之前整理的...)

1)說說常見的集合有哪些吧?

答:Map接口和Collection接口是所有集合框架的父接口:

  1. Collection接口的子接口包括:Set接口和List接口
  2. Map接口的實現類主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
  3. Set接口的實現類主要有:HashSet、TreeSet、LinkedHashSet等
  4. List接口的實現類主要有:ArrayList、LinkedList、Stack以及Vector等

2)HashMap與HashTable的區別?

答:

  1. HashMap沒有考慮同步,是線程不安全的;Hashtable使用了synchronized關鍵字,是線程安全的;

  2. HashMap允許K/V都為null;后者K/V都不允許為null;

  3. HashMap繼承自AbstractMap類;而Hashtable繼承自Dictionary類;


3)HashMap的put方法的具體流程?

圖引用自:https://blog.csdn.net/u011240877/article/details/53358305

答:下面先來分析一下源碼

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
			   boolean evict) {
	HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
	// 1.如果table為空或者長度為0,即沒有元素,那么使用resize()方法擴容
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length;
	// 2.計算插入存儲的數組索引i,此處計算方法同 1.7 中的indexFor()方法
	// 如果數組為空,即不存在Hash沖突,則直接插入數組
	if ((p = tab[i = (n - 1) & hash]) == null)
		tab[i] = newNode(hash, key, value, null);
	// 3.插入時,如果發生Hash沖突,則依次往下判斷
	else {
		HashMap.Node<K,V> e; K k;
		// a.判斷table[i]的元素的key是否與需要插入的key一樣,若相同則直接用新的value覆蓋掉舊的value
		// 判斷原則equals() - 所以需要當key的對象重寫該方法
		if (p.hash == hash &&
				((k = p.key) == key || (key != null && key.equals(k))))
			e = p;
		// b.繼續判斷:需要插入的數據結構是紅黑樹還是鏈表
		// 如果是紅黑樹,則直接在樹中插入 or 更新鍵值對
		else if (p instanceof HashMap.TreeNode)
			e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		// 如果是鏈表,則在鏈表中插入 or 更新鍵值對
		else {
			// i .遍歷table[i],判斷key是否已存在:采用equals對比當前遍歷結點的key與需要插入數據的key
			//    如果存在相同的,則直接覆蓋
			// ii.遍歷完畢后任務發現上述情況,則直接在鏈表尾部插入數據
			//    插入完成后判斷鏈表長度是否 > 8:若是,則把鏈表轉換成紅黑樹
			for (int binCount = 0; ; ++binCount) {
				if ((e = p.next) == null) {
					p.next = newNode(hash, key, value, null);
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
						treeifyBin(tab, hash);
					break;
				}
				if (e.hash == hash &&
						((k = e.key) == key || (key != null && key.equals(k))))
					break;
				p = e;
			}
		}
		// 對於i 情況的后續操作:發現key已存在,直接用新value覆蓋舊value&返回舊value
		if (e != null) { // existing mapping for key
			V oldValue = e.value;
			if (!onlyIfAbsent || oldValue == null)
				e.value = value;
			afterNodeAccess(e);
			return oldValue;
		}
	}
	++modCount;
	// 插入成功后,判斷實際存在的鍵值對數量size > 最大容量
	// 如果大於則進行擴容
	if (++size > threshold)
		resize();
	// 插入成功時會調用的方法(默認實現為空)
	afterNodeInsertion(evict);
	return null;
}

圖片簡單總結為:


4)HashMap的擴容操作是怎么實現的?

答:通過分析源碼我們知道了HashMap通過resize()方法進行擴容或者初始化的操作,下面是對源碼進行的一些簡單分析:

/**
 * 該函數有2中使用情況:1.初始化哈希表;2.當前數組容量過小,需要擴容
 */
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) {
		// 針對情況2:若擴容前的數組容量超過最大值,則不再擴容
		if (oldCap >= MAXIMUM_CAPACITY) {
			threshold = Integer.MAX_VALUE;
			return oldTab;
		}
		// 針對情況2:若沒有超過最大值,就擴容為原來的2倍(左移1位)
		else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
				oldCap >= DEFAULT_INITIAL_CAPACITY)
			newThr = oldThr << 1; // double threshold
	}

	// 針對情況1:初始化哈希表(采用指定或者使用默認值的方式)
	else if (oldThr > 0) // initial capacity was placed in threshold
		newCap = oldThr;
	else {               // zero initial threshold signifies using defaults
		newCap = DEFAULT_INITIAL_CAPACITY;
		newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
	}

	// 計算新的resize上限
	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) {
		// 把每一個bucket都移動到新的bucket中去
		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 { // preserve order
					Node<K,V> loHead = null, loTail = null;
					Node<K,V> hiHead = null, hiTail = null;
					Node<K,V> next;
					do {
						next = e.next;
						if ((e.hash & oldCap) == 0) {
							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;
					}
					if (hiTail != null) {
						hiTail.next = null;
						newTab[j + oldCap] = hiHead;
					}
				}
			}
		}
	}
	return newTab;
}

5)HashMap是怎么解決哈希沖突的?

參考資料:https://juejin.im/post/5ab99afff265da23a2291dee

答:在解決這個問題之前,我們首先需要知道什么是哈希沖突,而在了解哈希沖突之前我們還要知道什么是哈希才行;

什么是哈希?

Hash,一般翻譯為“散列”,也有直接音譯為“哈希”的,這就是把任意長度的輸入通過散列算法,變換成固定長度的輸出,該輸出就是散列值(哈希值);這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出,所以不可能從散列值來唯一的確定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。

所有散列函數都有如下一個基本特性:根據同一散列函數計算出的散列值如果不同,那么輸入值肯定也不同。但是,根據同一散列函數計算出的散列值如果相同,輸入值不一定相同。

什么是哈希沖突?

當兩個不同的輸入值,根據同一散列函數計算出相同的散列值的現象,我們就把它叫做碰撞(哈希碰撞)。

HashMap的數據結構

在Java中,保存數據有兩種比較簡單的數據結構:數組和鏈表。數組的特點是:尋址容易,插入和刪除困難;鏈表的特點是:尋址困難,但插入和刪除容易;所以我們將數組和鏈表結合在一起,發揮兩者各自的優勢,使用一種叫做鏈地址法的方式可以解決哈希沖突:

這樣我們就可以將擁有相同哈希值的對象組織成一個鏈表放在hash值所對應的bucket下,但相比於hashCode返回的int類型,我們HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要遠小於int類型的范圍,所以我們如果只是單純的用hashCode取余來獲取對應的bucket這將會大大增加哈希碰撞的概率,並且最壞情況下還會將HashMap變成一個單鏈表,所以我們還需要對hashCode作一定的優化

hash()函數

上面提到的問題,主要是因為如果使用hashCode取余,那么相當於參與運算的只有hashCode的低位,高位是沒有起到任何作用的,所以我們的思路就是讓hashCode取值出的高位也參與運算,進一步降低hash碰撞的概率,使得數據分布更平均,我們把這樣的操作稱為擾動,在JDK 1.8中的hash()函數如下:

static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 與自己右移16位進行異或運算(高低位異或)
}

這比在JDK 1.7中,更為簡潔,相比在1.7中的4次位運算,5次異或運算(9次擾動),在1.8中,只進行了1次位運算和1次異或運算(2次擾動);

JDK1.8新增紅黑樹

通過上面的鏈地址法(使用散列表)擾動函數我們成功讓我們的數據分布更平均,哈希碰撞減少,但是當我們的HashMap中存在大量數據時,加入我們某個bucket下對應的鏈表有n個元素,那么遍歷時間復雜度就為O(n),為了針對這個問題,JDK1.8在HashMap中新增了紅黑樹的數據結構,進一步使得遍歷復雜度降低至O(logn);

總結

簡單總結一下HashMap是使用了哪些方法來有效解決哈希沖突的:

1. 使用鏈地址法(使用散列表)來鏈接擁有相同hash值的數據;
2. 使用2次擾動函數(hash函數)來降低哈希沖突的概率,使得數據分布更平均;
3. 引入紅黑樹進一步降低遍歷的時間復雜度,使得遍歷更快;


6)HashMap為什么不直接使用hashCode()處理后的哈希值直接作為table的下標?

答:hashCode()方法返回的是int整數類型,其范圍為-(2 ^ 31)~(2 ^ 31 - 1),約有40億個映射空間,而HashMap的容量范圍是在16(初始化默認值)~2 ^ 30,HashMap通常情況下是取不到最大值的,並且設備上也難以提供這么多的存儲空間,從而導致通過hashCode()計算出的哈希值可能不在數組大小范圍內,進而無法匹配存儲位置;

面試官:那怎么解決呢?

答:

  1. HashMap自己實現了自己的hash()方法,通過兩次擾動使得它自己的哈希值高低位自行進行異或運算,降低哈希碰撞概率也使得數據分布更平均;

  2. 在保證數組長度為2的冪次方的時候,使用hash()運算之后的值與運算(&)(數組長度 - 1)來獲取數組下標的方式進行存儲,這樣一來是比取余操作更加有效率,二來也是因為只有當數組長度為2的冪次方時,h&(length-1)才等價於h%length,三來解決了“哈希值與數組大小范圍不匹配”的問題;

面試官:為什么數組長度要保證為2的冪次方呢?

答:

  1. 只有當數組長度為2的冪次方時,h&(length-1)才等價於h%length,即實現了key的定位,2的冪次方也可以減少沖突次數,提高HashMap的查詢效率;

  2. 如果 length 為 2 的次冪 則 length-1 轉化為二進制必定是 11111……的形式,在於 h 的二進制與操作效率會非常的快,而且空間不浪費;如果 length 不是 2 的次冪,比如 length 為 15,則 length - 1 為 14,對應的二進制為 1110,在於 h 與操作,最后一位都為 0 ,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,數組可以使用的位置比數組長度小了很多,這意味着進一步增加了碰撞的幾率,減慢了查詢的效率!這樣就會造成空間的浪費。

面試官:那為什么是兩次擾動呢?

答:這樣就是加大哈希值低位的隨機性,使得分布更均勻,從而提高對應數組存儲下標位置的隨機性&均勻性,最終減少Hash沖突,兩次就夠了,已經達到了高位低位同時參與運算的目的;


7)HashMap在JDK1.7和JDK1.8中有哪些不同?

答:

不同 JDK 1.7 JDK 1.8
存儲結構 數組 + 鏈表 數組 + 鏈表 + 紅黑樹
初始化方式 單獨函數:inflateTable() 直接集成到了擴容函數resize()
hash值計算方式 擾動處理 = 9次擾動 = 4次位運算 + 5次異或運算 擾動處理 = 2次擾動 = 1次位運算 + 1次異或運算
存放數據的規則 無沖突時,存放數組;沖突時,存放鏈表 無沖突時,存放數組;沖突 & 鏈表長度 < 8:存放單鏈表;沖突 & 鏈表長度 > 8:樹化並存放紅黑樹
插入數據方式 頭插法(先講原位置的數據移到后1位,再插入數據到該位置) 尾插法(直接插入到鏈表尾部/紅黑樹)
擴容后存儲位置的計算方式 全部按照原來方法進行計算(即hashCode ->> 擾動函數 ->> (h&length-1)) 按照擴容后的規律計算(即擴容后的位置=原位置 or 原位置 + 舊容量)

8)為什么HashMap中String、Integer這樣的包裝類適合作為K?

答:String、Integer等包裝類的特性能夠保證Hash值的不可更改性和計算准確性,能夠有效的減少Hash碰撞的幾率

  1. 都是final類型,即不可變性,保證key的不可更改性,不會存在獲取hash值不同的情況
  2. 內部已重寫了equals()hashCode()等方法,遵守了HashMap內部的規范(不清楚可以去上面看看putValue的過程),不容易出現Hash值計算錯誤的情況;

面試官:如果我想要讓自己的Object作為K應該怎么辦呢?

答:重寫hashCode()equals()方法

  1. 重寫hashCode()是因為需要計算存儲數據的存儲位置,需要注意不要試圖從散列碼計算中排除掉一個對象的關鍵部分來提高性能,這樣雖然能更快但可能會導致更多的Hash碰撞;

  2. 重寫equals()方法,需要遵守自反性、對稱性、傳遞性、一致性以及對於任何非null的引用值x,x.equals(null)必須返回false的這幾個特性,目的是為了保證key在哈希表中的唯一性


9)ConcurrentHashMap和Hashtable的區別?

答:ConcurrentHashMap 結合了 HashMap 和 HashTable 二者的優勢。HashMap 沒有考慮同步,HashTable 考慮了同步的問題。但是 HashTable 在每次同步執行時都要鎖住整個結構。 ConcurrentHashMap 鎖的方式是稍微細粒度的。

面試官:ConcurrentHashMap的具體實現知道嗎?

參考資料:http://www.importnew.com/23610.html

答:在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式進行實現,結構如下:

  1. 該類包含兩個靜態內部類 HashEntry 和 Segment ;前者用來封裝映射表的鍵值對,后者用來充當鎖的角色;

  2. Segment 是一種可重入的鎖 ReentrantLock,每個 Segment 守護一個HashEntry 數組里得元素,當對 HashEntry 數組的數據進行修改時,必須首先獲得對應的 Segment 鎖。

JDK1.8中,放棄了Segment臃腫的設計,取而代之的是采用Node + CAS + Synchronized來保證並發安全進行實現,結構如下:

插入元素過程(建議去看看源碼):

  1. 如果相應位置的Node還沒有初始化,則調用CAS插入相應的數據;
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
        break;                   // no lock when adding to empty bin
}
  1. 如果相應位置的Node不為空,且當前該節點不處於移動狀態,則對該節點加synchronized鎖,如果該節點的hash不小於0,則遍歷鏈表更新節點或插入新節點;
if (fh >= 0) {
    binCount = 1;
    for (Node<K,V> e = f;; ++binCount) {
        K ek;
        if (e.hash == hash &&
            ((ek = e.key) == key ||
             (ek != null && key.equals(ek)))) {
            oldVal = e.val;
            if (!onlyIfAbsent)
                e.val = value;
            break;
        }
        Node<K,V> pred = e;
        if ((e = e.next) == null) {
            pred.next = new Node<K,V>(hash, key, value, null);
            break;
        }
    }
}
  1. 如果該節點是TreeBin類型的節點,說明是紅黑樹結構,則通過putTreeVal方法往紅黑樹中插入節點;如果binCount不為0,說明put操作對數據產生了影響,如果當前鏈表的個數達到8個,則通過treeifyBin方法轉化為紅黑樹,如果oldVal不為空,說明是一次更新操作,沒有對元素個數產生影響,則直接返回舊值;

  2. 如果插入的是一個新節點,則執行addCount()方法嘗試更新元素個數baseCount;


10)Java集合的快速失敗機制 “fail-fast”?

答:

是java集合的一種錯誤檢測機制,當多個線程對集合進行結構上的改變的操作時,有可能會產生 fail-fast 機制。

例如:假設存在兩個線程(線程1、線程2),線程1通過Iterator在遍歷集合A中的元素,在某個時候線程2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),那么這個時候程序就會拋出 ConcurrentModificationException 異常,從而產生fail-fast機制。

原因:迭代器在遍歷時直接訪問集合中的內容,並且在遍歷過程中使用一個 modCount 變量。集合在被遍歷期間如果內容發生變化,就會改變modCount的值。每當迭代器使用hashNext()/next()遍歷下一個元素之前,都會檢測modCount變量是否為expectedmodCount值,是的話就返回遍歷;否則拋出異常,終止遍歷。

解決辦法:

1. 在遍歷過程中,所有涉及到改變modCount值得地方全部加上synchronized。

2. 使用CopyOnWriteArrayList來替換ArrayList


11)ArrayList 和 Vector 的區別?

答:

這兩個類都實現了 List 接口(List 接口繼承了 Collection 接口),他們都是有序集合,即存儲在這兩個集合中的元素位置都是有順序的,相當於一種動態的數組,我們以后可以按位置索引來取出某個元素,並且其中的數據是允許重復的,這是與 HashSet 之類的集合的最大不同處,HashSet 之類的集合不可以按索引號去檢索其中的元素,也不允許有重復的元素。

ArrayList 與 Vector 的區別主要包括兩個方面:

  1. 同步性:
    Vector 是線程安全的,也就是說它的方法之間是線程同步(加了synchronized 關鍵字)的,而 ArrayList 是線程不安全的,它的方法之間是線程不同步的。如果只有一個線程會訪問到集合,那最好是使用 ArrayList,因為它不考慮線程安全的問題,所以效率會高一些;如果有多個線程會訪問到集合,那最好是使用 Vector,因為不需要我們自己再去考慮和編寫線程安全的代碼。

  2. 數據增長:
    ArrayList 與 Vector 都有一個初始的容量大小,當存儲進它們里面的元素的個人超過了容量時,就需要增加 ArrayList 和 Vector 的存儲空間,每次要增加存儲空間時,不是只增加一個存儲單元,而是增加多個存儲單元,每次增加的存儲單元的個數在內存空間利用與程序效率之間要去的一定的平衡。Vector 在數據滿時(加載因子1)增長為原來的兩倍(擴容增量:原容量的 2 倍),而 ArrayList 在數據量達到容量的一半時(加載因子 0.5)增長為原容量的 (0.5 倍 + 1) 個空間。


12)ArrayList和LinkedList的區別?

答:

  1. LinkedList 實現了 List 和 Deque 接口,一般稱為雙向鏈表;ArrayList 實現了 List 接口,動態數組;
  2. LinkedList 在插入和刪除數據時效率更高,ArrayList 在查找某個 index 的數據時效率更高;
  3. LinkedList 比 ArrayList 需要更多的內存;

面試官:Array 和 ArrayList 有什么區別?什么時候該應 Array 而不是 ArrayList 呢?

答:它們的區別是:

  1. Array 可以包含基本類型和對象類型,ArrayList 只能包含對象類型。
  2. Array 大小是固定的,ArrayList 的大小是動態變化的。
  3. ArrayList 提供了更多的方法和特性,比如:addAll(),removeAll(),iterator() 等等。

對於基本類型數據,集合使用自動裝箱來減少編碼工作量。但是,當處理固定大小的基本數據類型的時候,這種方式相對比較慢。


13)HashSet是如何保證數據不可重復的?

答:HashSet的底層其實就是HashMap,只不過我們HashSet是實現了Set接口並且把數據作為K值,而V值一直使用一個相同的虛值來保存,我們可以看到源碼:

public boolean add(E e) {
	return map.put(e, PRESENT)==null;// 調用HashMap的put方法,PRESENT是一個至始至終都相同的虛值
}

由於HashMap的K值本身就不允許重復,並且在HashMap中如果K/V相同時,會用新的V覆蓋掉舊的V,然后返回舊的V,那么在HashSet中執行這一句話始終會返回一個false,導致插入失敗,這樣就保證了數據的不可重復性;


14)BlockingQueue是什么?

答:

Java.util.concurrent.BlockingQueue是一個隊列,在進行檢索或移除一個元素的時候,它會等待隊列變為非空;當在添加一個元素時,它會等待隊列中的可用空間。BlockingQueue接口是Java集合框架的一部分,主要用於實現生產者-消費者模式。我們不需要擔心等待生產者有可用的空間,或消費者有可用的對象,因為它都在BlockingQueue的實現類中被處理了。Java提供了集中BlockingQueue的實現,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。


歡迎轉載,轉載請注明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關注公眾微信號:wmyskxz_javaweb
分享自己的Java Web學習之路以及各種Java學習資料
想要交流的朋友也可以加qq群:3382693


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM