1. 程式人生 > 程式設計 >Java容器部分知識點

Java容器部分知識點

集合類圖

这里写图片描述


什麼是HashMap

眾所周知,HashMap是一個用於儲存Key-Value鍵值對的集合,每一個鍵值對也叫Entry。這些鍵值對分散在一個陣列中,這個陣列就是HashMap的主幹。

HashMap陣列的每一個初始值都是Null

img

HashMap用陣列+連結串列的形式解決Hash函式下index的衝突情況,比如下面這種情況

hash衝突你還知道哪些解決辦法?(1) 開放地址法(2)鏈地址法(3)再雜湊法(4)公共溢位區域法

img

HashMap陣列的每一個元素不止是一個Entry物件,也是一個連結串列的頭節點。每一個Entry物件通過Next指標指向它的下一個Entry節點。當新來的Entry對映到衝突的陣列位置時,只需要插入到對應的連結串列即可。

由於剛才所說的Hash衝突,同一個位置有可能匹配到多個Entry,這時候就需要順著對應連結串列的頭節點,一個一個向下來查詢。假設我們要查詢的Key是“apple”:

img

需要注意的是,新來的Entry節點插入連結串列時,使用的是“頭插法”。是因為HashMap的發明者認為,後插入的Entry被查詢的可能性更大


HashMap面試問題

HashMap面試必問的6個點,你知道幾個?原作者【程式設計師追風】連結 基於此文做了小量摘抄和補充

HashMap預設的初始長度是多少?為什麼這麼規定?

  • HashMap預設初始長度是16,並且每次自動擴充套件或是手動初始化時,長度必須是2的冪
    • 長度16或者其他2的冪,Length-1的值是所有二進位制位全為1,這種情況下,index的結果等同於HashCode後幾位的值。只要輸入的HashCode本身分佈均勻,Hash演演算法的結果就是均勻的。
  • HashMap的函式採用的位運算的方式

可以用LinkedList代替陣列結構嘛? ps:Entry就是一個連結串列節點

意思原始碼中的

Entry[] table = new Entry[capacity] 
複製程式碼

List<Entry> table = new LinkedList<Entry>();
複製程式碼

表示是否可行,答案很明顯,必須是可以

既然可以,為什麼HashMap不用LinkedList,而使用陣列?

因為陣列效率更高,並且採用基本陣列結構,可以自己定義擴容機制,比如HashMap中陣列擴容是2的次冪,在做取模運算的時候效率高,而ArrayList的擴容機制是1.5倍擴容

HashMap在什麼條件下擴容

load factor為0.75,為了最大程度避免雜湊衝突,也就是當load factor * current cpacity(當前陣列大小)時,就要resize

為什麼擴容是2的次冪?

HashMap為了存取高效,要儘量較少碰撞,就是要儘量把資料分配均勻,每個連結串列長度大致相同,這個實現就在把資料存到哪個連結串列中的演演算法;這個演演算法實際就是取模,hash%length。

但是,大家都知道這種運算不如位移運算快。

因此,原始碼中做了優化hash&(length-1)。

也就是說hash%length==hash&(length-1)

那為什麼是2的n次方呢?

因為2的n次方實際就是1後面n個0,2的n次方-1,實際就是n個1。

例如長度為8時候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞。

而長度為5的時候,3&(5-1)=0 2&(5-1)=0,都在0上,出現碰撞了。

所以,保證容積是2的n次方,是為了保證在做(length-1)的時候,每一位都能&1 ,也就是和1111……1111111進行與運算。

知道hashmap中put元素的過程是什麼樣麼?

  1. 對key的hashCode()做hash運算,計算index;

    此處注意,對於key的判斷如下((k = p.key) == key || (key != null && key.equals(k)))只要滿足其一就會被算作重複,然後覆蓋,也就是如下程式碼,Map的大小為1

    HashMap<String,Integer> map = new HashMap();
    map.put("A",1);
    map.put(new String("A"),1);
    System.out.println(map.size());	//大小為1
    複製程式碼
  2. 如果沒碰撞直接放到bucket裡;

  3. 如果碰撞了,以連結串列的形式存在buckets後;

  4. 如果碰撞導致連結串列過長(大於等於TREEIFY_THRESHOLD),就把連結串列轉換成紅黑樹(JDK1.8中的改動);

  5. 如果節點已經存在就替換old value(保證key的唯一性)

  6. 如果bucket滿了(超過load factor*current capacity),就要resize。

hashmap中get元素的過程?

  1. 對key的hashCode()進行hash運算,得到index
  2. 去bucket中查詢對應的index,如果命中,則直接返回
  3. 如果有衝突,則通過key的equals去查詢對應的entry;
    • 若為樹,則在樹中查詢,時間複雜度為O(logn)
    • 若為連結串列,則在連結串列中查詢,時間複雜度為O(n)

還知道哪些Hash演演算法?

比較出名的有MD4、MD5等

String的hashcode的實現?

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}
複製程式碼

String類中的hashCode計算方法還是比較簡單的,就是以31為權,每一位為字元的ASCII值進行運算,用自然溢位來等效取模。

知道jdk1.8中hashmap改了啥麼?

  • 陣列+連結串列的結構改為陣列+連結串列+紅黑樹
  • 優化了高位運算的hash演演算法:h^(h>>>16)
  • 擴容後,元素要麼是在原位置,要麼是在原位置再移動2次冪的位置,且連結串列順序不變。

最後一條是重點,因為最後一條的變動,hashmap在1.8中,不會在出現死迴圈問題。

為什麼在解決hash衝突的時候,不直接用紅黑樹?而選擇先用連結串列,再轉紅黑樹?

因為紅黑樹需要進行左旋,右旋,變色這些操作來保持平衡,而單連結串列不需要。

當元素小於8個當時候,此時做查詢操作,連結串列結構已經能保證查詢效能。當元素大於8個的時候,此時需要紅黑樹來加快查詢速度,但是新增節點的效率變慢了。

因此,如果一開始就用紅黑樹結構,元素太少,新增效率又比較慢,無疑這是浪費效能的。

我不用紅黑樹,用二叉查詢樹可以麼?

可以。但是二叉查詢樹在特殊情況下會變成一條線性結構(這就跟原來使用連結串列結構一樣了,造成很深的問題),遍歷查詢會非常慢。

當連結串列轉為紅黑樹後,什麼時候退化為連結串列?

為6的時候退轉為連結串列。中間有個差值7可以防止連結串列和樹之間頻繁的轉換。假設一下,如果設計成連結串列個數超過8則連結串列轉換成樹結構,連結串列個數小於8則樹結構轉換成連結串列,如果一個HashMap不停的插入、刪除元素,連結串列個數在8左右徘徊,就會頻繁的發生樹轉連結串列、連結串列轉樹,效率會很低。

HashMap在併發程式設計環境下有什麼問題啊?

  • 多執行緒擴容,引起的死迴圈問題
  • 多執行緒put的時候可能導致元素丟失
  • put非null元素後get出來的卻是null

在jdk1.8中還有這些問題麼?

在jdk1.8中,死迴圈問題已經解決。其他兩個問題還是存在。

你一般怎麼解決這些問題的?

比如ConcurrentHashmap,Hashtable等執行緒安全等集合類。

HashMap的鍵可以為Null嘛

可以

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製程式碼

你一般用什麼作為HashMap的key?

一般用Integer、String這種不可變類當HashMap當key,而且String最為常用。

  • 因為字串是不可變的,所以在它建立的時候hashcode就被快取了,不需要重新計算。這就使得字串很適合作為Map中的鍵,字串的處理速度要快過其它的鍵物件。這就是HashMap中的鍵往往都使用字串。
  • 因為獲取物件的時候要用到equals()和hashCode()方法,那麼鍵物件正確的重寫這兩個方法是非常重要的,這些類已經很規範的覆寫了hashCode()以及equals()方法。
  • 用可變類當HashMap的key有什麼問題? hashcode可能發生改變,導致put進去的值,無法get出

其實嚴格來說,String並不是一個嚴謹的不可變類,在Java1.5以後,我們可以通過反射技術修改String裡的value[]陣列的值。

如果讓你實現一個自定義的class作為HashMap的key該如何實現?

此題考察兩個知識點

  • 重寫hashcode和equals方法注意什麼?
  • 如何設計一個不變類

針對問題一,記住下面四個原則即可

(1)兩個物件相等,hashcode一定相等

(2)兩個物件不等,hashcode不一定不等

(3)hashcode相等,兩個物件不一定相等

(4)hashcode不等,兩個物件一定不等

針對問題二,記住如何寫一個不可變類

(1)類新增final修飾符,保證類不被繼承。

如果類可以被繼承會破壞類的不可變性機制,只要繼承類覆蓋父類的方法並且繼承類可以改變成員變數值,那麼一旦子類以父類的形式出現時,不能保證當前類是否可變。

(2)保證所有成員變數必須私有,並且加上final修飾

通過這種方式保證成員變數不可改變。但只做到這一步還不夠,因為如果是物件成員變數有可能再外部改變其值。所以第4點彌補這個不足。

(3)不提供改變成員變數的方法,包括setter

避免通過其他介面改變成員變數的值,破壞不可變特性。

(4)通過構造器初始化所有成員,進行深拷貝(deep copy)

