Java容器部分知識點

集合類圖

这里写图片描述


什麼是HashMap

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

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算法?

比較出名的有MD四、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(…))

集合算法運用

  • 計算複雜度:最佳、最壞以及平均複雜度
  • 內存使用:空間複雜度
  • 遞歸算法:排序算法中是否⽤到了遞歸
  • 穩定性:當相同的健存在時,通過排序後,其值也保持相對的順序(不發生變化)
  • 比較排序:集合中的兩個元素進行比較排序
相關文章
相關標籤/搜索