四、深入理解Java中的HashMap「网易面试快答」

导读

前面文章一、深入理解-Java集合初篇 中我们对Java的集合体系进行一个简单的分析介绍,上两篇文章二、Jdk1.7和1.8中HashMap数据结构及源码分析三、JDK1.7和1.8HashMap数据结构及源码分析-续 中我们分别对JDK1.7和JDK1.8中HashMap的数据结构、主要声明变量、构造函数、HashMap的put操作方法做了深入的讲解和源码分析。本篇文章是前面几篇文章的后续,主要针对面试中常见问题的解答。

如果大家在面试中针对Java集合或者Java中的HashMap大家还有什么疑问或者其他问题,可以评论区告诉我。

简单介绍

JDK1.7—》哈希表,链表

JDK1.8—》哈希表,链表,红黑树— JDK1.8之后,当链表长度超过8使用红黑树。

非线程安全

0.75的负载因子,扩容必须为原来的两倍。

默认大小为16,传入的初始大小必须为2的幂次方的值,如果不为也会变为2的幂次方的值。

根据HashCode存储数据。

HashMap为什么使用数组?

数组在有下标的情况下时间复杂度是O(1)

hashcode % array. length = Index。

通过对象的hashcode取余数组的长度得到对象在数组中下标的位置,使查找某个对象所在位置的时间复杂度降低为O(1)。

在HashMap中默认的数组长度为16,如果要指定,
那么传入的参数须为2的n次方的值,
如果传入的初始长度不为2的n次方的值,
也会通过位移改变为大于传入值且是最小的2的n次方的值。
/**

 * The default initial capacity - MUST be a power of two.
默认的初始容量为16---为2的4次方

 */

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

变更传入值的大小,如果不为2的n次幂,则变更为2的n次幂。

如果传入的容量不为2的n次幂,则变更为大于传入数值且为2的n次幂的最小值。

例如:传入11,则大于11且大于2的n次幂的最小值为16.则把初始容量变更为16.

2的3次幂为8,2的4次幂为16. 2的5次幂为32.

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;

}

数组—》默认16长度的数组,最大长度2的30次幂,–hashcode

数组——》当有下标读取时候,时间复杂度是O(1)。增删的时候由于要移动数组中数组的位置,因此时间复杂度为O(n)。

hash碰撞(冲突),如何解决?

Object1.hashcode % array.length = Object2.hashcode % array. length

当出现Hash冲突的时候,把数组中的位置变为线性链表。

Hash碰撞的概率无法计算。
线性链表的特点:

  1. 查找慢,需要一个节点一个节点的访问,时间复杂度是O(n)。

  2. 增删快,时间复杂度是O(1)。

在JDK1.7中的线性链表插入方式为头部插入法。

在JDK1.8中的线性链表插入方式为尾部插入法。

链表—》头部插入法(JDK1.7),尾部插入法(JDK1.8),equles

由于线性链表的查找速度为0(n),因此当HashMap中数据越来越多的时候,Hash冲突的概率也越来越大,因此线性链表的长度也越来越长,性能也越来越低。为了解决这个问题,在JDK1.8之后,当线性链表的长度超过8之后,把线性链表转为红黑树。
红黑树的特性:

红黑树—》JDK1.8之后-链表长度超过8之后转为红黑树,左旋,右旋;

红黑树是接近于平衡的搜索二叉树。

红黑树确保最长长度不是最低长度的两倍。

性能均衡,查找,插入,删除等都是0(logn)的时间复杂度。

五个特性:

1.节点要么是红色、要么是黑色。

2.根节点必须是黑色。

3.每个叶子结点必须是黑色。

4.如果一个节点是红色,那么他的两个儿子必须是黑色。

5.对于任意节点而言,其到叶子节点树尾端指针的每条路径都包含相同数量的黑节点。

JDK1.8-之后长度超过8转变为红黑树,长度8的由来?

为什么这个长度是8.

因为统计学角度,一般很少hash的碰撞值会达到7.

泊松分布----> 根据泊松分布的概率统计学角度上。

在负载因子在0.75的时候,如果链表长度大于8之后的分布概率上来说,概率可以忽略不计了。

也就是说在大多数情况下是不会到达8转红黑树的。一亿分之六的概率。
/**

 * The load factor used when none specified in constructor.
默认的负载因素为不太精确的 0.75f构造

 */

static final float DEFAULT_LOAD_FACTOR = 0.75f;

