心得:如何学习源码:

从某个执行过程入手,建议先从整体入手,了解底层的数据结构是怎么一步一步优化的。最后,在了解完底层的数据结构优化过程后,从重要的核心方法入手,从它的执行流程入手,先去网上搜索了解它的执行流程过程(推荐看流程图),再去看源码。

(1) 从某个执行过程入手,建议先从整体入手,了解底层的数据结构是怎么一步一步优化的

比如虽然我们知道HashMap 底层数据结构是:数组+链表+红黑树,但是一开始研究的是增删改查某个流程的执行

它的整体思路是只用到数组作为哈希表存储数据,即key 根据 哈希函数,换算得到 索引,然后根据索引定位操作对应的 数组元素

但是存储过程发生了哈希冲突,在解决哈希冲突使用-链地址法,于是HashMap的底层结构才加入单链表

但是为提升效率,使用效率高的数据结构-红黑树,在链表的结点大于8且数组容量大于64,单链表转成红黑树,于是HashMap的底层结构才加入红黑树

(2) 最后,在知道了底层的数据结构优化过程后,从重要核心方法的执行流程入手,先去了解到它的执行流程 (推荐看流程图),再去看源码:

  • 比如先了解整明白put方法执行流程图:

  • 然后再使用idea 点击进入HashMap的put方法,自己去看源码:

一、HashMap 哈希表

1、 哈希表简单介绍:是【空间换时间】的典型应用

  • 哈希函数,也叫做散列函数
  • 哈希表内部的数组元素,很多地方也叫 Bucket(),整个数组叫 Buckets 或者 BucketArray

★ 2、(增删改查) 执行流程:

  • 比如增加流程:

增删改查的流程都是类似的:

利用哈希函数生成key 对应的索引 index 【O(1)】

根据 index 操作定位到的数组 元素 【O(1)】

put("Jack", 666);
put("Rose", 777);
put("Kate", 888);

  • 比如Key 是Object 类型,可以存储各种类型

★ 3、哈希冲突(哈希碰撞)

(1) 定义:2 个不同的 key,经过哈希函数计算出相同的结果(索引相同)

(2) 解决哈希冲突方法:

  • 开放定址法

例如上图,key="Rose" 经过哈希函数换算得到索引是03,然后将key对应的value放到索引为03的数组元素位置上;

此时,key="Kate" 经过哈希函数换算得到索引也是03,而03的元素位置已经被key="Rose"对应的value 占据了,那么 从索引03的下一个位置 开始往下找一个新的"坑",往下一个一个地找(线性探测),直到找到哈希表的元素(桶),桶是空的,然后放上key="Kate"对应的value

当然往下继续找空桶,可以按平方找 12、22、32、42 等等 (平方探测)

  • 再哈希法:设计了多个哈希函数

上图,按先来的先占坑,03索引的元素(桶)的"坑位" 已经被 key="Rose" 占据了,后来者key="Kate",换算得到的索引也是 03,而"坑位"已经被占据了,那么key="Kate",选择使用另外一个哈希函数,换算得到一个新的索引,直到可以成功占到"坑位"。

  • 链地址法: 比如通过链表将同一index的元素串起来

    put("Jack", 666);
    put("Rose", 777);
    put("Kate", 888);

★ 4、JDK1.8的哈希冲突解决方法:链地址法(数据结构:单链表+红黑树)

默认使用单向链表将元素串起来

添加元素的过程,可能会由单向链表转为红黑树来存储元素

比如当哈希表容量 ≥ 64 且 单向链表的节点数量大于 8 时

当红黑树节点数量少到一定程度时,会由红黑树转为单向链表

JDK1.8中的哈希表是使用链表+红黑树解决哈希冲突

为什么使用 单向链表,不使用双向链表呢?

为了节省内存空间,单向链表比双向链表少一个指针;而且每次都需要从头开始遍历,使用单向链表完全满足

至此,咱就已经知道HashMap的底层数据结构为什么是数组+链表+红黑树,

接下来,咱需要了解具体实现HashMap的细节:比如哈希函数的具体实现过程,哈希值怎么计算,增删改查的具体实现细节、哈希表的扩容等等

5、哈希函数(散列函数)的作用:将各种类型的key 换算成哈希表(数组) 的索引

(1) 哈希函数的具体实现过程:

① 首先根据 key,生成 哈希值(必须是整数-int 类型)

