概述
集合框架是Java中用於存儲數據的容器,是面試中的常客,接下來我從面試的角度對常見的集合框架面試題進行一些解析。
面試題解析
常用的集合類有哪些?
Map接口和Collection接口是所有集合框架的父接口:
Collection接口的子接口包括:Set接口和List接口
Map接口的實現類主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
Set接口的實現類主要有:HashSet、TreeSet、LinkedHashSet等
List接口的實現類主要有:ArrayList、LinkedList、Stack以及Vector等
瞭解Java的集合框架嗎,口述一下他們的繼承體系
List,Set,Map三者的區別?
List:一個有序(元素存入集合的順序和取出的順序一致)容器,元素可以重複,可以插入多個null元素,元素都有索引。常用的實現類有 ArrayList、LinkedList 和 Vector。
Set:一個無序(存入和取出順序有可能不一致)容器,不可以存儲重複元素,只允許存入一個null元素,必須保證元素唯一性。Set 接口常用實現類是 HashSet、LinkedHashSet 以及 TreeSet。
Map是一個鍵值對集合,存儲鍵、值和之間的映射。 Key無序,唯一;value 不要求有序,允許重複。Map沒有繼承於Collection接口,從Map集合中檢索元素時,只要給出鍵對象,就會返回對應的值對象。
Map 的常用實現類:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap,其中HashTable只作爲了解即可,目前已經不再使用。
說一說他們的底層數據結構是什麼
Collection
1、List
Arraylist :底層使用的是Object數組;
LinkedList :底層使用的是雙向鏈表數據結構(JDK1.6之
前爲循環鏈表,JDK1.7取消了循環。注意雙向鏈表和雙向循環鏈表的區別:);
vector:就比arraylist多了個同步化機制(線程安全),因爲效率較低,現在已經不太建議使用;
statck:對vector進行進出限制,堆棧類,先進後出。
2、Set
HashSet(無序,唯一):基於 HashMap 實現的,底層採用 HashMap 來保存元素;
LinkedHashSet: LinkedHashSet 繼承於 HashSet,並且其內部是通過 LinkedHashMap 來實現的;
TreeSet(有序,唯一): 紅黑樹(自平衡的排序二叉樹。
Map
1、HashMap:JDK1.8之前HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的(“拉鍊法”解決衝突).JDK1.8以後在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲8)時,將鏈表轉化爲紅黑樹,以減少搜索時間;
2、HashTable:Hashtable 和 JDK1.8 之前的 HashMap 的底層數據結構類似都是採用 數組+鏈表 的形式,但它是HashMap的線程安全版本,它使用 synchronized 來保證線程安全,效率非常低下。當一個線程訪問同步方法時,其他線程也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用put 添加元素,另一個線程不能使用 put 添加元素,也不能使用 get,競爭會越來越激烈效率越低;
3、TreeMap: 紅黑樹(自平衡的排序二叉樹);
4、LinkedHashMap:LinkedHashMap 繼承自 HashMap,所以它的底層仍然是基於拉鍊式散列結構即由數組和鏈表或紅黑樹組成。另外,LinkedHashMap 在上面結構的基礎上,增加了一條雙向鏈表,使得上面的結構可以保持鍵值對的插入順序。同時通過對鏈表進行相應的操作,實現了訪問順序相關邏輯;
5、ConcurrentHashMap:JDK1.7的 ConcurrentHashMap 底層採用 分段的數組+鏈表 實現,JDK1.8 採用的數據結構跟HashMap1.8的結構一樣,數組+鏈表/紅黑二叉樹。
如何保證線程安全:在JDK1.7的時候,ConcurrentHashMap(分段鎖) 對整個桶數組進行了
分割分段(Segment),每一把鎖只鎖容器其中一部分數據,多線程訪問容器裏不同數據段的數據,就不會存在鎖競爭,提高併發訪問率。 到了 JDK1.8 的時候已經摒棄了Segment的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,併發控制使用 synchronized 和 CAS 來操作。(JDK1.6以後 對 synchronized鎖做了很多優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在JDK1.8中還能看到 Segment 的數據結構,但是已經簡化了屬性,只是爲了兼容舊版本。
說一下 ArrayList 的優缺點
ArrayList的優點如下:
ArrayList 底層以數組實現,是一種隨機訪問模式。ArrayList 實現了 RandomAccess 接口,因此查找的時候非常快。
ArrayList 在順序添加一個元素的時候非常方便。
ArrayList 的缺點如下:
刪除元素的時候,需要做一次元素複製操作。如果要複製的元素很多,那麼就會比較耗費性能。
插入元素的時候,也需要做一次元素複製操作,缺點同上。
ArrayList 比較適合順序添加、隨機訪問的場景。
List 和 Set 的區別
List , Set 都是繼承自Collection 接口
List 特點:一個有序(元素存入集合的順序和取出的順序一致)容器,元素可以重複,可以插入多個null元素,元素都有索引。常用的實現類有 ArrayList、LinkedList 和 Vector。
Set 特點:一個無序(存入和取出順序有可能不一致)容器,不可以存儲重複元素,只允許存入一個null元素,必須保證元素唯一性。Set 接口常用實現類是 HashSet、LinkedHashSet 以及 TreeSet。
另外 List 支持for循環,也就是通過下標來遍歷,也可以用迭代器,但是set只能用迭代,因爲他無序,無法用下標來取得想要的值。
Set和List對比:
Set:檢索元素效率低下,刪除和插入效率高,插入和刪除不會引起元素位置改變。
List:和數組類似,List可以動態增長,查找元素效率高,插入刪除元素效率低,因爲會引起其他元素位置改變。
Map(Map是面試重點,所以單獨拿出來講)
什麼是哈希?
Hash,一般翻譯爲“散列”,也有直接音譯爲“哈希”的,這就是把任意長度的輸入通過散列算法,變換成固定長度的輸出,該輸出就是散列值(哈希值);這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出,所以不可能從散列值來唯一的確定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。
所有散列函數都有如下一個基本特性:根據同一散列函數計算出的散列值如果不同,那麼輸入值肯定也不同。但是,根據同一散列函數計算出的散列值如果相同,輸入值不一定相同。
爲什麼計算散列值的過程不可逆
當運算過程中出現進位時,進位被直接丟失而不會保存。也就是說,散列函數計算散列值的運算過程存在信息丟失。由於不知道運算過程中會有多少個進位在哪一步被丟棄,因而僅僅根據散列函數的計算過程和得到的最終結果,是無法逆向計算出明文的。
什麼是哈希衝突?
當兩個不同的輸入值,根據同一散列函數計算出相同的散列值的現象,我們就把它叫做碰撞(哈希碰撞)。
爲什麼在鏈表長度爲8時需要將鏈表轉換爲紅黑樹
當鏈表過長時,查詢效率就會變低,轉換爲紅黑樹能一直保證查詢效率的高效。
建議閱讀一下HashMap源碼
HashMap 與 HashTable 有什麼區別?
線程安全: HashMap 是非線程安全的,HashTable 是線程安全的;HashTable 內部的方法基本都經過 synchronized 修飾。(如果你要保證線程安全的話就使用 ConcurrentHashMap 吧!);
效率: 因爲線程安全的問題,HashMap 要比 HashTable 效率高一點。另外,HashTable 基本被淘汰,不要在代碼中使用它;
對Null key 和Null value的支持: HashMap 中,null 可以作爲鍵,這樣的鍵只有一個,可以有一個或多個鍵所對應的值爲 null。但是在 HashTable 中 put 進的鍵值只要有一個 null,直接拋NullPointerException。
初始容量大小和每次擴充容量大小的不同 : ①創建時如果不指定容量初始值,Hashtable 默認的初始大小爲11,之後每次擴充,容量變爲原來的2n+1。HashMap 默認的初始化大小爲16。之後每次擴充,容量變爲原來的2倍。②創建時如果給定了容量初始值,那麼 Hashtable 會直接使用你給定的大小,而 HashMap 會將其擴充爲2的冪次方大小。也就是說 HashMap 總是使用2的冪作爲哈希表的大小,後面會介紹到爲什麼是2的冪次方。
底層數據結構: JDK1.8 以後的 HashMap 在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲8)時,將鏈表轉化爲紅黑樹,以減少搜索時間。Hashtable 沒有這樣的機制。
推薦使用:在 Hashtable 的類註釋可以看到,Hashtable 是保留類不建議使用,推薦在單線程環境下使用 HashMap 替代,如果需要多線程使用則用 ConcurrentHashMap 替代。
HashMap 和 ConcurrentHashMap 的區別
ConcurrentHashMap對整個桶數組進行了分割分段(Segment),然後在每一個分段上都用lock鎖進行保護,相對於HashTable的synchronized鎖的粒度更精細了一些,併發性能更好,而HashMap沒有鎖機制,不是線程安全的。(JDK1.8之後ConcurrentHashMap啓用了一種全新的方式實現,利用CAS算法。)
HashMap的鍵值對允許有null,但是ConCurrentHashMap都不允許。
ConcurrentHashMap 和 Hashtable 的區別?
ConcurrentHashMap 和 Hashtable 的區別主要體現在實現線程安全的方式上不同。
底層數據結構: JDK1.7的 ConcurrentHashMap 底層採用 分段的數組+鏈表 實現,JDK1.8 採用的數據結構跟HashMap1.8的結構一樣,數組+鏈表/紅黑二叉樹。Hashtable 和 JDK1.8 之前的 HashMap 的底層數據結構類似都是採用 數組+鏈表 的形式,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突而存在的;
實現線程安全的方式(重要): ① 在JDK1.7的時候,ConcurrentHashMap(分段鎖) 對整個桶數組進行了分割分段(Segment),每一把鎖只鎖容器其中一部分數據,多線程訪問容器裏不同數據段的數據,就不會存在鎖競爭,提高併發訪問率。(默認分配16個Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的時候已經摒棄了Segment的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,併發控制使用 synchronized 和 CAS 來操作。(JDK1.6以後 對 synchronized鎖做了很多優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在JDK1.8中還能看到 Segment 的數據結構,但是已經簡化了屬性,只是爲了兼容舊版本;② Hashtable(同一把鎖) :使用 synchronized 來保證線程安全,效率非常低下。當一個線程訪問同步方法時,其他線程也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 添加元素,另一個線程不能使用 put 添加元素,也不能使用 get,競爭會越來越激烈效率越低。
ConcurrentHashMap 底層具體實現知道嗎?實現原理是什麼?
JDK1.7
首先將數據分爲一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據時,其他段的數據也能被其他線程訪問。
在JDK1.7中,ConcurrentHashMap採用Segment + HashEntry的方式進行實現,結構如下:
一個 ConcurrentHashMap 裏包含一個 Segment 數組。Segment 的結構和HashMap類似,是一種數組和鏈表結構,一個 Segment 包含一個 HashEntry 數組,每個 HashEntry 是一個鏈表結構的元素,每個 Segment 守護着一個HashEntry數組裏的元素,當對 HashEntry 數組的數據進行修改時,必須首先獲得對應的 Segment的鎖。
該類包含兩個靜態內部類 HashEntry 和 Segment ;前者用來封裝映射表的鍵值對,後者用來充當鎖的角色;
Segment 是一種可重入的鎖 ReentrantLock,每個 Segment 守護一個HashEntry 數組裏得元素,當對 HashEntry 數組的數據進行修改時,必須首先獲得對應的 Segment 鎖。
JDK1.8
在JDK1.8中,放棄了Segment臃腫的設計,取而代之的是採用Node + CAS + Synchronized來保證併發安全進行實現,synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不衝突,就不會產生併發,效率又提升N倍。
結構如下:
易考算法
集合框架在我看了其實就是Java已經封裝好的數據結構,所以我們最後再說一說數據結構易考的簡單題目:
1、鏈表易考算法:
-
刪除鏈表中等於給定值 val 的所有節點。 OJ鏈接
-
反轉一個單鏈表。 OJ鏈接
-
給定一個帶有頭結點 head 的非空單鏈表,返回鏈表的中間結點。如果有兩個中間結點,則返回第二個中間結點。OJ鏈接
-
輸入一個鏈表,輸出該鏈表中倒數第k個結點。 OJ鏈接
-
將兩個有序鏈表合併爲一個新的有序鏈表並返回。新鏈表是通過拼接給定的兩個鏈表的所有節點組成的。OJ鏈接
-
編寫代碼,以給定值x爲基準將鏈表分割成兩部分,所有小於x的結點排在大於或等於x的結點之前 。OJ鏈接
-
在一個排序的鏈表中,存在重複的結點,請刪除該鏈表中重複的結點,重複的結點不保留,返回鏈表頭指針。 OJ鏈接
-
鏈表的迴文結構。OJ鏈接
-
輸入兩個鏈表,找出它們的第一個公共結點。OJ鏈接
-
給定一個鏈表,判斷鏈表中是否有環。 OJ鏈接
-
給定一個鏈表,返回鏈表開始入環的第一個節點。 如果鏈表無環,則返回 NULL OJ鏈接
2、樹
樹主要就是考遍歷
二叉樹的分層遍歷 OJ鏈接
二叉樹的前序遍歷,非遞歸迭代實現 。(遞歸雖然簡單,但也要會)OJ鏈接
二叉樹中序遍歷 ,非遞歸迭代實現。(遞歸雖然簡單,但也要會)OJ鏈接
二叉樹的後序遍歷 ,非遞歸迭代實現。(遞歸雖然簡單,但也要會)OJ鏈接
以上就是一些比較簡單的數據結構算法,雖然簡單但在面試中被問到的概率特別高,並且非常重要,一般來說,如果面試官問的時候你沒能回答上來,此次面試幾乎可以宣告失敗了,請大家一定要重視。還有就是每道題可能有幾種解法,所以大家自己寫出來之後儘量去看一下題解,找一找別的解法,講那些解法也熟記於心,因爲面試官可能會將所有可能出現的解法逐一問出來。再有就是簡單的排序算法大家也要重視,考的機率特別大。
我對集合框架的總結也就這麼多了,都是我前一陣面試被問到的,可能會存在許多遺漏,所以大家在看到我的這篇博客之後也能去看一下別的大神所總結的更爲全面的集合框架,但是我覺得我這裏面所提到的都是大家必須要掌握的,可能有的問題解答存在漏洞,望大家能在評論區指出。最後建議大家去找一下hashmap的源碼解析閱讀一下,裏面所涉及的細節太多我沒有注意列出。