导读
前面文章一、深入理解-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碰撞的概率无法计算。
线性链表的特点:
-
查找慢,需要一个节点一个节点的访问,时间复杂度是O(n)。
-
增删快,时间复杂度是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 类型,
至于为什么不是byte、long等类型,
原因是由于考虑到HashMap的性能问题而做的这种处理。
由于int类型的长度为4字节,也就是32个二进制位。按理说可以向左移动31位,
即2的31次幂。但是由于二进制数字中最高的一位,也就是最左边的一位是符号位,
用来表示正负之分(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集合
三、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集合还需要学习的内容