这篇文章记录一下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. zabbix图表出现中文乱码

    搭建完成Zabbix监控服务器之后,切换到中文语言,图表展示出现乱码,如图所示 按照网上流传的上传windows下的字体的方法,还是不行,最后发现是PHP编译时的问题: php在编译时开启了-enab ...

  2. (转)windows宿主机,ubuntu虚拟机下的上网设置(有线网络和无线网络)

    转自:http://hi.baidu.com/puppywst/item/d9f73734856e2af32684f4e3 虚拟机下ubuntu共享方式上网: 有线网络 在有线网络的条件下,vmwar ...

  3. LeetCode-最长回文串

    题目描述: 给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串. 在构造过程中,请注意区分大小写.比如 "Aa" 不能当做一个回文字符串. 注意:假设字符 ...

  4. 原创】Java并发编程系列2:线程概念与基础操作

    [原创]Java并发编程系列2:线程概念与基础操作 伟大的理想只有经过忘我的斗争和牺牲才能胜利实现. 本篇为[Dali王的技术博客]Java并发编程系列第二篇,讲讲有关线程的那些事儿.主要内容是如下这 ...

  5. 工作了这么长时间,是不是非用macbook pro不可呢?

    连续奋战了二十多天,肉眼可见自己的状态一天不如一天.总觉得自己是不是该放下一切好好休息两天,但是理智又告诉我不能停.不能停. 既然不能停,那就把之前攒下的文章写一写吧. 什么是macbook pro ...

  6. 欲善事先利器-IEAD插件篇

    工欲善其事,必先利其器,好鞋踢好球是非常合乎逻辑的事情. --<长江七号> 同样的开场白,不一样的酒,不一样的故事. 上篇<欲善事先利器--系统篇>已经推荐了一些个人常用的效率 ...

  7. docker系列详解<一>之docker安装

    1.Docker 要求 CentOS 系统的内核版本高于 3.10 ,查看本页面的前提条件来验证你的CentOS 版本是否支持 Docker . 通过 uname -r 命令查看你当前的内核版本 $ ...

  8. Java单例设计模式的实现

    1. 单例设计模式的定义 单例设计模式确保类只有一个实例对象,类本身负责创建自己的对象并向整个系统提供这个实例.在访问这个对象的时候,访问者可以直接获取到这个唯一对象而不必由访问者进行实例化. 单例设 ...

  9. 基于Docker搭建Nginx图片服务器

    前言 一般开发中,都会把图片上传到一个目录,然后将目录和文件名拼接存储在数据库中,但是,这种方法如果没弄好的话可能有一定的缺陷. 若项目搬迁,即时这台服务器本身还在用,存放在服务器的跟项目相关的图片也 ...

  10. thinkPHP渗透之经验决定成败

    如上图,目标就一个登陆框,最近 Thinkphp 程序很多,根据后台地址结构,猜测可能是 ThinkPHP ,随手输入 xxx 得到 thinkPHP 报错页面,确定目标程序和版本. 然后上 5.X ...