java集合基礎及相關面試題整理

目錄

1.添加一組元素

Collection 和 Collections的區別

2 迭代器

2.1 Iterator

2.2 ListIterator

2.3 Foreach與迭代器

2.4 快速失敗(fail-fast)和安全失敗(fail-safe)的區別

3 List

3.1 ArrayList

3.2 LinkedList

4 Stack

5 Queue

5.1 PriorityQueue

6 Set

7 Map

7.1 HashMap

7.2 HashMap和Hashtable的區別

7.3 ConcurrentHashMap

7.3 TreeMap

7.4 LinkedHashMap

8 補充

8.1 List、Map、Set三個接口存取元素時各自的特點

8.2  集合類沒有實現Cloneable和Serializable接口


1.添加一組元素

Collections.addAll():接受一個Collection對象,以及一個數組或是一個用逗號分割的列表,將元素添加到Collection中。

Collection.addAll():只能接受另一個Collection對象作爲參數。

Arrays.asList():接受一個數組或是一個用逗號分隔的元素列表(使用可變參數),並將其轉換爲一個List對象。注意,該list對象底層是數組,因此不能調整尺寸。

Collection 和 Collections的區別

  • Collection是集合類的上級接口,繼承於它的接口主要有Set 和List.
  • Collections是針對集合類的一個工具類,提供一系列靜態方法實現對各種集合的搜索、排序、線程安全化等操作。

2 迭代器

2.1 Iterator

Iterator提供了統一遍歷集合元素操作的統一接口,  Java的Iterator只能單向移動,可用來遍歷Set和List集合:使用方法iterator()要求容器返回一個Iterator,Iterator將準備好返回序列的第一個元素;使用next()獲得序列中的下一個元素;使用hasNext()檢查序列中是否還有元素;使用remove()將迭代器新返回的元素刪除(調用remove()方法之前必須先調用next()方法)。

注:在迭代元素的時候不能通過集合的方法刪除元素, 否則會拋出ConcurrentModificationException異常. 但是可以通過Iterator接口中的remove()方法進行刪除.

2.2 ListIterator

ListIterator可以雙向移動,只用來遍歷List集合:通過調用listIterator()方法產生一個指向List開始處的ListIterator,並且還可以通過調用ListIterator(n)方法創建一個一開始就指向列表索引爲n的元素處的ListIterator;可以使用set()方法替換它訪問過的最後一個元素。

2.3 Foreach與迭代器

foreach語法主要用於數組,也可以用於任何Collection對象。任何實現了Iterable的類,都可以用於foreach語句中。但是數組本身並不是一個Iterable。

2.4 快速失敗(fail-fast)和安全失敗(fail-safe)的區別

  • 快速失敗:在用迭代器遍歷一個集合對象時,如果遍歷過程中對集合對象的內容進行了修改(增加、刪除、修改),則會拋出ConcurrentModificationException。
  • 安全失敗:在遍歷時先複製原有集合內容,在拷貝的集合上進行遍歷。由於迭代時是對原集合的拷貝進行遍歷,所以在遍歷過程中對原集合所作的修改並不能被迭代器檢測到,所以不會觸發Concurrent Modification Exception。
  • java.util包下面的所有的集合類都是快速失敗的,而java.util.concurrent包下面的所有的類都是安全失敗的。

3 List

  • 當確定一個元素是否屬於某個List,發現某個元素的索引,以及從某個List中移除一個元素等,都會用到equals()方法。
  • containsAll():與裏面元素的順序無關。

3.1 ArrayList

用於隨機訪問元素,但是在List的中間插入和移除元素時較慢。

ArrayList自動擴容機制:ArrayList的默認初始容量爲10,也可以通過ArrayList(int initialCapacity)構造一個具有指定初始容量的空列表,隨着動態的向其中添加元素,其容量可能會動態的增加,每次擴充至原有基礎的1.5倍。例如初始化容量爲20,總共有50個元素,擴容次數依次爲:20->30->45->67。另外,ArrayList併發add()可能出現數組下標越界異常。這是因爲ArrayList在擴容的過程中,內部的一致性被破壞,但由於沒有鎖的保護,另外一個線程訪問到了這個不一致的內部狀態,導致出現越界問題。

ArrayList和Vector的區別:都使用數組方式存儲數據,索引數據快但增刪慢,Vector中的方法由於添加了synchronized修飾,因此Vector是線程安全的容器,但性能上較ArrayList差,因此已經是Java中的遺留容器。

3.2 LinkedList

使用雙向鏈表實現存儲,通過代價較低的在List中間進行的插入和刪除操作,提供了優化的順序訪問。LinkedList還添加了可以使其用作棧、隊列或雙端隊列的方法。

  • 返回列表的頭:getFirst()、element()、peek().如果列表爲空,peek()返回null,其它拋出異常;
  • 移除並返回列表的頭:removeFirst()、remove()、poll().如果列表爲空,poll()返回null,其它拋出異常;
  • 插入列表尾部:add()、offer()、addLast().

