HashMap底层原理分析
技术本质
数据结构:
JDK7:数组+链表
JDK8:数组+链表+红黑树
算法:哈希算法
//数组:采用连续的存储单元来存储数据
//特点:查询快,插入慢
链表:创建一个简单的链表
public class Node {
private Object data;
public Node next;
public Node(Object date){
this.data=date;
}
//链表:链表是一种物理存储单元上非连续、非顺序的存储结构
//特点:插入、删除时间复杂度 o(1)查询遍历时间复杂度o(N)
//总结:插入快、查询慢(查询时要通过遍历链表来查询)
public static void main(String[] args) {
Node head=new Node("sovzn");//头节点
head.next=new Node("张三");
head.next.next=new Node("李四");
/*
|sovzn|---->next---->|张三|---->----|李四|
地址并非要连续,因为存在next引用
*/
System.out.println(head.data);
System.out.println(head.next.data);
System.out.println(head.next.next.data);
}
}
sovzn
张三
李四
Process finished with exit code 0
HashMap 源码中的链表Node(1.7中的是 Entry ):
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //hash值
final K key; //key值
V value; //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;
}
ArrayList底层采用的就是数组:所以ArrayList具有查询快,插入慢的特点
LinkedList底层采用的是链表:而且是双向链表(next、prev),所以其特点为插入快、查询慢
//LinkedList中的Node源码
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
Hash算法
哈希算法(也叫散列),就是把任意长度值(key)通过散列算法变换成固定长度的key(地址),通过这个地址进行访问的数据结构。
它通过把关键码值映射到一个表中。位置来访问记录,以加快查询的速度。
Hashcode
Hashcode:通过字符串算出它的ASCII码,进行Mod(取模,节省空间),算出哈希表中的下标。
Hash碰撞
哈希冲突(碰撞) 解决哈希冲突的方法:
可以通过链表,如果两个值的HashCode相等,取模后必然也相等,此时可以在产生冲突的数组位置形成链表,通过next指向新存储的值。
JDK8使用红黑树的原因
在JDK8中HashMap源码中有这一属性:
链表的阈值为8:
static final int TREEIFY_THRESHOLD = 8;
在put方法中有如下源码:
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
当链表的长度大于TREEIFY_THRESHOLD - 1(之所以是TREEIFY_THRESHOLD - 1,即7,因为链表的索引值binCount也是从0开始计算的)时。执行 treeifyBin(tab, hash);也就是让这个链表树化,将其结构转为红黑树。
将链表转为红黑树的目的:
以为在执行查询操作时,对于链表,是通过遍历的方法来进行查询的,如果链表过长的话,查询效率会非常低,而红黑树是一种树结构,查询效率要比链表高,但是对于链插入操作而言,红黑树的效率要比链表低。
总结:
常见问题
1.HashMap是如何定位下标的?
先获取Key,然后对Key进行hash,获取一个hash值,然后用hash值对HashMap的容量进行取余(实际上不是真的取余,而是使用按位与操作),最后得到下标。
2.HashMap由什么组成?
数组+单链表,jdk1.8以后又加了红黑树,当链表节点个数超过8个(m默认值)以后,开始使用红黑树,使用红黑树一个综合取优的选择,相对于其他数据结构,红黑树的查询和插入效率都比较高。而当红黑树的节点个数小于6个(默认值)以后,又开始使用链表。这两个阈值为什么不相同呢?主要是为了防止出现节点个数频繁在一个相同的数值来回切换,举个极端例子,现在单链表的节点个数是9,开始变成红黑树,然后红黑树节点个数又变成8,就又得变成单链表,然后节点个数又变成9,就又得变成红黑树,这样的情况消耗严重浪费,因此干脆错开两个阈值的大小,使得变成红黑树后“不那么容易”就需要变回单链表,同样,使得变成单链表后,“不那么容易”就需要变回红黑树。
3.HashMap往链表里插入节点的方式?
jdk1.7以前是头插法,jdk1.8以后是尾插法,因为引入红黑树之后,就需要判断单链表的节点个数(超过8个后要转换成红黑树),所以干脆使用尾插法,正好遍历单链表,读取节点个数。也正是因为尾插法,使得HashMap在插入节点时,可以判断是否有重复节点。
4.HashMap默认容量和负载因子的大小是多少?
jdk1.7以前默认容量是16,负载因子是0.75。
5.HashMap初始化时,如果指定容量大小为10,那么实际大小是多少?
16,因为HashMap的初始化函数中规定容量大小要是2的指数倍,即2,4,8,16,所以当指定容量为10时,实际容量为16。
6.哈希值相同,对象一定相同吗?对象相同,哈希值一定相同吗?
不一定。一定。 (联想hash冲突)
7.HashMap的扩容与插入元素的顺序关系?
jdk1.7以前是先扩容再插入,jdk1.8以后是先插入再扩容。
8.HashMap扩容的原因?
提升HashMap的get、put等方法的效率,因为如果不扩容,链表就会越来越长,导致插入和查询效率都会变低。
9.jdk1.8引入红黑树后,如果单链表节点个数超过8个,是否一定会树化?
不一定,它会先去判断是否需要扩容(即判断当前节点个数是否大于扩容的阈值),如果满足扩容条件,直接扩容,不会树化,因为扩容不仅能增加容量,还能缩短单链表的节点数,一举两得。
10.HashMap满足扩容条件的大小(即扩容阈值)怎么计算?
扩容阈值=容量*加载因子
假设,我们直接new hashmap,不指定任何的参数,那么默认这个hashmap的容量就为16,加载因子为0.75,当hashmap的大小大于或者等于扩容阈值即容量 * 加载因子0.75 ,也就是12时,就会把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作。
方法流程
jdk1.7
put:
- 判断数组是否为空,如果为空进行初始化,初始化的是数组容量
- 判断key是否为空,执行方法,key为null存在index为0的位置
- 根据key得到hash,对key进行hashcode
- 根据hash值和容量得到下标,indexFor方法
- 覆盖逻辑,遍历链表,短路与先判断hash值是否相等,再判断key,value覆盖,返回oldvalue
- addEntry(hash,key,value,i),先有(判断扩容),再根据四个值,头插法或尾插法插入
get:
- 判断key是否为空 ,如果为空,直接取出index为0的位置的值
- 根据key获得entry,计算hash值,得到下标,遍历链表,先比较hash再比较key,就得到了
- 根据entry获得value
1.7和1.8流程差不多。只是在执行put和get时先判断是链表还是红黑树,并执行各自的插入和读取方法。
- Post link: http://sovzn.github.io/2021/03/25/HashMap%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%8601/
- Copyright Notice: All articles in this blog are licensed under unless otherwise stated.
若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub Issues