零 前期准备
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,更加抠内存
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。