HashMap(JDK1.7)详细源码分析开胃菜-包含总结可直接杀死面试

关注公众号:”奇叔码技术“
回复:“java面试题大全”或者“java面试题”
即可领取资料

喜爱的源码分析又来了!

1.如果不喜欢位运算想知道JDK1.7的hashmap源码面试总结可直接看最后面即可;

2.文章会携带小部分,___下划线的小题目,用于大脑思考,增强各位同学同志的记忆;

一、HashMap中的<<左移动和>>右移动的移位

注意四个点:

原码、反码、补码的首位称为符号位0为正,首位1为负.

  • 原码、反码、补码的首位称为符号位0为正,首位1为负.
  • 我们平常看到的正负数值都是原码(二进制0和1),而计算机是无法进行计算的(这个问题,等看完文章之后,再说明例子。)而位移和运算时是补码进行的位移和运算
  • 正数我们直接可以得到补码,负数需要经过原码->反码->补码得到(这也是为什么需要学习原码、反码、补码的原因)
  • 这里我们假设类比java中的byte类型,1个byte,8个bit来进行移位。(实际java中是int类型的4个byte,32个bit位来玩儿的).

具体内容:

正数:以+5为例,

从右至左数,第一个bit位表示2的0次方为1,第三个bit位表示2的2次方为4,以此类推。即2的2次方+2的0次方=5.

提问,这里的首位(符号位)为0则是正的还是负的呢?

答:(问题一)

原码 0000 0101

反码 0000 0101

补码 0000 0101

规律:正数的原码、反码、补码是一致的




负数:以-5为例,

提问,这里的首位(符号位)为1则是正的还是负的呢?

答:(问题二)

原码 1000 0101

反码 1111 1010

补码 1111 1011

规律:
原码:首位即符号位为1为负,其余和正5没区别。
反码:首位即符号位不变,其余位数取反,0变为1,1变为0。
补码:在反码基础上都不变的情况下末尾+1,(bit位是逢2进1再变0).


符号 >> 右箭头为右移动 << 左箭头为左移动,统称为有符号运算.

(1)补码的算数移位,首位即符号位是不变的,仅对数值位进行移位。

(2)正数:>> 右移则表示从最后面去掉bit位,并在最前面补0。即高位补0,低位舍弃。如果舍弃的位为0,相当于/2(除以2);如果舍弃的bit位!=0为1的话,则会丢失精度。

(3)正数:<< 左移则表示向末尾补0 则所有bit位都进位了。即低位补0,高位舍弃。若舍弃的位=0,则相当于*2(乘以2);如果舍弃的bit位!=0为1的话,则会出现严重误差。

(4)负数:>> 右移:高位补1,低位舍弃。一般情况下相当于/2(除以2);如果舍弃的bit位!=0为1的话,则会丢失精度。

(5)负数:<< 左移:低位补0,高位舍弃。一般情况下相当于*2(乘以2);如果舍弃的bit位!=0为1的话,则会出现严重误差。

(6)由于左移动乘以2,右移动除以2是不规律的,最好的就是原码->反码->补码进行转化后,再由补码进行移位和运算再转化为补码->反码->原码,得到的值才是准确的,这也是我们为什么学习原码、反码、补码。

(7)简单来记就是,左移动乘,右移动除。再学习这篇文章之后,自己再动手进行原码、反码、补码进行转化和移位得到精确值吧


5 >> 2 表示 5的补码的bit位向右边移动两位,即是乘以还是除以呢?答:(问题三)

原码 0000 0101

反码 0000 0101

补码 0000 0001 01(这里注意末尾的01被去掉了,是不要的)




再从补码->反码->原码,由于是正数都是不变的,所以原码依然是 0000 0001

首位为符号位为0,即为正+

从右边往左边数第一个为1,表示2的0次方为1。

得到5 >> 2 值为+1。



5 << 2 表示 5的补码的bit位向左边移动两位,且末尾补0,即是乘以还是除以呢?答:(问题四)

原码 0000 0101

反码 0000 0101

补码 0001 0100 (这里注意末尾添加了00)



再从补码->反码->原码,由于是正数都是不变的,所以原码依然是 0001 0100

