文章目录
HashMap源码阅读
介绍:
- 继承自Map.Entry<K,V>
- 无序、允许为null、非同步
结构图:
(https://raw.githubusercontent.com/iszhonghu/Picture-bed/master/img/20200702162547.png)
分析
参数
- 默认容量16
- 最大容量2的31次方
- 默认的填充因子0.75(太大查询效率低,太小数组利用率低)
- 当bucket上的结点大于8时会转成红黑树
- 当bucket上的结点小于6时会转换为链表
- 树化参数64,当小于64时只是简单的扩容,大于64是会树化
- Node:静态内部类,用来表示键值对
- TreeNode:红黑树节点
关键概念
-
存储容器
-
因为hashmap内部是用一个数组来保存内容的,数组定义如下
transient Node<K,V>[] table;
-
-
Node类型
-
table是一个Node类型的数组,Node是其中定义的静态内部类,主要包含hash、key、value、和next属性
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; }
-
-
TreeNode
- 当桶内的链表达到8的时候,会将链表转化为红黑树,就是TreeNode类型,也就是HashMap中定义的静态内部类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; }
常见问题
1、HashMap1.7和1.8的区别
- 数据结构不同
- 1.7是数组加链表
- 1.8是数组加链表或红黑树
- 链表插入顺序不同
- 1.7是头插入
- 1.8是链表尾插入
- hashcode抖动次数不同
- 1.7进行了四次异或运算
- 1.8进行了一次异或运算
- 扩容
- 1.7扩容时重新hash
- 1.8扩容时优化,只需要看原来的hash值心在的那个bit是1还是0,是0的话索引没变是1的话索引变成原索引+原来数组的大小
哈希表结构(链表散列:数组+链表),结合数组和链表的有点,当链表长度超过8时,链表转化为红黑树
2、HashMap 工作原理
HashMap的底层是hash数组和单向链表实现的,数组中的每个元素都是链表,由Node内部类(实现Map.Entry接口)实现,HashMap通过put&get方法存储和获取
存储对象的时候,将k/v传给put()方法:
- 初始化table:判断table是否为空或者为null
- 计算hash值:通过高16位和低16位的异或运算(让高16位也参入进来以便减低冲突)
- 插入或更新节点:根据(n-1)&hash计算得到插入的数组下标i然后进行判断
- table[i]==null:没有hash冲突直接新建节点添加
- table[i]!=null:判断首个节点和key是否一样
- 相同:直接更新value
- 不相同:判断是否是红黑树
- 红黑树:直接在树中插入键值对
- 链表:遍历链表判断是否已经存在此key:
- 存在更新
- 不存在就插入(1.8是尾插入),然后判断是否转为红黑树
- 扩容:根据size和数组长度*负载因子来判断是否需要扩容
获取对象时,将 K 传给 get() 方法:
- 调用 hash(K) 方法(计算 K 的 hash 值)从而获取该键值所在链表的数组下标;
- 确定桶的位置后,会出现三种情况
- 单节点类型:只要不发生哈希碰撞就是这种类型
- 链表类型:遍历链表,知道找到key相等的Node
- 红黑树类型:使用红黑树专用的快速查找方法
hashCode 是定位的,存储位置;equals是定性的,比较两者是否相等。
3、为什么要一起重写hashCode()和equal()方法
- 假设都不重写
这样hashMap就可以存在key相同的值
- 只重写hashCode
无法正确地与链表元素进行相等判断,从而无法保证去重
- 只重写equals()方法
映射到HashMap中不同数组下标,无法保证去重
4、当两个对象的hashCode相同会发生什么
因为hashCode相同,但是不一定就是相等的(需要根据equals方法比较),所以两个对象所在数组的下标相同,碰撞就此发生。
5、hash的实现
在jdk1.8中,是通过hashCode()的高16位异或低16位实现的:(h=k.hashCode())^h(>>>16),主要从速度、功效和质量来考虑的,减少系统开销,也不会造成因为高位没有参数下标的计算从而引起的碰撞
6、异或运算符的好处
保证了对象的hashCode32位值只要有一位发生改变,整个hash()返回值就会改变,尽可能减少碰撞
7、数组扩容的过程
创建一个新的数组,其容量为旧数组的两倍,并重新计算旧数组中结点的存储位置。
结点在新数组中的存储位置只有两种:原下标或者原下标+旧数组的大小
8、HashMap的table的容量是如何确定的,以及loadFactory是什么,该容量如何变化,这种变化会带来什么问题
- table的数组大小是由capacity这个参数来确定的,默认16,也可以在构造时传入,最大限制1<<30
- loadFactory是装载因子,主要用来确定table数组是否需要动态扩展,默认是0.75。
- 扩容时,调用resize()方法,将长度变为原来的两倍(是table的长度,而不是threshold)
- 如果数据很大的情况下,扩展时会带来性能的损失,在性能要求很高的地方,这种损失是致命的。
9、拉链法导致链表过深问题为什么使用红黑树不选择二叉树,为啥不一直使用红黑树
因为二叉查询树在特殊情况下回变成一条线性结构(造成一样的问题),导致遍历查询缓慢,而红黑树可以解决这个问题。
红黑树在插入新数据后,可能需要左旋、右旋、变色这些操作来保持平衡,但是为了保持平衡需要付出一些代价,所以在链表长度较小(8)时,不使用红黑树
10、红黑树的介绍
- 每个结点非红即黑
- 根节点总是黑色的
- 如果节点是红色的,则其子节点必须是黑色的
- 每个叶子节点都是黑色的空节点(NIL节点)
- 从根节点到叶子节点或空节点的每条路径,必须包含相同数目的黑色节点
11、HashMap、LinkedHashMap、TreeMap有什么区别与使用场景
HashMap:(使用最多)
- 区别:
- 使用场景:在map中插入、删除和定位元素
LinkedHashMap:
- 区别:保存了记录的插入顺序,在用Iterator遍历时,先取到的记录肯定是先插入的,遍历比HashMap慢
- 使用场景:在需要输出的顺序和输入的顺序相同的情况下
TreeMap:
- 区别:实现SortMap接口,能够把它保存的记录根据键排序(默认升序)
- 使用场景:需要按自然顺序或自定义顺序遍历键的情况
12、HashMap和HashTable区别
- HashMap是线程不安全的,HashTable是线程安全的,导致效率也低下
- HashMap最多只允许一条记录的键为null,与韩旭旭多条记录的值为null,而HashTable不允许
- HashMap默认初始化数组大小为16,扩容为两倍、HashTable为11,扩容为两倍+1
最后
- 如果觉得看完有收获,希望能给我点个赞,这将会是我更新的最大动力,感谢各位的支持
- 欢迎各位关注我的公众号【java冢狐】,专注于java和计算机基础知识,保证让你看完有所收获,不信你打我
- 如果看完有不同的意见或者建议,欢迎多多评论一起交流。感谢各位的支持以及厚爱。