Java-HashMap實現原理

 

圖1 新建-數據存儲html

1,基本特性

  散列表(Hash table,也叫哈希表),是根據鍵(Key)而直接訪問在內存存儲位置的數據結構。java

  ①以鍵值對的形式進行存儲;算法

  ②不容許存在相同的key值,保證惟一映射,再次存入相同key數據,至關於更新數據;數組

  ③無序存儲、無序輸出【原理致使,詳見三、底層實現部分】;數據結構

  ④能夠存儲爲null的鍵和值;ide

 注意--hashMap與hashTable的區別

 

2,Java使用實例

樣例1:函數

package com.cnblogs.mufasa.demo1;
import java.util.HashMap;
public class Client {
    public static void main(String[] args) {
        HashMap<Integer,Integer> hm=new HashMap<>(50);
        hm.put(1,1);
        hm.put(1,2);//至關於把數據更新
        hm.put(null,3);
        hm.put(2,null);
        hm.forEach((k,v)->{
            System.out.println("key:"+k+",value:"+v);
        });
    }
}
View Code

樣例1輸出:性能

key:null,value:3
key:1,value:2
key:2,value:null

3,底層實現邏輯

  3.1基本實現流程測試

  ①HashMap本質上是在內存中開闢一個固定大小的數組空間,②而後根據key計算的hashcode來定位將value存儲在數組空間中的哪裏【浪費空間不少,完美的狀況是每一個key對應的數組空間地址都不相同而且都恰好把空間填滿!】,③可是在經過key計算hash值的時候總會出現所得結果相同的狀況【除非開闢的原始空間特別大、hash算法特別好】,這個就是hash衝突。優化

  經過上面三個基本步驟能夠知道HashMap中兩個關鍵的技術點:①hash算法;②hash衝突;

注意:①hashcode值相同可是有可能不是同一個對象,有多是hash衝突;②同一個對象的hashcode值必定相同;

 如今命名爲Node,之前命名爲Entry

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;

    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    public final K getKey() {
        return key;
    }

    public final V getValue() {
        return value;
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }

    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    public final String toString() {
        return getKey() + "=" + getValue();
    }
}
View Code

 

4,注意事項

4.1 hashCode實現

hashcode實現:

package com.cnblogs.mufasa.demo2;

public class hmcode extends Object{
    @Override
    public int hashCode() {
        return super.hashCode();
    }//public native int hashCode();???

    public static int hashcode2(String str){
        int code=0,len=str.length();
        char[] strs=str.toCharArray();
        for(int i=0;i<len;i++){
            code = 31 * code + (Integer.valueOf(strs[i]) & 0xff);
        }
        return code;
    }

}

class Client{
    public static void main(String[] args) {
        System.out.println((new hmcode()).hashCode());
        System.out.println(hmcode.hashcode2("123"));
        System.out.println("123".hashCode());
    }
}

輸出【原生的hashCode調用的是C++的方法,已經被編譯成了DLL文件了】:

1854778591
48690
48690

  其中hashcode越分散,在hashmap應用中性能越好!

  hashcode優化路線:①普通的映射函數-h(x);②二次映射-a*h(x)+a^2*h(x);/③多函數組合法-a*h1(x)+b*h2(x);【再次驗證正確能夠這樣解決稀疏問題】

4.2 hash衝突解決

  當兩個key值不一樣的數據返回的hash值相同的時候,能夠採用拉鍊法來將新數據鏈接到以前數據的最後【鏈表型數據】,而且當這個hashMap的容量超過限定的容量DEFAULT_LOAD_FACTOR時,就須要對容量進行擴充(通常狀況下是進行2倍擴充),而且還要將原始數據轉移到新的hashMap中【這個過程至關於查詢、存儲數據,有些耗時】,原始的數據變成了垃圾空間。

  應該注意到鏈表的插入是以頭插法方式進行的,例如上面的 <K3,V3> 不是插在 <K2,V2> 後面,而是插入在鏈表頭部。下面的先put  K2,V2後put K3,V3。

  查找須要分紅兩步進行:

  • 計算鍵值對所在的桶;
  • 在鏈表上順序查找,時間複雜度顯然和鏈表的長度成正比。

 

5,手動實現HashMap

  hashMap擴容:

  ①HashMap是先遍歷舊table再遍歷舊table中每一個元素的單向鏈表,取得Entry之後,從新計算hash值,而後存放到新table的對應位置。

  ②LinkedHashMap是遍歷的雙向鏈表,取得每個Entry,而後從新計算hash值,而後存放到新table的對應位置。

  從遍歷的效率來講,遍歷雙向鏈表的效率要高於遍歷table,由於遍歷雙向鏈表是N次(N爲元素個數);而遍歷table是N+table的空餘個數(N爲元素個數)。

 

 hMap函數代碼:

package com.cnblogs.mufasa.demo0;

import java.util.HashMap;
class hMap<K,V> {
    static final int MAXIMUM_CAPACITY = 1 << 30;//hm最大開闢空間
    static final float DEFAULT_LOAD_FACTOR = 0.75f;//最大容量,超過這個容量就須要進行內存的擴充
    private int initialCapacity=13;
    private float loadFactor= 0.75f;
    private int num=0;
    transient Node<K,V>[] table;//不可序列化

    /*
    第一步:構建數據存儲結構,特定長度數組、每一個裏面是一個Node鏈表型數據<K,V>
    三個構造函數【方法重載】
     */
    public hMap(){
        table=new Node[initialCapacity];
    }
    public hMap(int initialCapacity){
        if(initialCapacity>MAXIMUM_CAPACITY){
            this.initialCapacity=MAXIMUM_CAPACITY;
        }else {
            this.initialCapacity=initialCapacity;
        }
        table=new Node[initialCapacity];
    }
    public hMap(int initialCapacity,float loadFactor){
        this(initialCapacity);
        if(loadFactor<=0||loadFactor>DEFAULT_LOAD_FACTOR){
            this.loadFactor=DEFAULT_LOAD_FACTOR;
        }else {
            this.loadFactor=loadFactor;
        }
        table=new Node[initialCapacity];
    }

    /*
    第二步:經過key計算hashcode
     */
    private int hashcode(Object key){
        int h;
        return (key==null?0:((h = key.hashCode()) ^ (h >>> 16)));
    }

    /*
    第三步:數據存、讀、刪、擴容
     */
    public void put(K k, V v){//數據存儲
        if(num>(int) initialCapacity*loadFactor){//須要進行容量擴充了
            int newCap=initialCapacity<<1;//默認不超過最大容量,這裏須要注意最大容量問題
            hMap<K,V> preTable=new hMap<>(newCap);
            System.out.println("當前size="+num+",數據內存進行擴容"+",之前大小爲:"+initialCapacity+",如今大小爲:"+newCap);
            //從新進行舊數據到新數據的轉移
            //①遍歷;②計算存儲
            for(int i=0;i<initialCapacity;i++){//對原始數據進行遍歷整合到新的數據中
                if(table[i]!=null){
                    Node pre=table[i];
                    while (pre!=null){
                        preTable.put((K)pre.getK(),(V)pre.getV());
                        pre=pre.getNext();
                    }
                }
            }
            table=preTable.getTable();
            num=preTable.getNum();
            initialCapacity=newCap;

            preTable=null;//成爲垃圾
//            System.gc();//手動GC
        }

        int hash=hashcode(k);
        hash=hash&(initialCapacity-1);//計算出應該存儲的位置

        if(table[hash]==null){
            table[hash]=new Node<>(k,v);
            num++;//計數+1
        }else {//在那個位置存在一個數據,可能爲【hash衝突】,也多是數據更新
            Node pre=table[hash];
            while (pre!=null){
                if(pre.getK()==k){
                    pre.setV(v);
                    break;
                }
                pre=pre.getNext();
            }
            if(pre==null){
                pre=new Node(k,v);
            }
        }
    }

    public V getValue(K k){//數據讀取
        int hash=hashcode(k);
        hash=hash&(initialCapacity-1);//計算出應該存儲的位置
        Node pre=table[hash];
        V v = null;
        while (pre!=null){
            if(pre.getK().equals(k)){
                v= (V) pre.getV();
                break;
            }
            pre=pre.getNext();
        }
        return v;
    }

    public void remove(K k){//數據刪除
        int hash=hashcode(k);
        hash=hash&(initialCapacity-1);//計算出應該存儲的位置
        Node pre=table[hash];
        if(pre==null){//空數據
            return;
        }else {//存在數據
            --num;//計數自減
            while (pre!=null){
                if(pre.getK().equals(k)){//找到數據位置
                    pre=pre.getNext();
                    break;
                }
            }
        }
    }

    public int size(){//獲取數據大小
        return num;
    }
    public void removeAll(){//清楚全部數據
        num=0;
        table=new Node[initialCapacity];
    }

    private Node<K,V>[] getTable(){
        return this.table;
    }
    private int getNum(){
        return this.num;
    }
}

 

自編寫的hashMap測試:

package com.cnblogs.mufasa.demo0;

public class Client {
    public static void main(String[] args) {

        //測試本身寫的hashMap數據結構
        hMap<Integer,Integer> hm=new hMap<>(4,0.5f);
        hm.put(null,1);
        hm.put(null,2);
        hm.put(1,10);
        hm.put(2,20);
        hm.put(3,30);
        for(int i=4;i<=50;i++){
            hm.put(i,i*10);
        }
        System.out.println(hm.size());

    }
}

輸出【經驗證正確】:

當前size=3,數據內存進行擴容,之前大小爲:4,如今大小爲:8
當前size=5,數據內存進行擴容,之前大小爲:8,如今大小爲:16
當前size=9,數據內存進行擴容,之前大小爲:16,如今大小爲:32
當前size=17,數據內存進行擴容,之前大小爲:32,如今大小爲:64
當前size=33,數據內存進行擴容,之前大小爲:64,如今大小爲:128
51

其中hashcode使用的是Object類中的方法!!!

 

參考連接

https://www.cnblogs.com/java-jun-world2099/p/9258605.html

http://www.javashuo.com/article/p-vukynzog-cb.html

http://www.javashuo.com/article/p-dhpzjlen-v.html

https://blog.csdn.net/liji_xc/article/details/79698223 

http://www.javashuo.com/article/p-kvujwcwz-cq.html

http://www.javashuo.com/article/p-vylxqvsp-ea.html

相關文章
相關標籤/搜索