HashTable
继承Dictionary类,实现Map接口,是JDK1.0时期创建的,对数据键值对,进行哈希存储管理, 所有的对外方法被synchronized修饰,可以看作是HashMap在远古时期的线程安全的容器。
HashTable的底层实现是个Entry数组,Entry对象管理了键值对的key和value,在有哈希冲突的时候,在冲突位置生成基于Entry对象的链表,这是一个单向链表。
基于Entry数组,生成索引的方法如下,0x7FFFFFFF是int的最大值,2的31次方值减一,也就是2147483647
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
构造方法创建HashTable对象,默认底层数组长度是11,扩容因子是0.75
public Hashtable() {
this(11, 0.75f);
}
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
put方法存储数据:
- 强制 要求value不能为null。并根据key计算出本次数据存储要放置的索引位置。
- 获取此位置已有的对象,并进行链表遍历,匹配哈希值相等,且key也相等的entry,覆盖它的value。
- 没匹配且当前存储的数据个数 大于等于待扩容条件,那么就进行扩容。
- 新的Entry数组的长度是就数组长度 *2的结果 加1,如果新容量超过了2147483639,且旧容量等于2147483639时,不做扩容,结束。旧容量不等时,那新容量就是2147483639。
- 重新设置扩容触发条件数字等于新容量 * 扩容因子0.75,
- 遍历旧数组,数组索引位置有链表对象,再遍历此链表,链表里每个节点的哈希值按照上述方法计算在新数组里的索引位置,并将该节点放在链表的头部。
- 重新计算本次新数据在扩容后数组的索引位置,并覆盖之前的计算结果
- 创建本次新数据生成的Entry对象,并放在指定索引位置的链表头部
get方法获取数据:
- 计算输入的key的哈希值对应的在底层数组的索引位置。
- 找到此位置的链表,并遍历,找到哈希值相等,key也相等的entry对象,返回它的value
remove方法移除指定key:
- 计算输入的key的哈希值对应的在底层数组的索引位置。
- 找到此位置的链表,并遍历,找到哈希值相等,key也相等的entry对象
- 将原本指向该Entry对象的操作指向该Entry对象的后置对象。
IdentityHashMap
继承AbstractMap,实现Map接口,它是一种比较特殊的哈希Map,数据只存在底层的数组中,包括key和value。同时,它支持输入的key为null,会特殊处理成一个new Object对象代替。
构造方法创建IdentityHashMap时,会时默认的底层数组长度为64,如果构造方法的参数里指定了长度,那么实际数组长度也就是指定长度 *2;
IdentityHashMap计算输入的key所在索引位置的方法:
private static int hash(Object x, int length) {
int h = System.identityHashCode(x);
// Multiply by -127, and left-shift to use least bit as part of hash
return ((h << 1) - (h << 8)) & (length - 1);
}
value基于当前key的索引和当前数组长度算出的索引位置:
private static int nextKeyIndex(int i, int len) {
return (i + 2 < len ? i + 2 : 0);
}
put方法存数据流程:
- 强制对Key进行判断,如果是null,就改为默认值new Object;
- 计算出当前输入的key,所在当前数组的索引位置;
- 如果当前数组此索引位置有对象item,根据当前key的索引值,基于当前数组长度,计算出value的索引位置。
- 如果此时item等于输入的key,那么value就放在第三步得到的索引+1的位置。
- 如果当前数组此索引位置没对象,或者有对象,但是与key不想等,设置当前存储的键值对个数+1,如果当前个数*3大于数组长度,执行扩容方法。
- 扩容成功后,重新从第2步开始执行。
- 最终在索引i位置存key,在索引i+1位置存value。
扩容流程:
- 新数组长度一般是旧数组长度的2倍,在到最大数时进行特殊处理。
- 从上述存储可知,这个数组是key-value-key-value这样排下去的,那么i的位置是key时,i+1的位置就是value,
- 将key基于新数组长度进行计算得到新的索引位置,存储key后接着存储vlaue
- 遇到冲突的情况时,跳过2个位置进行存储,直到能放置。
get方法获取数据:
- 计算key所在的索引位置,如果此位置为null,返回null
- 如果此位置的对象等于输入的key,那么就返回value
- 不满足上面2个条件时,对索引进行跳2位,重新从第1步开始比较。
WeakHashMap
弱哈希,继承AbstractMap,实现Map接口。
WeakHashMap的实现,与HashMap的区别在于,它对key的处理是弱引用,HashMap是强引用。
WeakHashMap的底层也是Entry数组 + Entry链表,实现方式可参考HashTable的介绍。
因为是若引用,所以在垃圾回收时,很容易被回收掉,虽然降低了OOM的风险,但是会导致原本应该存在的数据现在不存在了,所以WeakHashMap的底层在处理时,在获取数据后一定要判断是否为null。