追蹤解析 Netty IntObjectHashMap 原始碼

三流發表於2022-12-26

零 前期準備

0 FBI WARNING

文章異常囉嗦且繞彎。

1 版本

Netty : 5.0.0.Alpha5
IDE : idea 2022.2.4
maven 座標:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty5-all</artifactId>
    <version>5.0.0.Alpha5</version>
</dependency>

一 簡介

IntObjectHashMap 是 netty 封裝的,key 必須是 int 的 HashMap 容器。
在 netty 4 中,該類位於 netty-all 包下的 io.netty.util.collection 路徑下;在 netty 5 中,該類位於 netty5-common 包下的 io.netty5.util.collection 路徑下。
本文使用 netty 5 進行原始碼跟蹤。值得注意的是,截止到 2022 年 12 月,netty 5 還沒有進入生產就緒的狀態,不建議在生產環境使用。

二 Demo

import io.netty5.util.collection.IntObjectHashMap;
import io.netty5.util.collection.IntObjectMap;

public class IntHashMapTest {

    public static void main(String[] args) {
        // 建立一個容器
        // Map<Integer, String> map = new IntObjectHashMap<>();
        IntObjectMap<String> map = new IntObjectHashMap<>();
        
        // 存值
        map.put(1, "t1");
        map.put(2, "t2");

        // 輸出 t2
        System.out.println(map.get(2));

        // 刪除
        map.remove(2);
    }
}

三 IntObjectMap

IntObjectMap 是 IntObjectHashMap 的頂層介面。

package io.netty5.util.collection;

import java.util.Map;

/**
 * IntObjectMap 繼承了 map,但是 key 必須是 int
 */
public interface IntObjectMap<V> extends Map<Integer, V> {

    /**
     * PrimitiveEntry 是 IntObjectMap 內部定義的 Entry 類,相比 Entry 功能更為簡單
     * 它的實現在 IntObjectHashMap 中
     */
    interface PrimitiveEntry<V> {
        /** 獲取 key */
        int key();

        /** 獲取 value */
        V value();

        /** 存入 value */
        void setValue(V value);
    }

    /** 根據 key 獲取值 */
    V get(int key);

    /** 存入鍵值對 */
    V put(int key, V value);

    /** 刪除鍵值對,並返回值 */
    V remove(int key);

    /** 獲取 Entry 的迭代器 */
    Iterable<PrimitiveEntry<V>> entries();

    /** 判斷是否存在鍵值對 */
    boolean containsKey(int key);
}

四 IntObjectHashMap

1 變數

// 預設容量
public static final int DEFAULT_CAPACITY = 8;
// 用來 rehash 的 load 因子數
public static final float DEFAULT_LOAD_FACTOR = 0.5f;
// 當插入的 value 是 null 的時候,會塞入一個填充物件
private static final Object NULL_VALUE = new Object();
// 能實現的最大值
private int maxSize;
// rehash 的 load 因子數,會影響 maxSize
private final float loadFactor;
// key 的集合
private int[] keys;
// value 的集合
private V[] values;
// 當前的 size
private int size;
// 陣列最大的 index,等於 array.length - 1
private int mask;

// key 的集合 set
private final Set<Integer> keySet = new KeySet();
// k-v 的集合 set
private final Set<Entry<Integer, V>> entrySet = new EntrySet();
// 迭代器
private final Iterable<PrimitiveEntry<V>> entries = new Iterable<PrimitiveEntry<V>>() {
    @Override
    public Iterator<PrimitiveEntry<V>> iterator() {
        return new PrimitiveIterator();
    }
};

2 構造器

/** 使用預設容量和 load 因子的構造器 */
public IntObjectHashMap() {
    this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
}

