Java HashMap問題


   1:map集合簡述:    

        我們常用的集合實現類有HashMap、LinkedHashMap、TreeMap,HashTable。HashMap根據key的hashCode值來保存value,需要注意的是,HashMap不保證遍歷的順序和插入的順序是一致的。HashMap允許有一條記錄的key為null,但是對值是否為null不做要求。
         
        HashTable類是線程安全的,它使用synchronize來做線程安全,全局只有一把鎖,在線程競爭比較激烈的情況下hashtable的效率是比較低下的。因為當一個線程訪問hashtable的同步方法時,其他線程再次嘗試訪問的時候,會進入阻塞或者輪詢狀態,比如當線程1使用put進行元素添加的時候,線程2不但不能使用put來添加元素,而且不能使用get獲取元素。所以,競爭會越來越激烈。相比之下,ConcurrentHashMap使用了分段鎖技術來提高了並發度,不在同一段的數據互相不影響,多個線程對多個不同的段的操作是不會相互影響的。每個段使用一把鎖。所以在需要線程安全的業務場景下,推薦使用ConcurrentHashMap,而HashTable不建議在新的代碼中使用,如果需要線程安全,則使用ConcurrentHashMap,否則使用HashMap就足夠
   
        LinkedHashMap屬於HashMap的子類,與HashMap的區別在於LinkedHashMap保存了記錄插入的順序。TreeMap實現了SortedMap接口,TreeMap有能力對插入的記錄根據key排序,默認按照升序排序,也可以自定義比較強,在使用TreeMap的時候,key應當實現Comparable接口。
可以看到HashMap繼承自父類(AbstractMap),實現了Map、Cloneable、Serializable接口。其中,Map接口定義了一組通用的操作;Cloneable接口則表示可以進行拷貝,在HashMap中,實現的是淺層次拷貝,即對拷貝對象的改變會影響被拷貝的對象;Serializable接口表示HashMap實現了序列化,即可以將HashMap對象保存至本地,之后可以恢復狀態。
 

2:HashMap集合的實現:

      HashMap的實現使用了一個數組,每個數組里面有一個鏈表的方式來實現,因為HashMap使用key的hashCode來尋找存儲位置,不同的key可能具有相同的hashCode,這時候就出現哈希沖突了,也叫做哈希碰撞,為了解決哈希沖突,有開放地址方法,以及鏈地址方法。HashMap的實現上選取了鏈地址方法,也就是將哈希值一樣的entry保存在同一個數組項里面,可以把一個數組項當做一個桶,桶里面裝的entry的key的hashCode是一樣的。
  在Java8之后,HashMap在原有的基礎上實現了紅黑樹結構,當桶中鏈表長度超過8時,會轉化為紅黑樹結構來存儲,當鏈表長度小於6時,將紅黑樹轉化為鏈表,使得在桶里面查找數據的復雜度從O(n)降到O(logn)
   HashMap的初始桶的數量為16,loadFact為0.75,當桶里面的數據記錄超過閾值的時候,HashMap將會進行擴容則操作,每次都會變為原來大小的2倍,直到設定的最大值之后就無法再resize了。

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簡直是完美,一點不浪費,而且也不會擴容。

    如果某個Map存儲了10000個數據,那么他會擴容到 20000,實際上,根本不用 20000,只需要 10000* 1.34= 13400 個,然后向上找到一個2 的冪次方,也就是 16384 初始容量足夠

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呢?

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM