手撕HashMap

山嶽之巔發表於2020-12-01

前言:

      平時工作的時候,用的最多的就是ArrayList和HashMap了,今天看了遍HashMap的原始碼,決定自己手寫一遍HashMap。

 

一、建立MyHashMap介面

      我們首先建立一個MyHashMap的入口,暴露一個外部呼叫的介面,裡面簡單的定義一下putget。

public interface MyHashMap<K,V> {

    public V put(K k,V v);
    public V get(K k);
    interface Entry<K,V>{
        public K getKey();
        public V getValue();
    }

}

二、建一個實現類MyHashMapImpl

      介面定義完成之後,那就要開始實現了,我們首先建立一個類MyHashMapImpl來實現MyHashMap。然後我們定義一些變數。以及建構函式,比如我們定義的陣列初始長度為16,載入因子為0.75。這兩個引數會涉及到自動擴容,我們後面再說。

public class MyHashMapImpl<K, V> implements MyHashMap<K, V> {
//陣列的初始長度
private static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

//閥值比例(載入因子)
private static final float DEFAULT_LOAD_FACTOR = 0.75f;

private int defaultInitSize;

private final float defaultLoadFactor;

//Map當中entry的數量
private int entryUseSize;

//陣列
private Entry<K, V>[] table;

//建構函式
public MyHashMapImpl() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

public MyHashMapImpl(int defaultInitialCapacity, float defaultLoadFactor) {

if (defaultInitialCapacity < 0)
//容量不合規
throw new IllegalArgumentException("Illegal initial capacity" + defaultInitialCapacity);
if (defaultLoadFactor <= 0 || Float.isNaN(defaultLoadFactor))
//不合規的載入因子
throw new IllegalArgumentException("Illegal load factor" + defaultLoadFactor);
this.defaultInitSize = defaultInitialCapacity;
this.defaultLoadFactor = defaultLoadFactor;
table = new Entry[this.defaultInitSize];
}

}

     

三、重寫put方法

      我們首先重寫下put方法,可以看到,當Map中儲存的資料大於載入因子*初始化資料長度的時候,會第一時間觸發擴容機制,擴容的過程也就是重新設定一個更大的陣列,並把原本的陣列地址指過去,並且把原本的值重新put進去。這個過程如果頻繁發生還是很消耗機器效能的,所以我們在寫程式碼的時候最好是預估好初始大小,儘量不觸發擴容機制。

      

 @Override
    public V put(K k, V v) {
        V oldValue;
        //是否需要擴容
        //擴容完畢,肯定需要重新雜湊
        if (entryUseSize >= defaultInitSize * defaultLoadFactor) {
            resize(2 * defaultInitSize);
        }
        int index = hash(k) & (defaultInitSize - 1);
        if (table[index] == null) {
            table[index] = new Entry<K, V>(k, v, null);
            ++entryUseSize;
        } else {
            Entry<K, V> entry = table[index];
            Entry<K, V> e = entry;
            while (e != null) {
                if (k == e.getKey() || k.equals(e.getKey())) {
                    oldValue = e.value;
                    e.value = v;
                    return oldValue;
                }
                e = e.next;
            }
            table[index] = new Entry<K, V>(k, v, entry);
            ++entryUseSize;
        }

        return null;
    }


 private void resize(int i) {
        Entry[] newTable = new Entry[i];
        defaultInitSize = i;
        entryUseSize = 0;
        rehash(newTable);
    }


private void rehash(Entry<K, V>[] newTable) {
        //得到原來老得entry集合,注意遍歷單連結串列
        List<Entry<K, V>> entryList = new ArrayList<Entry<K, V>>();
        for (Entry<K, V> entry : table) {
            if (entry != null) {
                do {
                    entryList.add(entry);
                    entry = entry.next;
                } while (entry != null);
            }

        }
        //覆蓋舊的引用
        if (newTable.length > 0) {
            table = newTable;
        }
        //重新hash也就是重新put entry到hashmap
        for (Entry<K, V> entry : entryList) {
            put(entry.getKey(), entry.getValue());
        }

    }

  class Entry<K, V> implements MyHashMap.Entry<K, V> {

        private K key;
        private V value;
        private Entry<K, V> next;

        public Entry() {

        }

        public Entry(K key, V value, Entry<K, V> next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }


        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }

    }

 

四、重寫get方法

      如果要拿到陣列中的值,我們首先要獲取對應的位置。其中有一個基本概念要說一下,每一個資料通過hash函式都會得到一個值,並且這個值是固定的,所以我們可以通過k.hashCode()

來獲取對應的hash值,然後按照雜湊演算法均勻分散hash值,然後通過hashcode獲取對應的值,得到基本陣列的下標。這時候就能拿到我們存在map中的值了,但是hash值並不是一定是唯一的,也就是說可以能a.hash和b.hash值是一樣的,但是a不等於b,所以如果兩個資料hash值相同,會觸發hash衝突。嚴重降低hashmap的效能,本次hash方法的作用也就是儘量減少hash衝突。使資料排列的更加均勻一些。當我們遇到hash衝突的時候可以再次hash解決衝突。

  @Override
    public V get(K k) {
        int index = hash(k) & (defaultInitSize - 1);
        if (table[index] == null) {
            return null;
        } else {
            Entry<K, V> entry = table[index];
            do {
                if (k == entry.getKey() || k.equals(entry.getKey())) {
                    return entry.value;
                }
                entry = entry.next;

            } while (entry != null);
        }

        return null;
    }

 

相關文章