如果構造器傳入的物件直接賦值給成員變數,還是可以通過對傳入物件的修改進而導致改變內部變數的值。例如:

public final class ImmutableDemo {  
    private final int[] myArray;  
    public ImmutableDemo(int[] array) {  
        this.myArray = array; // wrong  
    }  
}
複製程式碼

這種方式不能保證不可變性,myArray和array指向同一塊記憶體地址,使用者可以在ImmutableDemo之外通過修改array物件的值來改變myArray內部的值。

為了保證內部的值不被修改,可以採用深度copy來建立一個新記憶體儲存傳入的值。正確做法:

public final class MyImmutableDemo {  
    private final int[] myArray;  
    public MyImmutableDemo(int[] array) {  
        this.myArray = array.clone();   
    }   
}
複製程式碼

這裡要注意,Object物件的clone()方法,實現了物件中各個屬性的複製,但它的可見範圍是protected的,所以實體類使用克隆的前提是:

① 實現Cloneable介面,這是一個標記介面,自身沒有方法。 ② 覆蓋clone()方法,可見性提升為public。

也就是說,一個預設的clone()方法實現機制,仍然是賦值。

如果一個被複制的屬性都是基本型別,那麼只需要實現當前類的cloneable機制就可以了,此為淺拷貝。

如果被複制物件的屬性包含其他實體類物件引用,那麼這些實體類物件都需要實現cloneable介面並覆蓋clone()方法。

(5)在getter方法中,不要直接返回物件本身,而是克隆物件,並返回物件的拷貝

這種做法也是防止物件外洩,防止通過getter獲得內部可變成員物件後對成員變數直接操作,導致成員變數發生改變。

準備用HashMap存1w條資料,構造時傳10000還會觸發擴容嗎?

  • 在 HashMap 中,提供了一個指定初始容量的構造方法 HashMap(int initialCapacity),這個方法最終會呼叫到 HashMap 另一個構造方法,其中的引數 loadFactor 就是預設值 0.75f。

  • 從構造方法的邏輯可以看出,HashMap 並不是直接使用外部傳遞進來的 initialCapacity,而是經過了 tableSizeFor() 方法的處理,再賦值到 threshole 上。

  • tableSizeFor() 方法中,通過逐步位運算,就可以讓返回值,保持在 2 的 N 次冪。以方便在擴容的時候,快速計算資料在擴容後的新表中的位置。

    那麼當我們從外部傳遞進來 1w 時,實際上經過 tableSizeFor() 方法處理之後,就會變成 2 的 14 次冪 16384,再算上負載因子 0.75f,實際在不觸發擴容的前提下,可儲存的資料容量是 12288(16384 * 0.75f)。

    這種場景下,用來存放 1w 條資料,綽綽有餘了,並不會觸發我們猜想的擴容。


unmodify不可變的集合

概述:通常用於方法的返回值,通知呼叫者該方法是隻讀的,並且不期望對方修改狀態

補充:Arrays.asList返回的Arrays.ArrayList其實並非不可變,只是add方法沒有重寫,但是可以通過set進行下標資料交換

在Java 8我們可以使用Collections來實現返回不可變集合

方法名 作用
List unmodifiableList(List<? extends T> list) 返回不可變的List集合
Map<K,V> unmodifiableMap(Map<? extends K,? extends V> m) 返回不可變的Map集合
Set unmodifiableSet(Set<? extends T> s) 返回不可變的Set集合

Java集合類:"隨機訪問" 的RandomAccess介面

如果我們用Java做開發的話,最常用的容器之一就是List集合了,而List集合中用的較多的就是ArrayList 和 LinkedList 兩個類,這兩者也常被用來做比較。因為最近在學習Java的集合類原始碼,對於這兩個類自然是不能放過,於是乎,翻看他們的原始碼,我發現,ArrayList實現了一個叫做 RandomAccess 的介面,而 LinkedList 是沒有的

  • RandomAccess 是一個標誌介面,表明實現這個這個介面的 List 集合是支援快速隨機訪問的。也就是說,實現了這個介面的集合是支援 快速隨機訪問 策略的。

  • 同時,官網還特意說明瞭,如果是實現了這個介面的 List,那麼使用for迴圈的方式獲取資料會優於用迭代器獲取資料。


Stream流操作注意事項

stream.of(),最好不要傳基本型別,因為 Stream主要用於物件型別的集合 ,如果傳基本型別,會將基本型別陣列當做一個物件處理。

當然JDK還提供了基本型別的Stream,例如我們可以直接使用IntStream,而不是Stream。


Java 集合框架基礎運用