② 然后再根据 key 的哈希值和 数组的大小 进行相关运算,生成一个 索引值

    static final int hash(Object key) {
//return key.hashCode() % table.length;
// 为了提升运算效率,使用 & 位运算取代 % 运算(前提:数组长度设计为2的幂)
return key.hashCode() & (table.length - 1);
}

(2) 补充一下 与运算 &:

不管一个数 x 多大,x & 100, 最终的范围就是[0, 100]

  • 使用 & 位运算取代 % 运算(前提:数组长度设计为2的幂):是为了保证被与数全是1

    • 比如 2^1 是 10,2^1-1 是 00
    • 比如 2^2 是 100,2^2-1 是 11
    • 比如 2^3 是 1000,2^3-1 是 111
    • 比如 2^4 是 10000,2^4-1 是 1111

(3) 良好的哈希函数(哈希函数作用):

  • 让哈希值更加均匀分布减少哈希冲突次数 → 提升哈希表的性能

6、哈希值计算

  • 我们知道哈希函数第一步是根据key获取到哈希值,而且哈希值必须是整型;而key的类型是各种各样的,那么各种各样的key是如何获取到哈希值的呢?

不同种类的 key,哈希值的生成方式不一样,但目标是一致的

♢ 尽量让每个 key 的哈希值是唯一的

♢ 尽量让 key 的所有信息参与运算

在Java中,HashMap 的 key 必须实现 hashCode、equals 方法,也允许 key 为 null

(1) 数值类型-哈希计算:包括整型、Long类型、浮点型Float、Double类型

① 整型-哈希值计算
  • 整型的哈希值,已经满足哈希值是整数的特点了,所以整型的哈希值是它本身。

    public static int hashCode(int value){
    rerurn value;
    }
② 浮点型-哈希值计算
  • 将浮点数对应的二进制数转成整数

    • float 类型,直接调用:Float对象的floatToIntBits方法,直接将float类型的数据对应的二进制数转成整数
  public static int hashCode(int value){
rerurn Float.floatToIntBits(value);
}
  • double 类型,直接调用:Double 对象的doubleToIntBits方法,直接将double类型的数据对应的二进制数转成整数
细节:java 要求哈希值必须是int 类型,是32位的,对于 long(64位)、double(64位) 类型超过32位的都需要进行处理:
  • 将64位分成两部分,让 高32bit 和 低32bit 混合计算出 32bit 的哈希值,充分利用所有信息计算出哈希值。
// double 类型
public static int hashCode(int value){
long bits = Double.DoubleToIntBits(value);
rerurn (int)(bits ^ (bits >>> 32));
} //long 类型
public static int hashCode(int value){
rerurn (int)(value^(value >>> 32));
}

♢ 无符号的位运算>>> 和 异或运算^ 作用?

将64位分成两部分,让 高32bit 和 低32bit 混合计算出 32bit 的哈希值,充分利用所有信息计算出哈希值。

■ 补充一个细节:hash 函数,对哈希值进行右移16位后取异或

对哈希值的进一步处理:扰动计算,是哈希值分布更加均匀。

    static final int hash(Object key) {
int h = key.hashCode();
return (h ^ (h >>> 16)) & (table.length - 1);//将哈希值低16位与高16 位进行混合,企图让哈希值分布更加均匀
}

(3) 字符串-哈希值计算

  • 利用字符串的字符本质是整数

例如:整数 5487 计算过程: 5 * 10^3 + 4 * 10^2 + 8 * 10^1 + 7 * 10^0

同理:字符串"kate",是由'k'、'a'、't'、'e' 组成:可以表示为 k * n^3 + a * n^2 + t * n^1 + e * n^0

​ 合并,减少计算次数,等价于 [(k * n + a) * n + t] * n + e

  • java中 n=31,则[(k * n + a) * n + t] + e = [(k * 31 + a) * 31 + t] * 31 + e

    发现规律:当前字符a或t或e的哈希值:是先算出前面的字符*31 + 当前的字符

    public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value; for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

(4) 自定义对象-哈希值计算

  • 对自定义对象的属性根据key类型计算得出哈希值后,进行相加

7、为什么需要重写 equals

​ 需要判断 key 是否相同,相同的话,直接用key 对应的value 覆盖掉原先的value

