1:map集合簡述:
2:HashMap集合的實現:
3:HashMap的成員變量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
-> 數組默認初始容量:16
static final int MAXIMUM_CAPACITY = 1 << 30;
-> 數組最大容量2 ^ 30 次方
static final float DEFAULT_LOAD_FACTOR = 0.75f;
-> 默認負載因子的大小:0.75
static final int MIN_TREEIFY_CAPACITY = 64;
-> 樹形最小容量:哈希表的最小樹形化容量,超過此值允許表中桶轉化成紅黑樹
static final int TREEIFY_THRESHOLD = 8;
-> 樹形閾值:當鏈表長度達到8時,將鏈表轉化為紅黑樹
static final int UNTREEIFY_THRESHOLD = 6;
-> 樹形閾值:當鏈表長度小於6時,將紅黑樹轉化為鏈表
transient int modCount; -> hashmap修改次數
int threshold; -> 可存儲key-value 鍵值對的臨界值 需要擴充時;值 = 容量 * 加載因子
transient int size; 已存儲key-value 鍵值對數量
final float loadFactor; -> 負載因子
transient Set< Map.Entry< K,V >> entrySet; -> 緩存的鍵值對集合
transient Node< K,V>[] table; -> 鏈表數組(用於存儲hashmap的數據)
4:重寫hashCode&&equals方法
如果沒有重寫 hashcode 方法,JDK 默認使用 Object 類 native 的 hashCode 方法,返回的是一般是一個與存儲地址相關聯的數
HashMap不能存儲key值相同的數據,是因為在存儲時,會先判斷hashCode是否相同,緊接着equals繼續判斷
所以用到hashCode時,必然要重寫hashCode和equals方法
例:寫一個Student類,重寫hashCode和equals方法
public class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return this.name+"--"+this.age; } @Override public boolean equals(Object o) { Student student=(Student) o; return this.name.equals(student.name)&&this.age==student.age; } @Override public int hashCode() { return name.hashCode()+age*38; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
5:resize機制
HashMap的擴容機制就是重新申請一個容量是當前的2倍的桶數組,然后將原先的記錄逐個重新映射到新的桶里面,然后將原先的桶逐個置為null使得引用失效。
擴容處理會遍歷所有的元素,時間復雜度很高;經過一次擴容處理后,元素會更加均勻的分布在各個桶中,會提升訪問效率。所以我們盡量避免進行擴容處理,當我們知道需要存儲數據的個數時,在newHashMap時就給定初始容量,避免重復擴容
給定擴容容量我們必須要給定容量大於我們預計數據量的 1.34 倍,並且為2的冪次方
例如:如果是2個數據的話,將初始化容量設置為4。
如果預計大概會插入 12 條數據的話,那么初始容量為16簡直是完美,一點不浪費,而且也不會擴容。
6:為什么HashMap線程不安全
1>put的時候導致的多線程數據不一致。
這個問題比較好想象,比如有兩個線程A和B,首先A希望插入一個key-value對到HashMap中,首先計算記錄所要落到的桶的索引坐標,然后獲取到該桶里面的鏈表頭結點,此時線程A的時間片用完了,而此時線程B被調度得以執行,和線程A一樣執行,只不過線程B成功將記錄插到了桶里面,假設線程A插入的記錄計算出來的桶索引和線程B要插入的記錄計算出來的桶索引是一樣的,那么當線程B成功插入之后,線程A再次被調度運行時,它依然持有過期的鏈表頭但是它對此一無所知,以至於它認為它應該這樣做,如此一來就覆蓋了線程B插入的記錄,這樣線程B插入的記錄就憑空消失了,造成了數據不一致的行為。
2>發生在擴容時,重新調整HashMap大小的時候,多個線程確實存在條件競爭,因為如果兩個線程都發現HashMap需要重新調整大小了,它們會同時試着調整大小。在調整大小的過程中,存儲在LinkedList中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap並不會將元素放在LinkedList的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。如果條件競爭發生了,那么就死循環了。這個時候,你可以質問面試官,為什么這么奇怪,要在多線程的環境下使用HashMap呢?