面试必备1:HashMap(JDK1.8)增、删、改、查以及扩容源码分析
首先附上我的几篇其它文章链接感兴趣的可以看看,如果文章有异议的地方欢迎指出,共同进步,顺便点赞谢谢!!!
Android framework 源码分析之Activity启动流程(android 8.0)
Android studio编写第一个NDK工程的过程详解(附Demo下载地址)
面试必备2:JDK1.8LinkedHashMap实现原理及源码分析
Android事件分发机制原理及源码分析
View事件的滑动冲突以及解决方案
Handler机制一篇文章深入分析Handler、Message、MessageQueue、Looper流程和源码
Android三级缓存原理及用LruCache、DiskLruCache实现一个三级缓存的ImageLoader
HashMap概述:
在分析HashMap的源码前,需要了解一下几个问题。
面试必备2LinkedHashMap的源码分析(基于JDK1.8)
1:HashMap的数据结构
我们需要先知道HashMap是基于什么样的数据结构来进行数据存储的,知道了这些我们再去看源码就容易的多。
散列表(哈希表) 我们常用的数据结构就是数组和链表,数组具有增删慢查找快的特点,而链表具有增删快查找慢 的特点;基于上述特点,HashMap 即想要查询效率快,又想增删效率高,基于这样的特点HashMap的数据结构就是通过数组和链表组成的散列链表。
一直到JDK7为止,HashMap的结构都基于一个数组以及多个链表的实现,hash值冲突的时候,就将对应节点以链表的形式存储;JDK8中,HashMap采用的是数组(位桶)+链表/红黑树组成,当同一个hash值的节点数不小于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树,提高查询效率,这就是JDK7与JDK8中HashMap实现的最大区别。
2:存储节点 : Node 和TreeNode
HashMap存储数据时如何组织出这样一个散列表呢?HashMap中将我们存入的每一个数据封装成一个Node类(Node节点),Node应该有以下下属性 :key--------键、value-----值 组成我们put的键值对,既然要组织起链表则必须有Node next属性、还有一个根据key算出的int 类型hash值,hash值用来确定该该节点所在数组的索引后面会进行详细描述。
/**
* 散列表的节点 Node 部分源码
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//用于确定该节点所在数组的位置, 下标
final K key;// 我们所操作的键
V value;// 值
Node<K,V> next;//存储下一个节点,通过next组织起链表
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;//位置
this.key = key;
this.value = value;
this.next = next;
}
/**
* 返回当前节点的根节点
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
当链表长度大于8时将链表转换成红黑树,红黑树节点TreeNode部分源码
static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left;//左子树
TreeNode<K,V> right;//右子树
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;//颜色属性
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
当我们放入或取出元素时大致流程是:
- 现根据hash函数去求出key的Hash值,至于求hash值得算法将在后面进行详细分析
- 根据上一步的hash值可以确定在数组的哪个位置,定下标(数组的索引)
- 根据索引从数组中获取数据,如果为NULL 则将该元素直接放入或取出(直接在数组中操作),如果该位置数据不为NuLL,则需要向该元素所在的链表进行数据的操作。
哈希冲突(哈希碰撞)
如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。
3:HashMap中散列表中数组(或位桶)如何表示?
HashMap源码中与数组相关的几个成员属性:
- 数组的表示:
transient Node<K,V>[] table;
- 数组的大小 : 数组的初始化需要一个指定的大小,在HashMap中 数组的大小永远是 2的幂次方(原因将在源码中分析)
- 数组的默认大小:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 , 1 << 4 二进制中1左移四位就是2^4=16, 不直接写16的原因是,效率高,计算机最终读取的二进制
; - 数组的最大容量:
static final int MAXIMUM_CAPACITY = 1 << 30//最大为2^30,使用位移操作符的原因也是效率高 ;
- 数组扩容因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f//用于确定数组扩容的临界值;
- 以存放数据大小:
transient int size//记录 hashMap 当前存储的元素的数量;
- 数组扩容的临界值:
int threshold;//数组已使用量size>threshold时需要扩容,threshold=数组大小*扩容因子提升效率,没有必要等到数组都用完了在进行扩容,每次扩大2倍
**注意:数组的扩容是在当已使用量大于数组容量*扩容因子(默认0.75)**时进行扩容
4:HashMap中散列表中链表的长度限制:长度越长存储查询效率低的问题
链表越长,期查询效率越低,所以链表的长度也有一个限制,我们称为阈值 ,JDK1.8的时候,链表的长度大于阈值,将其结构改成一个红黑二叉树,以提升差值效率,同时也伴随者性能的损耗增加
HashMap源码中与链表相关的几个成员属性:
- 转换红黑树的阈值:
static final int TREEIFY_THRESHOLD = 8;//阈值 链表越深查找存储效率都低 ,超过8以后将其转换成红黑二叉树 ,提升查找效率
- 转换成链表的阈值
static final int UNTREEIFY_THRESHOLD = 6; //当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
static final int MIN_TREEIFY_CAPACITY = 64;//当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。
6:新的数据封装成的Node对象如何去存储:
根据key计算hashCode ----》int类型的值,hash(key)然后就知道要存在数组的那个位置