■ 为什么有hashCode 方法,比较 key 是否相同还需要 重写equals方法?

  • 因为hashCode 方法的作用是 生成key的哈希值,而对于不同的key,比如key类型是不同,最后算出的哈希值却是相同

    所以hashCode 方法并不能判断两个key是否相同,需要重写equals方法,进一步比较两个key是否相同。

■ 总结:hashCode 方法 和 equals 方法 作用是不同的?

  • hashCode 方法:生成key的哈希值
  • equals 方法:比较key是否相同

8、HashMap源码分析

(1) 了解重要的几个常量

Node<K,V>[] table;//哈希桶数组,明显它是一个Node的数组
// Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。
  • HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决问题,Java中HashMap采用了链地址法。链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上

  • 减少哈希冲突,依靠:好的Hash算法和扩容机制

int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的哈希桶的长度 16,要求哈希桶的长度必须是2的n次方

/* 构造函数就是对下面几个字段进行初始化 */
int threshold; // 可以容纳最大的Node个数(最大元素数目)
final float loadFactor; // 负载因子,默认值是0.75
int modCount; // 记录HashMap内部结构发生变化的次数
int size; // 节点的数量
// threshold = length * loadFactor 能容纳的最大节点个数=哈希桶容量*负载因子
  • threshold就是在此loadFactor和length(数组长度)对应下允许的最大元素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。
■ 要求哈希桶的长度必须是2的n次方?

HashMap采用这种非 常规设计(常规是设计成素数),主要是为了在取模和扩容时做优化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位 参与运算的过程。

int TREEIFY_THRESHOLD = 8;		// 树化阀值
int MIN_TREEIFY_CAPACITY = 64; // 树化哈希桶最小容量
int UNTREEIFY_THRESHOLD = 6; // 树降级为链表的阀值
  • 添加元素时当哈希表容量 ≥ 64 且 单向链表的节点数量大于 8 时单向链表转为红黑树来存储元素

(2) 了解几个重要的方法

■ 确定哈希桶数组索引位置
方法一:
static final int hash(Object key) { //jdk1.8 & jdk1.7
int h;
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的(在put方法中,有与运算来定位到索引位置)
return h & (length-1); //第三步 取模运算
}
总结:哈希函数将key换算成哈希桶数组的索引具体实现过程:

① 生成key的哈希值 ② 高位参与运算 ③ 取模运算

  • 高位参与运算:将哈希值低16位与高16 位进行混合,企图让哈希值分布更加均匀
  • 取模运算:定位到数组的索引位置