集合介面

  • Collection實現Interable介面
    • 所有java.util下的集合類都是fast-fail(快速失敗)的
    • 結論:Iterator迭代器只能在 next() 方法執行後,通過 remove() 移除資料,也無法對源 Collection 物件操作
  • 介面分類 (Collection為主,Map為輔)
  • 基於 java.util.Collection 介面的單維度資料集合
    • 基於 java.util.Map 介面的雙維度資料集合或其他介面
  • 陣列的Copy和集合物件的clone是類似的,均為淺克隆
  • 幾乎所有遺留集合實現是執行緒安全
    • Vector、HashTable、Stack等

java.util.Collection 介面

  • 通用介面 * java.util.List(有序,允許重複,允許null)

    * java.util.Set(無序,不可重複,不允許null)
        * Set集合底層使用hash表實現,所以不可重複,每次add的時候都會呼叫自己的hashCode()方法去判斷是否發生碰撞,然後是否替換等操作
        * HashSet在一些特殊場景是有序的,如字母、數字
        
    * java.util.SortedSet  
    
        * TreeSet實現了SortedSet,提供了Compare和CompareTo來定製排序
        
    * java.util.NavigableSet(since Java 1.6)
    複製程式碼

Collection介面下面已細化了List,Set和Queue子介面,未什麼又定義了AbstractCollection這個抽象類?

三者都是Collection,總還是有共同行為的。

  • 抽象實現基於 java.util.Collection 接⼝
    • java.util.AbstractCollection
    • java.util.AbstractList
    • java.util.AbstractSet
    • java.util.AbstractQueue(Since Java 1.5)

ArrayList集合之subList

  • subList實際是原列表的一個檢視,對檢視的操作全部會被反映到原列表上
  • 對於區域性列表的操作可以使用sublist,例如刪除20-30的下標元素,List.sublist(20,30).clear();
  • 生成子列表後不要操作原列表,會拋ConcrrentModificationException異常

通常 Set 是 Map Key 的實現,Set 底層運用 Map 實現 在HashSet中,元素都存到HashMap鍵值對的Key上面,而Value時有一個統一的值private static final Object PRESENT = new Object();,(定義一個虛擬的Object物件作為HashMap的value,將此物件定義為static final)

java.util.Map介面

  • HashMap和HashTable區別【擴充套件ConcurrentHashMap】
    • HashMap執行緒非安全(寫操作),HashTable執行緒安全
    • HashMap允許value為null,HashTable的key和value不允許為null,ConcurrentHashMap的key 與 value 不允許 null
    • 如果 value 為空的話,ConcurrentHashMap 在查詢資料時,會產生歧義。到底是值為 null,還是執行緒不可見
  • 抽象實現基於 java.util.Map 接⼝
    • java.util.AbstractMap
介面 雜湊表 可變陣列 平衡樹 連結串列 雜湊表+連結串列
Set HashSet TreeSet LinkedHashSet
List ArrayList LinkedList
Deque ArrayDeque LinkedList
Map HashMap TreeMap LinkedHashMap

ArrayList和CopyOnWriteArrayList的區別

  • CopyOnWriteArrayList
  • 實現了List介面
    • 內部持有一個ReentrantLock lock = new ReentrantLock();
    • 底層是用volatile transient宣告的陣列 array
    • 讀寫分離,寫時複製出一個新的陣列,完成插入、修改或者移除操作後將新陣列賦值給array
  • ArrayList
    • 底層是陣列,初始大小為10
    • 插入時會判斷陣列容量是否足夠,不夠的話會進行擴容
    • 所謂擴容就是新建一個新的陣列,然後將老的資料裡面的元素複製到新的陣列裡面
    • 移除元素的時候也涉及到陣列中元素的移動,刪除指定index位置的元素,然後將index+1至陣列最後一個元素往前移動一個格

Vector是增刪改查方法都加了synchronized,保證同步,但是每個方法執行的時候都要去獲得鎖,效能就會大大下降,而CopyOnWriteArrayList 只是在增刪改上加鎖,但是讀不加鎖,在讀方面的效能就好於Vector,CopyOnWriteArrayList支援讀多寫少的併發情況

Collections便利實現

介面型別

  • 單例集合介面(Collections.singleton*) 不可變集合、只有一個元素
    • 主要用於只有一個元素的優化,減少記憶體分配,無需分配額外的記憶體
  • 空集合介面(Collections.empty*) *
  • 轉換集合介面(Collections.、Arrays.*) *
  • 列舉集合介面(*.of(…))

集合演演算法運用

  • 計算複雜度:最佳、最壞以及平均複雜度
  • 記憶體使用:空間複雜度
  • 遞迴演演算法:排序演演算法中是否⽤到了遞迴
  • 穩定性:當相同的健存在時,經過排序後,其值也保持相對的順序(不發生變化)
  • 比較排序:集合中的兩個元素進行比較排序