Java容器部分知識點
集合類圖
什麼是HashMap
眾所周知,HashMap是一個用於儲存Key-Value鍵值對的集合,每一個鍵值對也叫Entry。這些鍵值對分散在一個陣列中,這個陣列就是HashMap的主幹。
HashMap陣列的每一個初始值都是Null
HashMap用陣列+連結串列的形式解決Hash函式下index的衝突情況,比如下面這種情況
hash衝突你還知道哪些解決辦法?(1) 開放地址法(2)鏈地址法(3)再雜湊法(4)公共溢位區域法
HashMap陣列的每一個元素不止是一個Entry物件,也是一個連結串列的頭節點。每一個Entry物件通過Next指標指向它的下一個Entry節點。當新來的Entry對映到衝突的陣列位置時,只需要插入到對應的連結串列即可。
由於剛才所說的Hash衝突,同一個位置有可能匹配到多個Entry,這時候就需要順著對應連結串列的頭節點,一個一個向下來查詢。假設我們要查詢的Key是“apple”:
需要注意的是,新來的Entry節點插入連結串列時,使用的是“頭插法”。是因為HashMap的發明者認為,後插入的Entry被查詢的可能性更大。
HashMap面試問題
HashMap面試必問的6個點,你知道幾個?原作者【程式設計師追風】連結 基於此文做了小量摘抄和補充
HashMap預設的初始長度是多少?為什麼這麼規定?
- HashMap預設初始長度是16,並且每次自動擴充套件或是手動初始化時,長度必須是2的冪
- 長度16或者其他2的冪,Length-1的值是所有二進位制位全為1,這種情況下,index的結果等同於HashCode後幾位的值。只要輸入的HashCode本身分佈均勻,Hash演演算法的結果就是均勻的。
- 長度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元素的過程是什麼樣麼?
-
對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 複製程式碼
-
如果沒碰撞直接放到bucket裡;
-
如果碰撞了,以連結串列的形式存在buckets後;
-
如果碰撞導致連結串列過長(大於等於TREEIFY_THRESHOLD),就把連結串列轉換成紅黑樹(JDK1.8中的改動);
-
如果節點已經存在就替換old value(保證key的唯一性)
-
如果bucket滿了(超過load factor*current capacity),就要resize。
hashmap中get元素的過程?
- 對key的hashCode()進行hash運算,得到index
- 去bucket中查詢對應的index,如果命中,則直接返回
- 如果有衝突,則通過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(…))
集合演演算法運用
- 計算複雜度:最佳、最壞以及平均複雜度
- 記憶體使用:空間複雜度
- 遞迴演演算法:排序演演算法中是否⽤到了遞迴
- 穩定性:當相同的健存在時,經過排序後,其值也保持相對的順序(不發生變化)
- 比較排序:集合中的兩個元素進行比較排序