首位为符号位为0,即为正+

从右边往左边数第三个为1,表示2的2次方为4.

从右边往左边数第五个为1,表示2的4次方为16.

得到5 >> 2 值为+20。



-5 >> 2 表示 -5的补码的bit位向右边移动两位,即是乘以还是除以呢?答:(问题五)

原码 1000 0101

反码 1111 1010

补码 1111 1011



补码右移动两位且高位补1,即左边的数移动到右边时都为1得到 1111 1110(这里注意末尾的11被去掉了,是不要的)

补码移位完成后,还不是结果,还需要进行变换到原码,才是我们的看到的数值。

补码 1111 1110

反码 1000 0001 (保留首位即符号位,其余都取反1变为0,0变为1)

原码 1000 0010 (在反码的基础上末尾+1,(bit位是逢2进1再变0))

首位为符号位为1,即为负-

从右边往左边数第二个为1,表示2的1次方为2.

得到-5 >> 2 值为-2。



-5 << 2 表示 -5的补码的bit位向左边移动两位,即是乘以还是除以呢?答:(问题六)

原码 1000 0101

反码 1111 1010

补码 1111 1011



补码左移动两位且末尾补0,得到1110 1100(这里注意末尾加了00)

补码移位完成后,还不是结果,还需要进行变换到原码,才是我们的看到的数值。

补码 1110 1100

反码 1001 0011 (保留首位即符号位,其余都取反1变为0,0变为1)

原码 1001 0100 (再反码的基础上末尾+1,(bit位是逢2进1再变0))

首位为符号位为1,即为符-

从右边往左边数第三个为1,表示2的2次方为4.

从右边往左边数第五个为1,表示2的4次方为16.

得到-5 >> 2 值为4+16,20,且为符号得-20。



这里是讲的8位二进制能表示的十进制数范围为(28/2-1)~(-28/2)即[+127,-128],这些数都是8位二进制来玩儿如果移位时有bit位为1超出了第八位则表示溢出会丢失.

如果是两个字节16位二进制范围内的数(216/2-1)~(-216/2)即[32767,-32768],这些数都是16位二进制来玩儿如果移位时有bit位为1超出了第十六位则表示溢出会丢失.

二、HashMap中的变量常量用final修饰的好处

  • final修饰的变量是常量。可以让方法区中的常量池进行缓存,可复用。
  • final修饰即不可变,多线程环境下是共享的,即线程安全。(对于变量可以,对于方法不可以)
  • final修饰的方法,jvm会进行内联,提高java效率,在确定该方法类不会被继承则尽量使用final修饰。

三、HashMap中的变量和方法用static修饰的好处和坏处

好处:

可以使变量在类中实现共享,在多个类对象之间共享该变量,而不必每个对象都存储一个拷贝
static变量由JVM初始化,不需要显式初始化
static变量可以在类加载时就分配内存,减少了程序运行时才分配内存带来的性能损失

坏处:

由于static变量内存分配固定,当使用大量static变量时,会对系统性能造成压力;
static变量是全局的,一旦发生变动,会对系统产生不可预知的影响;
static变量不能被继承,派生类无法访问父类的静态变量。

四、JDK1.7中HashMap的结论如下

  • 数据结构是数组+单链表,整个数组对象为:Entry[]数组对象,链表对象为:Entry
  • HashMap的无参构造方法初始化一个数组table,大小为0。
  • HashMap的PUT方法首先会判断table的大小是否为0,如果为0则数组会进行扩容为16。
  • HashMap的有参构造方法指定的数组初始容量,HashMap会初始化一个大小大于等于n的二次方数的一个数组,为后续方便hash散列运算。例如6则hashmap会初始化为2^3 为8,21则hashmap会初始化为2^5 为32。
  • 理解哈希冲突,对于我们的hashcode()方法,同个对象hashcode值相同,不同对象可能hashcode相同,不同对象返回了hashcode值相同则就是hash冲突了。
  • hashmap中的put()方法,详情看下面标题!!!
  • 为什么数组的长度一定要是二次方数?二次方数和算数组下标是息息相关的,而源码中的与位运算恰好可以保证能取到所有索引下标,且比取模更快,提高计算索引下标的效率。
  • 为什么需要扩容?扩容可以使链表上的数据的key的hash值重新被计算,平移存放到原来索引下标位置(假设0)和扩容之后的索引下标位置(假设0)+原来容量中(假设16),让链表变短,提高存放、查询、删除元素的效率。
  • hashmap中的get()方法:
  • (1)、先判断key是否为null,如果为null则直接从数组索引下标为0的单链表进行遍历判断key是否为null是则返回值,不是则返回null。
  • (2)、如果key不为null则根据key进行hash运算得到数组下标再进行单链表从头部进行遍历Entry结点比较key值是否相等,相等则返回value值,遍历完了没有相等的则返回null。