/** 使用 load 因子的構造器 */
public IntObjectHashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public IntObjectHashMap(int initialCapacity, float loadFactor) {
    // 有效性驗證
    if (loadFactor <= 0.0f || loadFactor > 1.0f) {
        throw new IllegalArgumentException("loadFactor must be > 0 and <= 1");
    }

    // 存入 load 因子
    this.loadFactor = loadFactor;

    // 存入容量
    int capacity = safeFindNextPositivePowerOfTwo(initialCapacity);
    // 陣列最大 size
    mask = capacity - 1;

    // keys
    keys = new int[capacity];
    // values
    @SuppressWarnings({ "unchecked", "SuspiciousArrayCast" })
    V[] temp = (V[]) new Object[capacity];
    values = temp;

    // 最大容量
    maxSize = calcMaxSize(capacity);
}

2.1 safeFindNextPositivePowerOfTwo

用來計算最大容量的方法,在 io.netty5.util.internal.MathUtil 裡。

public static int safeFindNextPositivePowerOfTwo(final int value) {
    return value <= 0 ? 1 : value >= 0x40000000 ? 0x40000000 : findNextPositivePowerOfTwo(value);
}

public static int findNextPositivePowerOfTwo(final int value) {
    assert value > Integer.MIN_VALUE && value < 0x40000000;
    return 1 << (32 - Integer.numberOfLeadingZeros(value - 1));
}

這兩個數學方法用以獲取比輸入數字更大的一個 2 的冪數。
舉例來說:
輸入 1, 返回 1;
輸入 2, 返回 2;
輸入 3, 返回 4;
輸入 55, 返回 64;
輸入 100,返回 128;
輸入 513,返回 1024。

2.2 calcMaxSize

private int calcMaxSize(int capacity) {
    int upperBound = capacity - 1;
    // 比較 cap - 1 和 cap * load 哪個比較小,取小的那個座位 maxSize 返回
    return Math.min(upperBound, (int) (capacity * loadFactor));
}

3 put

@Override
public V put(int key, V value) {
    // 使用 key 計算一個 hash 值作為起始 hash 值
    int startIndex = hashIndex(key);
    int index = startIndex;

    // 死迴圈
    for (;;) {
        if (values[index] == null) {
            // 如果 hash 值對應的 value 槽裡沒有值,就將新值插進去
            // 插入 key
            keys[index] = key;
            // 插入值
            values[index] = toInternal(value);
            // 判斷是否需要擴容陣列,如果需要的話在這裡會動態擴容
            growSize();
            return null;
        }

        // 到此處,說明 hash 值對應的 value 槽裡有東西了

        if (keys[index] == key) {
            // 此處說明, key 對應的這個插槽裡就似乎當前的 key
            V previousValue = values[index];
            // 用新值代替舊值
            values[index] = toInternal(value);
            // 返回原來的值
            return toExternal(previousValue);
        }

        // 此處會將 index 挪到下一個槽裡,繼續此迴圈
        if ((index = probeNext(index)) == startIndex) {
            // 如果下一個 index 槽就是當前,說明死迴圈了,拋錯
            throw new IllegalStateException("Unable to insert");
        }
    }
}

3.1 hashIndex

private int hashIndex(int key) {
    return hashCode(key) & mask;
}

private static int hashCode(int key) {
   return (int) key;
}

這兩個方法的核心是製造 hash 碰撞,然後指定一個在陣列長度內的插槽。

3.2 toInternal 和 toExternal

private static <T> T toExternal(T value) {
    assert value != null : "null is not a legitimate internal value. Concurrent Modification?";
    return value == NULL_VALUE ? null : value;
}

@SuppressWarnings("unchecked")
private static <T> T toInternal(T value) {
    return value == null ? (T) NULL_VALUE : value;
}

這兩個方法主要是杜絕 value 為 null 的情況,將 value 包裝成一個 object 再存入陣列中。

// NULL_VALUE 是一個 Object 物件
private static final Object NULL_VALUE = new Object();

3.3 growSize

private void growSize() {
    
    size ++;

    if (size > maxSize) {
        if(keys.length == Integer.MAX_VALUE) {
            throw new IllegalStateException("Max capacity reached at size=" + size);
        }

        // 如果 size 比 maxSize 大,則說明所有的槽都被佔滿了,此處需要 rehash
        // rehash 方法會將 maxSize 擴大一倍
        rehash(keys.length << 1);
    }
}

private void rehash(int newCapacity) {
    int[] oldKeys = keys;
    V[] oldVals = values;

    // 此處建立兩個新的陣列
    keys = new int[newCapacity];
    @SuppressWarnings({ "unchecked", "SuspiciousArrayCast" })
    V[] temp = (V[]) new Object[newCapacity];
    values = temp;

    // 重新計算 maxSize
    maxSize = calcMaxSize(newCapacity);
    mask = newCapacity - 1;

    // 將原來的資料重新插入到新的陣列裡
    // 具體過程和 put 方法差不多
    for (int i = 0; i < oldVals.length; ++i) {
        V oldVal = oldVals[i];
        if (oldVal != null) {
            int oldKey = oldKeys[i];
            int index = hashIndex(oldKey);

            for (;;) {
                if (values[index] == null) {
                    keys[index] = oldKey;
                    values[index] = oldVal;
                    break;
                }

                index = probeNext(index);
            }
        }
    }
}

3.4 probeNext

private int probeNext(int index) {
    return (index + 1) & mask;
}

獲取下一個位置。

4 get

@Override
public V get(int key) {
    int index = indexOf(key);
    return index == -1 ? null : toExternal(values[index]);
}

和 put 方法基本類似,也是透過 indexOf 方法獲取一個 key 所對應的槽,然後去直接讀取 value 並返回。

4.1 get 的 Map 介面方法

demo:

// 在這種方式下會強制 IntObjectHashMap 使用 Map 介面提供的 get 方法
String val = map.get(new Integer(2));

實現為:

@Override
public V get(Object key) {
    return get(objectToKey(key));
}

// 預設輸入的 key 只能是 Integer 型別的,然後將其轉為 int 型別
private int objectToKey(Object key) {
    return (int) ((Integer) key).intValue();
}

get(Object key) 這個方法是 java.util.Map 介面裡帶的方法,這裡 IntObjectHashMap 做了相容。

5 remove

@Override
public V remove(int key) {
    // 確定 index
    int index = indexOf(key);
    if (index == -1) {
        return null;
    }

    // 獲取原來的值
    V prev = values[index];
    // 刪除值
    removeAt(index);
    // 返回原來的值
    return toExternal(prev);
}


private boolean removeAt(final int index) {
    --size;

    // 還原這兩個陣列槽內的值
    keys[index] = 0;
    values[index] = null;

    // 這裡需要將當前卡槽後偏移的資料挪回來,避免槽內出現太多的空缺,影響查詢效率
    int nextFree = index;
    int i = probeNext(index);
    for (V value = values[i]; value != null; value = values[i = probeNext(i)]) {
        int key = keys[i];
        int bucket = hashIndex(key);
        if (i < bucket && (bucket <= nextFree || nextFree <= i) ||
            bucket <= nextFree && nextFree <= i) {

            // 此時的 i = probeNext(nextFree)
            // 也就是說,i 是 nextFree 的下一次偏移
            // 這裡將 i 對應的 key 和 value 轉換到 nextFree 對應的槽裡
            keys[nextFree] = key;
            values[nextFree] = value;
            keys[i] = 0;
            values[i] = null;
            nextFree = i;
        }
    }
    return nextFree != index;
}

四 總結

  • IntObjectHashMap 沒有和 jdk HashMap 一樣,使用連結串列法來解決 hash 衝突,而是使用了開放地址法
  • 由於使用了開放地址法,實際上在 hash 碰撞較為嚴重的場合,其查詢效能比 HashMap 會更好,但是其擴縮容的代價會比 HashMap 更大,效能更糟
  • 不適合資料量較大的場景,適合資料量較小且查詢速度要求較高的場景
  • 將 int 作為 key,更加摳記憶體

相關文章