学习计划 HashMap(1.7)

4 篇文章 0 订阅
2 篇文章 0 订阅

1.7的HashMap完整实现了哈希表,哈希表是一种根据键值(Key-Value)访问数据的结构,实现这种结构需要解决两个问题:

一. 哈希函数

理想的哈希函数对于不同的输入应该产生不同的结构,同时散列结果应当具有同一性(输出值尽量均匀)和雪崩效应(微小的输入值变化使得输出值发生巨大大变化)
1.7 hash函数实现:

    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
		//异或运算 相同返回0
        h ^= k.hashCode();
		//1.8改为高位参与运算 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
二. 冲突解决

冲突指的哈希函数计算出的访问地址已存在数据,均匀的哈希函数可以减少冲突,但不能避免冲突,发生冲突后,必须解决;也即必须寻找下一个可用地址
HashMap中使用拉链法来解决冲突, 将所有位置重复的数据使用单项链表存储,也就是数组加链表,HashMap使用嵌套类Entry存储元素,它包含四个属性:key,value,hash值和用于单向链表的next

   static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

下面是代码解析,建议对照着代码看

初始化

HashMap在使用put方法时才会创建Entry对象,同时他的初始大小大于等于2的幂次方,以7和9为例
大于等于7的2的幂次方为8
大于等于9的2的幂次方为16

put方法判断Entry
   if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }

threshold 是你传入的大小,如果没有传入,默认为16:
在这里插入图片描述

inflateTable
  private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize 查找一个大于等于toSize的2的幂次方数
        int capacity = roundUpToPowerOf2(toSize);
		//扩容阈值计算 
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //创建 capacity 大小的Entry 实例
        table = new Entry[capacity];
        //rehash 扩容之后需要重新计算hash
        initHashSeedAsNeeded(capacity);
    }

可以看到 new Entry[capacity] ,创建对象时,并没有使用我们传入的toSize,而是将它传入了roundUpToPowerOf2 这个方法

roundUpToPowerOf2
    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

关键点在于
Integer.highestOneBit((number - 1) << 1)
这段代码返回一个小于等于number的二的幂次方,并将他左移一位

highestOneBit
    public static int highestOneBit(int i) {
        // HD, Figure 3-1
        i |= (i >>  1);
        i |= (i >>  2);
        i |= (i >>  4);
        i |= (i >>  8);
        i |= (i >> 16);
        return i - (i >>> 1);
    }

看不懂?没关系,先打印一下:
在这里插入图片描述
下面我以i = 15为例,推算这个过程

第一行代码 i |= (i >> 1)

i >> 1 = 15右移一位(将值转换为2进制,正数时左边补0,最后一位去除) ,计算过程:
15=0000 1111(这里简单写为8位,实际上应该是32位,因为int是4个字节,每个字节占8位)
右移一位=0000 0111
然后将两个值进行 按位或(有一为一)运算
0000 1111
0000 0111
结果为:
0000 1111
此时i = 0000 1111 也就是 15

第二行代码 i |= (i >> 2)

i >> 2 = 15右移两位,计算过程
15=0000 1111
右移两位(最后两位去除,最左边补零)=0000 0011
按位或运算:
0000 1111
0000 0011
结果为:
0000 1111
也就是15

第三行代码 i |= (i >> 4)

15右移四位(最后四位去除),计算过程
15=0000 1111
右移四位=0000 0000
按位或运算:
0000 1111
0000 0000
结果还是0000 1111
至此发现后面的位移运算已经没有意义了,所以直接跳到最后一行代码

返回结果 return i - (i >>> 1)

i >>> 1 = 15无符号右移(不论正负,直接补零)一位
15=0000 1111
无符号右移= 000 0111
然后再相减,结果为:0000 1000
转换为10进制为8

这里有个技巧叫做8421 从高到低对应不同的权重,举例:
0000 1111 = 1* 8 + 1* 4 + 1* 2 + 1* 1 = 15
0000 0111 = 0* 8 + 1* 4 + 1* 2 + 1* 1 = 7
0000 1000 = 1* 8 +0* 4 + 0* 2 + 0* 1 = 8

最后highestOneBit(15)这个方法返回值为 8
我在开头写过 :HashMap初始大小大于等于2的幂次方,8显然不是大于等于15的2的幂次方
我们返回roundUpToPowerOf2方法,查看这段代码:
在这里插入图片描述
它将返回值 8 左移1位
8:0000 1000
左移一位(左边去除一位,右边补一位0):0001 0000
转换为10进制为: 16

到这一步就成功的创建了一个大小为16的HashMap!

Integer.highestOneBit((number - 1) 为什么要减一?

我们知道 HashMap的默认大小为1 << 4 也就是16
highestOneBit 会返回小于等于输入值的二的幂次方,传入16会返回16,
此时将16左移一位时会返回32,这样就会导致创建错误大小的HashMap

Put
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold); //初始化
        }
        if (key == null) 
            return putForNullKey(value);//key等于null 时,进入这个方法
        int hash = hash(key);//哈希函数 计算哈希值
        int i = indexFor(hash, table.length);//计算下标
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;//如果这个key已存在,返回老的value
            }
        }

        modCount++; //增加修改次数
        addEntry(hash, key, value, i); //添加数据
        return null;
    }
putForNullKey
    private V putForNullKey(V value) {
    //判断第一个key是否等于null
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;//覆盖老的value  ,并返回
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }
indexFor(hash, table.length)
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) ==
         1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

这一步叫做定位哈希桶索引
h & (length-1) 等价于h%table.length

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值