五、JDK1.7中HashMap的put方法详解

  • 先判断是否为{}对象,是则进行扩容默认16.
  • 再判断如果key为null则直接放到索引下标为0的数组中,遍历,判断Entry的单链表中是否有key为null的值,有则返回旧值,并替换为新值,然后返回结束。如果没有key为null,则进行头插法,插入链表,且把插入的元素作为数组的第一个Entry结点。然后返回null结束.
  • 如果key不为null,再进行计算key的hash值,里面会有hashcode计算和自己实现的算法目的是尽量的使二进制的高位进行运算,让hash更加散列,提高随机性.
  • ​根据hash值和当前数组的容量得到新put加入元素需要放入的数组的索引下标.
  • 获取数组索引下标的Entry单链表对象,进行判断如果不为null,则进行遍历,当Entry对象中hash值相同、key的引用即内存地址值相等、key的equals比较相同(类似String作为key的话,重写了equals方法则会比较每个元素内容是否相同,如果没有重写,则调用的是Object的equals方法实际就是比较内存地址值),再获取原来Entry的value值,为oldValue。当前值覆盖原来的值,返回旧值结束.
  • 记录操作次数,这里是在进行删除的时候,判断并发修改异常的一个标志,所以,推荐遍历时使用stream流的filter过滤或者迭代器的遍历用迭代器的remove方法.
  • 如果单链表遍历完了,仍然没有相同的key,则先会判断是否需要扩容,再添加新Entry结点.
  • jdk1.7的hashmap是先判断原来的元素个数是否超过阈值,且是否存在hash冲突才需要扩容,再添加值,再size++,size是从0开始的。具体的扩容判断如下:
    hashmap中的扩容前的判断if ((size >= threshold) && (null != table[bucketIndex])),当且当添加13个元素时,就已经添加12个了,现在添加第13个时进行判断
    以前的元素个数size有12个大于等于阈值12,且当前key经过hash算法处理之后得到的数组下标的Entry不等于null,即当前数组下标存在一个单链表Entry对象则才进行扩容。
  • 终极结论是:Hashmap的扩容需要满足两个条件:当前数据存储的数量(即size())大小必须大于等于阈值;当前加入的数据是否发生了hash冲突。因为上面这两个条件,所以存在下面这些情况
  • (1)、就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。
  • (2)、当然也有可能存储更多值(超多16个值,最多可以存27个值)都还没有扩容。原理:前12个值全部hash碰撞组成一个单链表,存到数组下标的同一个位置(这时hashmap中的size是拿的以前的元素个数即11小于阈值12,不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,所以不会扩容),前面12+15=27,所以在存入第28个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。
  • 综上,在默认容量16,荷载因子为0.75,阈值是12时,扩容的最小元素个数为添加的第13个,最大元素个数为添加的第28个。
  • 扩容的具体内容为,使用的是头插法,获取索引下标的Entry对象,新创建一个Entry对象,且把原来的Entry对象当做next属性存放,并把新的Entry对象赋值给数组作为当前索引下标的第一个Entry结点,最后size++.然后返回null结束

六、公布上述答案

问题一:正

问题二:负

问题三:除以

问题四:乘以

问题五:除以

问题六:乘以


如果错误或者有优化的方面,请各位同学同志进行指点迷津哦!一起进步一起成长!也欢迎点赞·在看·转发哟!

七、最后

点赞

评论

关注我

END
下篇来临!

你可能感兴趣的:(java,面试,hashmap,hashmap源码,hashmap1.7)