【集合3】HashMap特殊对待

1.HashMap几个核心的属性

transient int size;// 记录了Map中KV对的个数
final float loadFactor;// 装载因子,用来衡量 HashMap 满的程度
int threshold;// 临界值,当实际 K-V 个数超过 threshold 时,HashMap 会将容量 扩容,threshold=容量*装载因子
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;// 默认装载因子
capacity;// Map容量

2.size和capacity

HashMap 就像一个“桶”,那么 capacity 就是这个桶“当前”最多可以装多少元素,而 size 表示这个桶已经装了多少元素;

👉🏿 1.为什么capacity的默认值是16?
主要是可以使用按位与替代取模来提升 hash 的效率;
2.如果初始capacity不是2的指数,capacity会是多少?
如果用户通过构造函数指定了一个数字作为容量,那么 HashMap 会选择大于该数字的第一个 2 的幂作为capacity;

3.loadFactory和threshold

  1. HashMap 的扩容条件就是当 HashMap 中的元素个数(size)超过临界值(threshold)时就会自动扩容;
  2. HashMap中,threshold(临界值) = capacity(map容量) * loadFactor(装载因子)
  3. loadFactor 是装载因子,表示 HashMap 满的程度,默认值为 0.75f,设置成 0.75 有一个好处,那就是 0.75 正好是 3/4,而 capacity 又是 2 的幂。所以,两个数的乘积都是整数;
  4. HashMap 中还提供了一个支持传入 initialCapacity、loadFactor 两个参数的方法,来 初始化容量和装载因子。不过,一般不建议修改 loadFactor 的值;

4.HashMap中的hash方法

👉🏿 你知道 HashMap 中 hash 方法的具体实现吗?
你知道 HashTable、 ConcurrentHashMap中hash 方法的实现以及原因吗?
你知道为什么要这么实现吗?
你知道为什么 JDK 7 和 JDK 8 中 hash 方法实现的不同以及区别吗?

1. 先看哈希扫盲

2. HashMap的数据结构(数组+链表)

HashMap数据结构
该数据结构 所容纳的所有元素均包含一个指针,用于元素间的链接。我们根据元素的自身特征把元素分配到不同的链表中去,反过来我们也正是通过这些特征找到正确的链表,再从链表中找出正确的元素。其中,根据元素特征计算元素数组下标的方法就是哈希算法,即hash()函数;

3.Hash方法

hash()方法的功能就是根据 Key 来定位其在 HashMap 的数组链表中的位置;
HashTable、ConcurrentHashMap同理;

👉🏿 只要调用 Object 对象的 hashCode()方法,该方法会返回一个整数,然后用这个数对 HashMap 或者 HashTable 的容量进行取模就行了;
在具体实现上,有两个方法 int hash(Object k)int indexFor(int h, int length)来实现。
但是考虑到效率等问题,HashMap 的实现会稍微复杂一点;
hash() :该方法主要是将 Object 转换成一个整型。
indexFor() :该方法主要是将 hash 生成的整型转换成链表数组中的下标。

5. HashMap In Java 7

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

static int indexFor(int h, int length) {
    return h & (length - 1);// 计算下标
}

5.1 获取下标:按位与代替取模(效率更高)

indexFor(), return h & (length-1);
X % 2^n = X & (2^n - 1)公式

所以,只要保证 length 的长度是 2^n 的话,就可以实现取模运算了;
而 HashMap 中的 length 也确实是 2 的倍数;

5.2 冲突来源:按位与只能保证低位hash,不能解决高位冲突!

低位扰动算法

5.3 解决冲突:扰动计算

把高位的特征和低位的特征组合起来, 降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响

👉🏿 h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);

6.HashTable In Java 7

private int hash(Object k) { // hashSeed will be zero if alternative hashing is disabled. 
    return hashSeed ^ k.hashCode();
}
int index = (hash & 0x7FFFFFFF) % tab.length;// 计算下标
  1. 把 hash 值和 0x7FFFFFFF 做一次按位与操作:保证得到的index的第一位是0,得到一个正数;
  2. 使用取模的方式获取下标:HashTable 默认的初始大小为 11,之后每次扩充为原来的 2n+1;当哈希表的大小为素数时, 简单的取模哈希的结果会更加均匀;

7.ConcurrentHashMap In Java 7

private int hash(Object k) {
    int h = hashSeed;
    if ((0 != h) && (k instanceof String)) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode(); 
    // Spread bits to regularize both segment and index locations, 
    // using variant of single-word Wang/Jenkins hash. 
    h += (h << 15) ^ 0xffffcd7d;
   h ^= (h >>> 10); 
   h += (h << 3);
   h ^= (h >>> 6); 
   h += (h << 2) + (h << 14); 
   return h ^ (h >>> 16);
}

int j = (hash >>> segmentShift) & segmentMask;

ConcurrentHashMap 的 hash 实现其实和 HashMap 基本一样。

  1. 使用按位与代替取模;
  2. ConcurrentHashMap使用了一种变种的Wang/Jenkins 哈希算法,其主要目的也是为 了把高位和低位组合在一起,避免发生冲突;(目前没有办法证明哪个算法更优)

8.Java 7 HashMap 和 HashTable 中对于 hash 实现

👉🏿 HashMap 默认的初始化大小为 16,之后每次扩充为原来的 2 倍;
HashTable 默认的初始大小为 11,之后每次扩充为原来的 2n+1;

HashTable 的hash表是奇数,简单的取模,使hash结果更均匀,结果更分散,效果更好;
所以单从这一点上看,HashTable 的哈希表大小选择,更高明些;

HashMap 的hash表是2的次幂,可以直接使用位运算来得到结果,运算效率更高;
但是,为了提高效率使用位运算代替哈希,这又引入了哈希分布不均匀的问题,所以 HashMap 为解决这问题,又对 hash 算法做了一些改进,进行了扰动计算。

9.HashMap In Java 8

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

优化了高位运算的算法,通过
hashCode()的 高16位 异或 低16位(h = k.hashCode()) ^ (h >>> 16)
主要是从速度、功效、质量来考虑的;
以上方法得到的 int 的 hash 值,然后再通过 h & (table.length -1)来得到该对象在数据 中保存的位置;

10.HashTable In Java 8

HashTable 中,已经不在有 hash 方法了;
但是哈希的操作还是在的, 比如在 put 方法中就有如下实现:

int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

HashTable算法

11.ConcurrentHashMap In Java 8

从 hash() 改为了 spread();

static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

ConcurrentHashMap 同样是通过 Key 的哈希值与数组长度取模确定该 Key 在数组中的索引

Java 8 的 ConcurrentHashMap 作者认为引 入红黑树后,即使哈希冲突比较严重,寻址效率也足够高,所以作者并未在哈希值的计算上做过多设计,只是将 Key 的 hashCode 值与其高 16 位作异或,保证最高位为 0(从而保证最终结果为正整数)

参考文章:hash算法详解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值