注:1.ArrayList和LinkedList都是非線程安全的,如果遇到多個線程操作同一個容器的場景,則可通過工具類Collections中的synchronizedList方法將其轉換成線程安全的容器後再使用

2. 如果一直在list的尾部添加元素,當數據量小的時候,ArrayList需要擴容,所以LinkedList的效率就會比較高;當數據量很大的時候,new對象的時間大於擴容的時間,那麼ArrayList的效率比LinkedList高。

4 Stack

可以直接將LinkedList作爲棧使用,程序中應避免使用java.util.Stack(已過時)。

   public class Stack<T>{
       private LinkedList<T> list = new LinkedList<>();
       public void push(T v){
           list.addFirst(v);
       }
       public T peek(){
           return list.getFirst();
       }
       public T pop(){
           return list.removeFirst();
       }
       public boolean empty(){
           return list.isEmpty();
       }
       public String toString(){
           return list.toString();
       }
   }

5 Queue

LinkedList提供了方法以支持隊列的行爲,並且它實現了Queue接口。peek()和element()都將在不移除的情況下返回隊頭,但是peek()方法在隊列爲空時返回null,而element()會拋出NoSuchElementException異常。poll()和remove()方法將移除並返回隊頭,但是poll()在隊列爲空時返回null,而remove()會拋出NoSuchElementException異常。

5.1 PriorityQueue

PriorityQueue調用offer()方法來插入一個對象時,這個對象會在隊列中被排序,默認爲自然排序,我們可以通過提供自己的Comparator來修改這個順序。PriorityQueue可確保調用peek()、poll()和remove()方法時獲取的元素是隊列中優先級最高的元素。

6 Set

  • HashSet:底層使用散列函數
  • TreeSet:底層使用紅-黑樹數據結構
  • LinkedHashSet:使用散列以及鏈表來維護數據的插入順序。

7 Map

7.1 HashMap

HashMap用來快速訪問,是基於數組+鏈表+紅黑樹實現的,它的默認容量爲 16,負載因子爲 0.75。Map 在使用過程中不斷的往裏面存放數據,當數量達到了 16 * 0.75 = 12 就會進行擴容。

transient Node<K,V>[] table;

 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}

HashMap初始容量爲2^n及每次擴容爲原來的2倍

  • 通過(n - 1) & hash來計算索引位置,位運算&速度高於取模運算%
  • 散列更均勻,減少碰撞:HashMap的容量固定爲2的n次冪,(n-1)的2進制也就是1111111***111這樣形式的,這樣與添加元素的hash值進行位運算時,能夠充分的散列,使得添加的元素均勻分佈在HashMap的每個位置上,減少hash碰撞

hash方法原理

  • 先求key的哈希值,取key的hashcode值h與h的高8位做與運算(混合原始哈希碼的高位和低位,以此來加大低位的隨機性)
  • 取模:(n - 1) & hash