■ put 增加元素方法
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)//判断哈希桶数组是否为空/是否存在,不存在-扩容
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//判断定位到的哈希桶数组的索引对应的元素是否为空/是否存在,不存在,直接创建插入该元素
tab[i] = newNode(hash, key, value, null);
else {//哈希桶数组的元素存在,那么判断key是否存在
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//key存在,直接覆盖元素的值
else if (p instanceof TreeNode)//key不存在,判断是否是一颗树节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//是树节点,红黑树插入
else {
for (int binCount = 0; ; ++binCount) {//不是树节点,链表从头遍历插入该节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) //插入后节点大于8,开始树化成红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//key存在,直接覆盖元素的值
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)//插入元素后判断是否大于 threshold(可以容纳的最大元素数目/节点数) --大于,则需要扩容
resize();
afterNodeInsertion(evict);
return null;
}

总结put时的思考点:

1、哈希表是否为空判断---空(需要扩容)

2、定位哈希表索引对应的元素是否为空判断---空(直接插入)

3、元素非空,判断key是否存在,存在直接覆盖

​ 不存在,判断该节点是否是一颗树节点,是树节点->红黑树插入

​ 不是树节点->链表插入,插入后判断节点数量是否大于8,大于8树化成红黑树

4、插入元素后判断是否大于 threshold(可以容纳的最大元素数目/节点数) --大于(需要扩容)

参考文章:美团技术团队 《Java 8系列之重新认识HashMap

如果本文对你有帮助的话记得给一乐点个赞哦,感谢!

从HashMap的执行流程开始 揭开HashMap底层实现的更多相关文章

  1. angularjs源码分析之:angularjs执行流程

    angularjs用了快一个月了,最难的不是代码本身,而是学会怎么用angular的思路思考问题.其中涉及到很多概念,比如:directive,controller,service,compile,l ...

  2. angularjs执行流程

    angularjs源码分析之:angularjs执行流程   angularjs用了快一个月了,最难的不是代码本身,而是学会怎么用angular的思路思考问题.其中涉及到很多概念,比如:directi ...

  3. 追源索骥:透过源码看懂Flink核心框架的执行流程

    li,ol.inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-bottom:20px}dt, ...

  4. Spring Security 案例实现和执行流程剖析

    Spring Security Spring Security 是 Spring 社区的一个顶级项目,也是 Spring Boot 官方推荐使用的安全框架.除了常规的认证(Authentication ...

  5. 【原创】angularjs1.3.0源码解析之执行流程

    Angular执行流程 前言 发现最近angularjs在我厂的应用变得很广泛,下周刚好也有个angular项目要着手开始做,所以先做了下功课,从源代码开始入手会更深刻点,可能讲的没那么细,侧重点在于 ...

  6. Jedis cluster命令执行流程剖析

    Jedis cluster命令执行流程剖析 在Redis Cluster集群模式下,由于key分布在各个节点上,会造成无法直接实现mget.sInter等功能.因此,无论我们使用什么客户端来操作Red ...

  7. Hibernate的执行流程和集合的映射关系

    Hibernate的执行流程 集合映射 准被hibernate的运行环境 配置hibernate.cfg.xml主配置文件 1.Set集合 写User.java类 package com.gqx.co ...

  8. springMvc的执行流程(源码分析)

    1.在springMvc中负责处理请求的类为DispatcherServlet,这个类与我们传统的Servlet是一样的.我们来看看它的继承图 2. 我们发现DispatcherServlet也继承了 ...

  9. JBPM4.4_执行流程实例

    1. 执行流程实例 1.1. 启动流程实例 说明:流程实例创建后,直接就到开始活动后的第一个活动,不会在开始活动停留. 1.1.1. 示例代码1:使用指定key的最新版本的流程定义启动流程实例 Pro ...

随机推荐

  1. UNION 与 UNION ALL 的区别

    UNION:合并查询结果,并去掉重复的行. UNION ALL:合并查询结果,保留重复的行. 举例验证说明: 创建两个表:user_info 和 user_info_b,设置联合主键约束,联合主键的列 ...

  2. 【原创】项目五w1r3s.v1.0

    实战记录 1.nmap信息枚举 1)C段扫描 nmap -sP 192.168.186.0/24 2)扫描全端口信息 nmap -p- 192.168.186.143 3)扫描版本信息 nmap -p ...

  3. KNN算法推理与实现

    Overview K近邻值算法 KNN (K - Nearest Neighbors) 是一种机器学习中的分类算法:K-NN是一种非参数的惰性学习算法.非参数意味着没有对基础数据分布的假设,即模型结构 ...

  4. Javaer 面试必背系列!超高频八股之三色标记法

    可达性分析可以分成两个阶段 根节点枚举 从根节点开始遍历对象图 前文提到过,在可达性分析中,第一阶段 "根节点枚举" 是必须 STW 的,不然如果分析过程中用户进程还在运行,就可能 ...

  5. markdown常用到的语法

    一.标题 后加文字,几个#代表几级标题,最高为6 ,标准语法一般在#后跟个空格再写文字. 二.分割线 三个或者三个以上的 - 或者 * 三.图片 格式: ![A](B "C") A ...

  6. Mybatisi和Spring整合源码分析

    一.MybatisSpring的使用 1.创建 Maven 工程. 2.添加依赖,代码如下 <dependency> <groupId>org.mybatis</grou ...

  7. 使用aggregation API扩展你的kubernetes API

    Overview What is Kubernetes aggregation Kubernetes apiserver aggregation AA 是Kubernetes提供的一种扩展API的方法 ...

  8. React技巧之循环遍历对象

    原文链接:https://bobbyhadz.com/blog/react-loop-through-object 作者:Borislav Hadzhiev 正文从这开始~ 遍历对象的键 在React ...

  9. 搭建uipath

    我对windows也不太熟,也是第一次安装Uipath Orchestrator,希望有问题指出一起交流,可以留言,Uipath中文qq交流群:4656303241. 下载镜像 windows ser ...

  10. Kingbase重新数据初始化,设置大小写

    KingbaseV8数据库安装完成后,删除/opt/Kingbase/ES/V8/data下所有内容,重新在其他目录初始化数据库,设置为忽略大小写 [kingbase@dbserver bin]$ . ...