吊打面试官之HashMap详解

HashMap详解

HashMap<String, Object> objectHashMap = new HashMap<>();

1、实现原理

jdk1.7是基于哈希表(数组+链表)实现
jdk1.8是基于哈希表(数组+链表+红黑二叉树)实现
在这里插入图片描述

2、HashMap中数组的初始容量,加载因子是多少?

默认数组大小为16,默认加载因子0.75.

3、构造方法指定数组大小的结果

HashMap<String, Object> objectHashMap = new HashMap<>(10);
实际长度为16
HashMap<String, Object> objectHashMap = new HashMap<>(7);
实际长度为8
他是根据构造方法中的指定大小的值initialCapacity,计算大于initialCapacity最小的2的整数次幂的值为实际数组长度

4、HashMap的数组长度为什么需要是2的幂次方

HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;
这个算法实际就是取模,hash % length,计算机中直接求余效率不如位移运算,源码中做了优化 hash & (
length - 1 )

hash % length == hash & ( length - 1 ) 的前提是length是2的n次方; 为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1;

另外一个剪短的解释,2^n也就是说2的n次方的主要核心原因是hash函数的源码中右移了16位让低位保留高位信息,原本的低位信息不要,那么进行&操作另一个数低位必须全是1,否则没有意义,所以len必须是2 ^n ,这样能实现分布均匀,有效减少hash碰撞!

例如:长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了;

	3 & ( 9 - 1 ) = 0                    2 & ( 9 - 1 ) = 0   
	  0000 0011    3                        0000 0010    2
	& 0000 1000    8                      & 0000 1000    8
	= 0000 0000    0                      = 0000 0000    0

例如:长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞;

	3 & ( 8 - 1 ) = 0                    2 & ( 8 - 1 ) = 0   
	  0000 0011    3                        0000 0010    2
	& 0000 0111    7  					  & 0000 0111    7
	= 0000 0011    3    				  = 0000 0010    2

测试两种运算的效率差

    /**
     * 直接【求余】和【按位】运算的差别验证
     */
    public static void main(String[] args) {

        long time1 = System.currentTimeMillis();
        int num1=0;
        int count = 10000*10000;
        for (long i = 0; i < count; i++) {
            num1=9999%1024;
        }
        long time2 = System.currentTimeMillis();

        int num2=0;
        for (long i = 0; i < count; i++) {
            num2=9999&(1024-1);
        }

        long time3 = System.currentTimeMillis();
        System.out.println("最后的结果 num1="+num1+",num2="+num2);
        System.out.println("% 运算的时间为: "+(time2-time1));
        System.out.println("& 运算的时间为: "+(time3-time2));
    }


	 >>> 最后的结果 num1=783,num2=783
	 >>> % 运算的时间为: 359
	 >>> & 运算的时间为: 93

5、HashMap怎么实现的长度是2的幂次方

/**
 * 方法保证了HashMap的哈希表长度总位2的幂次方
 * 返回大于输入参数且最近的2的整数次幂的数
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

本质思想是想把cap-1后的二进制值,最高位1的后面全部利用或运算变成1,最后结果再加1,这样就可以得到最小的大于cap的2的幂次方。

例如:

假如cap输入18,那么cap的二进制10010,n=cap-1的二进制10001

第一次 右移1个  10001 | 01000 = 11001
第二次 右移2个  11001 | 00110 = 11111
第三次 右移4个  11111 | 00001 = 11111
第四次 右移8个  11111 | 00000 = 11111
第五次 右移16个  11111 | 00000 = 11111
最后判断如果n在0-1<<30(相当于2的31次幂)的区间内,那么n+1也就是 100000
输出32

程序测试

 public static void main(String[] args) {
        int num =  18;
        int result = tableSizeFor(num);
        System.out.println("最后输出 "+result);
    }

    static final int tableSizeFor(int cap) {
        System.out.println("cap的二进制是"+Integer.toBinaryString(cap));
        int n = cap - 1;
        System.out.println("n=cap-1的的二进制是"+Integer.toBinaryString(n));

        System.out.print("第一次 右移1个  "+Integer.toBinaryString(n)+" | "+Integer.toBinaryString(n>>>1));
        n |= n >>> 1;
        System.out.println(" = "+Integer.toBinaryString(n));

        System.out.print("第二次 右移2个  "+Integer.toBinaryString(n)+" | "+Integer.toBinaryString(n>>>2));
        n |= n >>> 2;
        System.out.println(" = "+Integer.toBinaryString(n));

        System.out.print("第三次 右移4个  "+Integer.toBinaryString(n)+" | "+Integer.toBinaryString(n>>>4));
        n |= n >>> 4;
        System.out.println(" = "+Integer.toBinaryString(n));

        System.out.print("第四次 右移8个  "+Integer.toBinaryString(n)+" | "+Integer.toBinaryString(n>>>8));
        n |= n >>> 8;
        System.out.println(" = "+Integer.toBinaryString(n));

        System.out.print("第五次 右移16个  "+Integer.toBinaryString(n)+" | "+Integer.toBinaryString(n>>>16));
        n |= n >>> 16;
        System.out.println(" = "+Integer.toBinaryString(n));

        System.out.println("最后n+1的二进制是 "+Integer.toBinaryString(n+1));

        //判断如果n在0-1<<30(相当于2的31次幂)的区间内,将n+1
        return (n < 0) ? 1 : (n >= 1<<30) ? 1<<30 : n + 1;
    }

6、HashMap的put方法

/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key  #传入参数key的特殊hash值
     * @param key the key   #传入的参数key
     * @param value the value to put  #传入的参数value  
     * @param onlyIfAbsent if true, don't change existing value #默认为false,如果为true,不改变原有value值
     * @param evict if false, the table is in creation mode.#如果为false则表现创建模式
     * @return previous value, or null if none #返回以前的值,如果没有,则返回null
     */
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

在这里插入图片描述

先说HashMap的Put⽅法的⼤体流程:

  1. 根据Key通过哈希算法与与运算得出数组下标

  2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是Node对象)并放⼊该位置

  3. 如果数组下标位置元素不为空,则要分情况讨论
     a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对
    象,并使⽤头插法添加到当前位置的链表中
     b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
      i. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过
    程中会判断红⿊树中是否存在当前key,如果存在则更新value
      ii. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插
    ⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链表中,插⼊
    到链表后,会看当前链表的节点个数,如果⼤于等于8,且数组长度大于等于64,那么则会将该链表转成红⿊树
      iii. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就
    扩容,如果不需要就结束PUT⽅法

7、计算key位置的hash函数如何设计的

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

8、为什么这么设计hash函数

hashcode为int类型,4个字节32位,为了确保散列性,肯定是32位都能进行散列算法计算是最好的。

由于绝大多数情况下数组length一般都小于2^16即小于65536。 当数组length=2的N次方, 下标运算结果取决于哈希值的低N位。 所以始终是hashcode 的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列。

为了让高16位也参与运算呢。所以才有hash(Object key)方法。让他的hashCode()和自己的高16位 ^ 运算。所以(h >>> 16)得到他的高16位与hashCode()进行 ^运算。

例一个key的hash值:
 h:0110 0100 1000 1111 0110 1001 0010 0101

 h>>>16:0000 0000 0000 0000 0110 0100 1000 1111

这样进行^运算,保证了 32位全部进行计算
(1)由于大多数取结果的后16位,这样保证了hashcode32位全部参与计算,也保证了0,1平均,散列性
(2)结果的前16位保证hashcode前16位了0,1平均散列性,附带hashcode前16位参与计算。
(3)16与16位数相同,利于计算,不需要补齐,移去位数数据 更多情况,hashmap只会用到后16位(临时数据一般不会这么大)
(4)利用了扰动函数
  1、降低hash碰撞,尽可能分散
  2、使用位运算,使得算法高效

9、为什么用^而不用&和|

在这里插入图片描述
因为&和|都会使得结果偏向0或者1 ,并不是均匀的概念,所以用^。

感谢:
部分图片引用自:https://blog.csdn.net/loveyouyuan/article/details/108104754
第四题参考:https://blog.csdn.net/sidihuo/article/details/78489820

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莯阳_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值