这篇文章记录一下hashmap的学习过程,文章并没有涉及hashmap整个源码,只学习一些重要部分,如有表述错误还请在评论区指出~

基本概念

Hashmap采用key算hash映射到具体的value,因此查找效率为o(1),为防止hash冲突,在数组的基础上加入链表、红黑树,为无序非线程安全的存储结构

jdk1.8之前采用以下方式存储数据:

左边实际上就是一个数组,右边则是key值相同的元素放到同一个链表中(图片侵删)

但是这种数组加单链表也存在问题,即单链表长度过长时,搜索值将耗费时间复杂度为o(n),因此jdk1.8中提出数组+链表+红黑树的方法

源码解析

该类是实现map接口的,并且也支持序列化、支持浅拷贝

构造方法

第一种可以自己指定容量大小与负载因子,那么此时阈值已经确定,使用tableSizefor来找到大于等于指定容量的最小2的次方数作为阈值,其中输入的值先-1,保证返回的值要大于等于输入值

第二种可以仅指定容量,使用默认的负载因子,此时也会初始化阈值

第三种使用默认的容量16以及默认的负载因子0.75

第四种是由map来创建一个hashmap,使用默认的负载因子,以及能够将map放进hashmap的容量创建(不常用)

默认容量1左移4位位16,这里容量大小必须为2的次方,很有讲究 ,后面解释原因

最大容量为2的30次方

默认的负载因子0.75,和扩容相关,主要表示当前hashmap的填充度

node表,真正存储元素的表,为2的次方,其为hashmap的一个内部类

    static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //key的hash值
final K key;
V value;
Node<K,V> next; //存储下一个节点的地址 Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

<key,value>元素的个数,包括数组中的和链表中的元素

关键方法

put方法,放入键值对:

首先将放入的键计算hash,然后调用putVal方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
     //如果当前hash表为空,即还没有放入任何元素,则进行扩容操作,相当于初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
     //根据当前key的hash算出当前元素应该放到hash表中的下标,如果改位置为null,则放入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k; //否则发生hash冲突,并且如果当前位置元素的hash和要放入元素的hash相同并且当前元素的key和要放入的key一样,则暂时保存当前冲突的node节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//若仅仅键的hash一样,但是key并不一样则首先判断是否是红黑树节点,如果是的话则将当前的键放进红黑树中,更新当前的hash表的冲突节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//否则当前节点为链表
        else {
//遍历链表(因为我们之前已经知道每个node节点都存储了下一个节点的地址,所以P.next变量即代表相对于当前node的下一个node,那么遍历到一个链表的尾部放入新的节点即可)
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 //放入后判断,如果当前hash表的长度>=7,则将当前hash位置处转为红黑树表示从而替换链表表示
treeifyBin(tab, hash);
break;
}
//如果遍历过程中发现链表中存在相同的key则break退出
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; //否则更新p节点为e,从而实现循环遍历链表
}
}
       //如果保存冲突节点的e变量不为null,则取冲突的值,根据onlyIfAbsent没有设置或者当前value为null,都将
if (e != null) { // existing mapping for key
V oldValue = e.value; //取到冲突节点的value
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //hashmap修改次数,防止多线程冲突的
if (++size > threshold) //判断当前node节点的多少有没有到扩容的阈值
resize();
afterNodeInsertion(evict);
return null;
}

所以整个put的流程为:

①.首先根据要放入的key计算hash,然后根据hash获取table中的放入位置,如果当前table为空,则进行初始化

②.判断放入位置是否为空,为空则直接放入,否则判断是否为红黑树节点,不是则为链表,则遍历链表查找是否存在相同的key,没找到则放入链表尾部并判断是否需要转为红黑树(TREEIFY_THRESHOLD)

③.若查找链表找到相同key则替换,放入后要判断node节点数是否超过threshold,判断是否需要resize

resize方法,扩充当前容量:

final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //保存旧的hash表
int oldCap = (oldTab == null) ? 0 : oldTab.length; //判断hash表的长度,若是第一次初始化则为0
int oldThr = threshold; //取旧的阈值
int newCap, newThr = 0; //定义新的长度和阈值
if (oldCap > 0) { //如果之前长度大于零
if (oldCap >= MAXIMUM_CAPACITY) { //如果之前的长度大于等于2的30次
threshold = Integer.MAX_VALUE; //则将node节点阈值设置为2的31次-1
return oldTab; //返回旧的hash表,不再扩容
}
        //否则满足扩容条件,进行扩容
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && //如果旧的容量扩大一倍小于2的30次并且旧的容量大于默认的初始化容量大小16,阈值也变为原来的2倍
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold 则容量扩大一倍
}
//如果旧的容量为0,但是旧的阈值大于零,则可能是初始化hashmap时指定了容量,则直接将新的容量设置为旧的阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//对于没有设置初始容量的情况
else { // zero initial threshold signifies using defaults //如果是第一次初始化,则设置容量为16,阈值为16*0.75=12,即hashmap可以放12个node节点
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新的阈值为0,则进行修正,令新的阈值为新的hash表容量长*负载因子
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//设置完新的容量和新的阈值后,则开始进项node节点元素转移
threshold = newThr; //先将新生成的阈值赋值给成员变量threshold
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //然后声明一个新的节点数组,容量即为扩充后的大小
table = newTab; //替换成员标量table为新表
if (oldTab != null) { //遍历旧的容量大小,取其每个node节点
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) { //如果该节点不为null
oldTab[j] = null; //则让旧表的该位置为null,进行垃圾回收
if (e.next == null) //如果当前遍历的节点下一个为null,说明为尾节点(单个node节点,无链表,无红黑树)
newTab[e.hash & (newCap - 1)] = e; //则直接将该节点放到新的hash表中
//如果下一个节点不为null,则判断当前节点是否是红黑树节点,若是,则将新标的该节点转为红黑树节点
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//否则为单链表节点,则遍历当前链中的节点决定要放入新hash表的位置
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) { if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

这里设计很妙,原来的容量为2的次方,则只有1位为1,原来的下标是容量-1,则新增的一位bit,决定了节点hash新增的一位为1还是为0,来决定其存放位置,其也为随机的,从而均匀地将节点放到新的hash表中,新增一位为0则放到低位中,即索引值不变,新增一位为1,则放到高位中,这样原本在一条链中的节点就能够分布到两条链上,也减少了搜索的开销

jdk1.7和1.8的Hashmap区别

1.jdk1.7中发生hash冲突新节点采用头插法,1.8采用的为尾插法

2.1.7采用数组+链表,1.8采用的是数组+链表+红黑树

3.1.7在插入数据之前扩容,而1.8插入数据成功之后扩容

总结

1.在算key的hash时将key的hashcode和与hashcode的高16位做异或降低hash冲突概率

2.HashMap 的 bucket (数组)大小一定是2的n次方,便于后面等效取模以及resize时定节点分布(low或者high)

3.HashMap 在 put 的元素数量大于 Capacity * LoadFactor(默认16 * 0.75)=12 之后会进行扩容,负载因子大于0.75则会减小空间开销,

4.影响hashmap性能的两个参数就是负载因子和初始容量,扩容影响性能,因此最好能提前根据负载因此估算hashmap大小,扩容实际上是将当前node节点放入一个新的node数组

5.tab[i = (n - 1) & hash] 实际上用与运算代替取模操作,性能更好,n即为容量大小,n为2的次方,则n-1则其二进制位为全1,从而代替模运算,e.hash & oldCap 用与运算决定hash增加的一位为0或者为1

关于负载因子设置:

负载因子的大小决定了HashMap的数据密度。
负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数

Linkedhashmap

简单了解一下linkedhashmap,它直接继承自hashmap,大部分方法与hashmap相同,不过结构有所变化

1.由hashmap的单链表变为双向链表(有头指针和尾指针)

2.在accessOrder为true的情况下(默认为false,即默认不按访问顺序排序),put,get方法取完的节点放到链表尾部,按照Lru(最近最长时间未使用的方法进行排列,也便于删除最老的节点),保证遍历顺序和插入顺序一致(后插入的一定在后面遍历到,先插入的先遍历)

3.LinkedHashMap.Entry继承至HashMap的静态内部类HashMap.Node

头节点和尾节点的说明,以及accessOrder实际上就定义了从hashmap的get方法拿到要寻找的节点后,是否要放到尾部

如果要研究linkedhashmap的具体三个回调函数,hashmap中留给子类linkedhashmap去实现的

参考

https://zhuanlan.zhihu.com/p/72296421

https://juejin.im/post/5aa47ef2f265da23a0492cc8#heading-4

https://blog.csdn.net/zxt0601/article/details/77429150

https://tech.meituan.com/2016/06/24/java-hashmap.html

https://blog.csdn.net/wangyi1225/article/details/99705173

https://blog.csdn.net/qq_36520235/article/details/82417949 1.7和1.8区别

https://blog.csdn.net/justloveyou_/article/details/52464440  == equals hashcode区别

从JDK源码学习Hashmap的更多相关文章

  1. 由JDK源码学习HashMap

    HashMap基于hash表的Map接口实现,它实现了Map接口中的所有操作.HashMap允许存储null键和null值.这是它与Hashtable的区别之一(另外一个区别是Hashtable是线程 ...

  2. JDK源码学习笔记——LinkedHashMap

    HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序. LinkedHashMap保证了元素迭代的顺序.该迭代顺序可以是插入顺序或者是访问顺序.通过维护一个 ...

  3. JDK源码学习--String篇(二) 关于String采用final修饰的思考

    JDK源码学习String篇中,有一处错误,String类用final[不能被改变的]修饰,而我却写成静态的,感谢CTO-淼淼的指正. 风一样的码农提出的String为何采用final的设计,阅读JD ...

  4. JDK源码学习系列05----LinkedList

                                             JDK源码学习系列05----LinkedList 1.LinkedList简介 LinkedList是基于双向链表实 ...

  5. JDK源码学习系列04----ArrayList

                                                                             JDK源码学习系列04----ArrayList 1. ...

  6. JDK源码学习系列03----StringBuffer+StringBuilder

                         JDK源码学习系列03----StringBuffer+StringBuilder 由于前面学习了StringBuffer和StringBuilder的父类A ...

  7. JDK源码学习系列02----AbstractStringBuilder

     JDK源码学习系列02----AbstractStringBuilder 因为看StringBuffer 和 StringBuilder 的源码时发现两者都继承了AbstractStringBuil ...

  8. JDK源码学习系列01----String

                                                     JDK源码学习系列01----String 写在最前面: 这是我JDK源码学习系列的第一篇博文,我知道 ...

  9. 【JDK】JDK源码分析-HashMap(2)

    前文「JDK源码分析-HashMap(1)」分析了 HashMap 的内部结构和主要方法的实现原理.但是,面试中通常还会问到很多其他的问题,本文简要分析下常见的一些问题. 这里再贴一下 HashMap ...

随机推荐

  1. 【.net core】电商平台升级之微服务架构应用实战

    一.前言 这篇文章本来是继续分享IdentityServer4 的相关文章,由于之前有博友问我关于微服务相关的问题,我就先跳过IdentityServer4的分享,进行微服务相关的技术学习和分享.微服 ...

  2. MATLAB神经网络(5) 基于BP_Adaboost的强分类器设计——公司财务预警建模

    5.1 案例背景 5.1.1 BP_Adaboost模型 Adaboost算法的思想是合并多个“弱”分类器的输出以产生有效分类.其主要步骤为:首先给出弱学习算法和样本空间($X$,$Y$),从样本空间 ...

  3. js 实现简易留言板功能

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  4. 动手建立jdbc连接

    工具:Idea  Navicat 环境:jdk 1.8  mysql-5.7.27-winx64 创建一个project 打开navicat开启连接. 在idea中导入数据库. 导入好后可以开始连接了 ...

  5. linux无文件执行— fexecve 揭秘

    前言 良好的习惯是人生产生复利的有力助手. 继续2020年的flag,至少每周更一篇文章. 无文件执行 之前的文章中,我们讲到了无文件执行的方法以及混淆进程参数的方法,今天我们继续讲解一种linux上 ...

  6. 如何用 Blazor 实现 Ant Design 组件库

    本文主要分享我创建 Ant Design of Blazor 项目的心路历程,已经文末有一个 Blazor 线上分享预告. Blazor WebAssembly 来了! Blazor 这个新推出的前端 ...

  7. SQL Server2008执行脚本

    "C:\Program Files\Microsoft SQL Server\100\Tools\Binn\osql.exe" -E -i C:\Users\zhiheng\Des ...

  8. MyBatis框架——多表查询

    MyBatis多表查询, 从表中映射主表,使用 association 标签,通过设置 javaType 属性关联实体类: 主表映射从表,使用 collection 标签,通过 ofType 属性关联 ...

  9. ElegantSnap 一个优雅的,易用的iOS/tvOS/macOS自动布局框架, 超级详细的使用教程,多视图水平等宽/垂直等高排列

    ElegantSnap ElegantSnap(Base on SnapKit) to make Auto Layout easy and elegant on both iOS and OS X. ...

  10. MySQL笔记(4)-- 索引优化

    索引失效情况: 最佳左前缀法则:如果索引了多列,要遵循最左前缀法则,指的是查询从索引的最左前列开始并且不跳过索引中的列:[覆盖索引有a,b,c,条件中使用了b或bc都导致该索引失效:如果条件使用了ac ...