static final int hash(Object key) {   //jdk1.8
     int h;
     // h = key.hashCode() 第一步 取hashCode值
     // h ^ (h >>> 16)  第二步 高位參與運算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

put方法:首次put元素需要進行擴容爲默認容量16,每次put,先根據key的hash值得到插入的數組索引i,如果索引i中有值,看key是否存在(equals()方法),如果存在就直接覆蓋,否則插入鏈表或者紅黑樹中。當鏈表元素個數大於等於8時,鏈表轉換成樹結構;若桶中鏈表元素個數小於等於6時,樹結構還原成鏈表。因爲紅黑樹的平均查找長度是log(n),長度爲8的時候,平均查找長度爲3,如果繼續使用鏈表,平均查找長度爲8/2=4,這纔有轉換爲樹的必要。鏈表長度如果是小於等於6,6/2=3,雖然速度也很快的,但是轉化爲樹結構和生成樹的時間並不會太短。還有選擇6和8,中間有個差值7可以有效防止鏈表和樹頻繁轉換

注:使用HashMap,如果key是自定義的類,就必須重寫hashcode()和equals()。

擴容機制:首次put元素需要進行擴容爲默認容量16,之後達到閾值就擴容爲原來的兩倍,接下來就是進行擴容後table的調整:假設擴容前的table大小爲2的N次方,而元素的table索引爲key的hash值的後N位確定,那麼擴容後元素的table索引爲其hash值的後N+1位確定,比原來多了一位,因此,若元素hash值第N+1位爲0,則不需要進行位置調整,反之如果爲1則調整至原索引的兩倍位置。擴容或初始化完成後,resize方法返回新的table。注意:JDK1.8之後是先插入後擴容

7.2 HashMap和Hashtable的區別

  • 父類不同: HashMap是繼承自AbstractMap類,而HashTable是繼承自Dictionary(已被廢棄)。 
  • null值問題:Hashtable不允許鍵或者值是null;HashMap允許鍵和值是null,只能有一個鍵爲null。當get()方法返回null值時,可能是 HashMap中沒有該鍵,也可能使該鍵所對應的值爲null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷。
  • 線程安全性:Hashtable是線程安全的,而HashMap不是線程安全的。多線程操作時可使用線程安全的ConcurrentHashMap。因ConcurrentHashMap使用了分段鎖,並不對整個數據進行鎖定,ConcurrentHashMap效率比Hashtable要高好多倍。
  • 初始容量:Hashtable的初始長度是11,之後每次擴充容量變爲之前的2n+1(n爲上一次的長度);而HashMap的初始長度爲16,之後每次擴充變爲原來的兩倍。
  • 計算哈希值的方法:Hashtable直接使用對象的hashCode,使用除留餘數法來獲得最終的位置,效率很低;HashMap將哈希表的大小固定爲了2的冪,這樣在取模預算時,不需要做除法,只需要做位運算。

7.3 ConcurrentHashMap

ConcurrentHashMap 類中包含兩個靜態內部類 HashEntry 和 Segment,一個 ConcurrentHashMap 實例中包含由若干個 Segment 對象組成的數組

  • HashEntry 用來封裝映射表的鍵 / 值對;在 HashEntry 類中,key,hash 和 next 域都被聲明爲final型,value 域被聲明爲volatile 型。
  • Segment用來充當鎖的角色,每個Segment對象守護整個散列映射表的若干個桶。每個桶是由若干個HashEntry對象鏈接起來的鏈表。
static final class HashEntry<K,V> {
       final K key;                       // 聲明 key 爲 final 型
       final int hash;                   // 聲明 hash 值爲 final 型
       volatile V value;                 // 聲明 value 爲 volatile 型
       final HashEntry<K,V> next;      // 聲明 next 爲 final 型
  
       HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
           this.key = key;
           this.hash = hash;
           this.next = next;
           this.value = value;
       }
}

在ConcurrentHashMap 中,在散列時如果產生“碰撞”,將採用“分離鏈接法”來處理“碰撞”:把“碰撞”的 HashEntry 對象鏈接成一個鏈表。由於 HashEntry 的 next 域爲 final 型,所以新節點只能在鏈表的表頭處插入。 下圖是在一個空桶中依次插入 A,B,C 三個 HashEntry 對象後的結構圖:

圖1. 插入三個節點後桶的結構示意圖:

注意:由於只能在表頭插入,所以鏈表中節點的順序和插入的順序相反。

Segment 類繼承於 ReentrantLock 類,從而使得 Segment 對象能充當鎖的角色。每個 Segment 對象用來守護其(成員對象 table 中)包含的若干個桶。

7.3 TreeMap

TreeMap是一個有序的key-value集合,基於紅黑樹實現。紅黑樹的插入、刪除、遍歷時間複雜度都爲O(lgN)。該映射根據其鍵的自然順序進行排序,或根據創建映射時提供的 Comparator進行排序,具體取決於使用的構造方法。TreeMap的特性(黑根黑葉路同黑,紅黑二色紅生黑

  • 根節點是黑色 
  • 每個節點都只能是紅色或者黑色
  • 每個葉節點(NULL節點)是黑色的。 
  • 如果一個節點是紅色的,則它兩個子節點都是黑色的,也就是說在一條路徑上不能出現兩個紅色的節點。
  • 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

7.4 LinkedHashMap

保持元素出入的順序,同時通過散列提供了快速訪問能力。

8 補充

8.1 List、Map、Set三個接口存取元素時各自的特點

  • List以特定索引來存取元素,可以有重複元素。
  • Set不能存放重複元素(用對象的equals()方法來區分元素是否重複)。
  • Map保存鍵值對(key-value pair)映射,映射關係可以是一對一或多對一。
  • Set和Map容器都有基於哈希存儲和排序樹的兩種實現版本,基於哈希存儲的版本理論存取時間複雜度爲O(1),而基於排序樹版本的實現在插入或刪除元素時會按照元素或元素的鍵(key)構成排序樹從而達到排序和去重的效果。

8.2  集合類沒有實現Cloneable和Serializable接口

克隆(cloning)或者是序列化(serialization)的語義和含義是跟具體的實現相關的。因此,應該由集合類的具體實現來決定如何被克隆或者是序列化。實現Serializable序列化的作用:將對象的狀態保存在存儲媒體中以便可以在以後重寫創建出完全相同的副本;按值將對象從一個從一個應用程序域發向另一個應用程序域。 實現 Serializable接口的作用就是可以把對象存到字節流,然後可以恢復。所以你想如果你的對象沒有序列化,怎麼才能進行網絡傳輸呢?要網絡傳輸就得轉爲字節流,所以在分佈式應用中,你就得實現序列化。如果你不需要分佈式應用,那就沒必要實現實現序列化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章