链表转红黑树是链表长度达到阈值,这个阈值是多少?

阈值是8,红黑树转链表阈值为6

又为什么红黑树转链表的阈值是6,不是8了呢?

因为经过计算,在hash函数设计合理的情况下,发生hash碰撞8次的几率为百万分之6,概率说话。。因为8够用了,至于为什么转回来是6,因为如果hash碰撞次数在8附近徘徊,会一直发生链表和红黑树的转化,为了预防这种情况的发生。

为什么HashMap允许的最大容量为2的30次方?

/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
HashMap允许的最大容量。---必须为2的幂且小于2的30次方,传入过大的值,将被替换
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

1.“《”为左移运算符,1表示十进制中的“1”,30表示十进制数字1转化为二进制后向左移动30位。在数值上等同于2的30次幂。

2.为什么是2的20次幂?

以一个字节为例:
1《2=4.
0000 0001(十进制1—二进制展示)
向左移动两位后变成4
0000 0100(十进制4—二进制展示)

可见1《30等同于十进制中的2的30次幂。

回到题目,为什么会是2的30次幂,而不是2的31次幂呢?

首先:JAVA中规定了该 static final 类型的静态变量为int 类型,
至于为什么不是bytelong等类型,
原因是由于考虑到HashMap的性能问题而做的这种处理。

由于int类型的长度为4字节,也就是32个二进制位。按理说可以向左移动31位,
即231次幂。但是由于二进制数字中最高的一位,也就是最左边的一位是符号位,
用来表示正负之分(0为正,1为负),所以只能向左移动30位,而不能移动到最高位。

为什么Hashmap长度保证2的n次幂

Hashmap默认初始长度16,后续每次加入的值都是2的指数次幂的值。

如果传入的值不是2的指数次幂,则变成大于这个值的最接近的2的指数次幂的值。

变更传入值的大小,如果不为2的n次幂,则变更为2的n次幂。
如果传入的容量不为2的n次幂,则变更为大于传入数值且为2的n次幂的最小值。

例如:传入11,则大于11且大于2的n次幂的最小值为16.则把初始容量变更为16.

2的3次幂为8,2的4次幂为16. 2的5次幂为32.

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;

}

计算hash的时候不是用取模,而是用二进制按位与,然后计算hash值进入对应的值。

取模运算很消耗时间。 位运算效率更高。
位运算的前提 length的值必须是2的指数次幂。

0 <= hash&(lehth-1) < 16

让hash与长度按位与的时候都在0-16之间。

二进制位运算展示(64为,高位0,低位是值)

Hash 101010100 0111 1100 -hash值

length 000000000 0001 0000 -height16 -初始化长度

length-1 000000000 0000 1111 -height-1 =15 -数组下标

hashkey & (length -1)

不管hash值如何变,那么只需要看低位即可进入对应的hash数组。

HashMap扩容机制-为什么负载因子默认为0.75f?

负载因子0.75 如果容量大大0.75则扩容为原来的两倍。

扩容因此 0.75

空间利用率和时间效率在0.75的时候达到了平衡。

在统计学上0.693是最佳的选择。然后可能更想着有空间利用率,而且在。Net语言中 hashmap的负载因子是0.7.

针对Java集合或者Java中的HashMap大家还有什么疑问或者其他问题,可以评论区告诉我。

往期文章链接

Java集合

一、深入理解-Java集合初篇

二、Jdk1.7和1.8中HashMap数据结构及源码分析

三、JDK1.7和1.8HashMap数据结构及源码分析-续
Java-IO体系

一、C10K问题经典问答
二、java.nio.ByteBuffer用法小结
三、Channel 通道
四、Selector选择器
五、Centos-Linux安装nc
六、windows环境下netcat的安装及使用
七、IDEA的maven项目的netty包的导入(其他jar同)
八、JAVA IO/NIO
九、网络IO原理-创建ServerSocket的过程
十、网络IO原理-彻底弄懂IO
十一、JAVA中ServerSocket调用Linux系统内核
十二、IO进化过程之BIO
十三、Java-IO进化过程之NIO
十四、使用Selector(多路复用器)实现Netty中Reactor单线程模型
十五、使用Selector(多路复用器)实现Netty中Reactor主从模型
十六、Netty入门服务端代码

如需了解更多更详细内容也可关注本人CSDN博客:不吃_花椒

Java集合还需要学习的内容

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值