<Java八股文面试>HashMap深度解析 , 一文让你彻底搞懂HashMap(二)

简介: <Java八股文面试>HashMap深度解析 , 一文让你彻底搞懂HashMap

4.4.2 情况2—当某些节点为NULL


情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表


退化过程图解:


c09e955c5b228b7d7e69d35a3e9b3488.png


备注:


检查节点是在移除之前进行的,如果移除(以上四种)之前存在,则移除之后依旧不会退化


当执行resize也就是扩容的情况下是判断元素小等于6,而在执行remove时才去判断root节点及子孙。两个情况不一样。 所以图中,在remove过程中即使节点数<=6,也不会退化成链表.


4.5 索引计算


4.5.1 索引计算方法


首先,计算对象的 hashCode() ,也就是原始Hash

再进行调用 HashMap 的 hash() 方法进行二次哈希

二次 hash() 是为了综合高位数据,让哈希分布更为均匀

最后 & (capacity – 1) 得到索引 (等价于取模运算,但是逻辑运算效率更高)

补充: 取模运算和逻辑运算的关系


bb3e38632061ae70348731fdae44cc57.png


4.5.2 为何要二次Hash


观察源码


156c867acc4b815e11b2cbdb8aa5c3eb.png


代码验证


当使用随机产生的数放入容量为16的Hash表中


public static void main(String[] args) {
    int[] array = Utils.randomArray(1000);
    System.out.println(Arrays.toString(array));
    int[] sizes = {16};
    printHashResult(array, sizes);
}
public static int[] randomArray(int n) {
    int lastVal = 1;
    Random r = new Random();
    int[] array = new int[n];
    for (int i = 0; i < n; i++) {
        int v = lastVal + Math.max(r.nextInt(10), 1);
        array[i] = v;
        lastVal = v;
    }
    shuffle(array);
    return array;
}
public static void printHashResult(int[] array, int[] sizes) {
    List<Map<Integer, AtomicInteger>> maps = new ArrayList<>();
    for (int size : sizes) {
        maps.add(getMap(size));
    }
    for (int hash : array) {
        for (int j = 0; j < sizes.length; j++) {
            maps.get(j).get(hash % sizes[j]).incrementAndGet();
        }
    }
    for (Map<Integer, AtomicInteger> map : maps) {
        System.out.printf("size:[%d] %s%n", map.size(), map);
    }
}

运行结果:

78aec4d85ebaeacf2c89029927b1737b.png

  • 当使用非随机数(特殊处理过)放入容量为16的Hash表中


public static void main(String[] args) {
    int[] array = Utils.lowSameArray(1000);
    System.out.println(Arrays.toString(array));
    int[] sizes = {16};
    printHashResult(array, sizes);
}
//产生特殊的随机数
public static int[] lowSameArray(int n) {
    int[] array = new int[n];
    Random r = new Random();
    for (int i = 0; i < n; i++) {
        array[i] = r.nextInt() & 0x7FFF0002;
    }
    return array;
}

运行结果:


afba58c67069651779ae7464d886f46a.png


改进:模仿jdk的hash算法进行二次Hash


1291024236e3ddb2f8e7ba77fa23d3f0.png


总结:二次Hash可以让数在HashMap上分布更为均匀,防止出现超长链表.


4.5.3 容量为何是2的n次幂


计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模

扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap

图解演示


ebd479d346524a7b9dc9c9347069d8a6.png


4.5.4 容量不是2的n次幂行不行

答案:可以的


代码演示

// .net 是原始容量 * 2 开始找下一个质数作为新容量
public static void main(String[] args) {
    int[] array = Utils.lowSameArray(1000);
    //  int[] array = Utils.evenArray(1000);
    System.out.println(Arrays.toString(array));
    int[] sizes = {11, 16, 23};
    printHashResult(array, sizes);
}
public static int[] evenArray(int n) {//产生n个随机偶数
    int[] array = new int[n];
    for (int i = 0; i < n; i++) {
        array[i] = i * 2;
    }
    return array;
}
public static int[] lowSameArray(int n) {//产生n个特殊的数
    int[] array = new int[n];
    Random r = new Random();
    for (int i = 0; i < n; i++) {
        array[i] = r.nextInt() & 0x7FFF0002;
    }
    return array;
}

0d47d39c87b2ebebf87d1442e5a8273e.png

c12b4732d87073d6a0157e120a90a1e9.png


结论


计算对象的HashCode(),在进行调用HashMap的hash()方法进行二次哈希,最后 & (capacity - 1)得到索引

二次hash()是为了提高数位数据,让哈希分布更为均匀.

计算索引时,如果是2的n次幂可以使用位于运算代替取模,效率更高; 扩容时hash & oldCap==0的元素保留在原来位置,否则新位置 = 旧位置 + oldCap

但 1、2、3都是为了配合容量为2的n次幂时的优化手段.也就是说如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash. 设计者应该是综合了各种因素,最终选择了使用2的n次幂作为容量.

没有采用这一设计的典型例子是 Hashtable

一句话:容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿. 如果容量不是 2 的 n 次幂,那么1,2,3这些手段也用不上.


**补充:**HashTable的扩容机制


de2447aa8abad1c99599a76e68a42bec.png


可以看到扩容的容量中有一些还并不是质数. 微软的.NET中的Dictionary每次扩容的时候,容量大于等于原有空间的2倍的最小质数,这种扩容规则就会让Hash分布更为均匀.


所以:如果要追求效率,则选择2^n作为容量.如果想要追求各个挺好的Hash分布性,则选择一个质数选择Hash容量.


4.6 HashMap的put方法


4.6.1 put过程分析


HashMap 是懒惰创建数组的,首次使用才创建数组 (首次使用put方法才创建数组)

计算索引(桶下标) (通过key计算hash值,然后进行二次hash,进行位与运算得到下标)

如果桶下标还没人占用,创建 Node(JDK1.7是Entry对象) 占位返回

如果桶下标已经有人占用

已经是 TreeNode 走红黑树的添加或更新逻辑

是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑

返回前检查容量是否超过阈值,一旦超过进行扩容 (添加完成才进行扩容的)


相关文章
|
2月前
|
人工智能 Cloud Native Java
2025 年 Java 应届生斩获高薪需掌握的技术实操指南与实战要点解析
本指南为2025年Java应届生打造,涵盖JVM调优、响应式编程、云原生、微服务、实时计算与AI部署等前沿技术,结合电商、数据处理等真实场景,提供可落地的技术实操方案,助力掌握高薪开发技能。
129 2
|
1月前
|
缓存 安全 Java
Java并发性能优化|读写锁与互斥锁解析
本文深入解析Java中两种核心锁机制——互斥锁与读写锁,通过概念对比、代码示例及性能测试,揭示其适用场景。互斥锁适用于写多或强一致性场景,读写锁则在读多写少时显著提升并发性能。结合锁降级、公平模式等高级特性,助你编写高效稳定的并发程序。
76 0
|
2月前
|
人工智能 Java 程序员
搭建AI智能体的Java神器:Google ADK深度解析
想用Java构建复杂的AI智能体?Google开源的ADK工具包来了!代码优先、模块化设计,让你像搭积木一样轻松组合智能体。从单体到多智能体系统,从简单工具到复杂编排,这篇文章带你玩转Java AI开发的全新境界。
|
2月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
110 0
|
1月前
|
安全 Oracle Java
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
126 0
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
|
18天前
|
算法 Java 测试技术
零基础学 Java: 从语法入门到企业级项目实战的详细学习路线解析
本文为零基础学习者提供完整的Java学习路线,涵盖语法基础、面向对象编程、数据结构与算法、多线程、JVM原理、Spring框架、Spring Boot及项目实战,助你从入门到进阶,系统掌握Java编程技能,提升实战开发能力。
62 0
|
2月前
|
存储 Java Linux
操作系统层面视角下 Java IO 的演进路径及核心技术变革解析
本文从操作系统层面深入解析Java IO的演进历程,涵盖BIO、NIO、多路复用器及Netty等核心技术。分析各阶段IO模型的原理、优缺点及系统调用机制,探讨Java如何通过底层优化提升并发性能与数据处理效率,全面呈现IO技术的变革路径与发展趋势。
45 1
|
2月前
|
并行计算 Java API
Java List 集合结合 Java 17 新特性与现代开发实践的深度解析及实战指南 Java List 集合
本文深入解析Java 17中List集合的现代用法,结合函数式编程、Stream API、密封类、模式匹配等新特性,通过实操案例讲解数据处理、并行计算、响应式编程等场景下的高级应用,帮助开发者提升集合操作效率与代码质量。
117 1
|
2月前
|
存储 Java 程序员
Java 基础知识点全面梳理包含核心要点及难点解析 Java 基础知识点
本文档系统梳理了Java基础知识点,涵盖核心特性、语法基础、面向对象编程、数组字符串、集合框架、异常处理及应用实例,帮助初学者全面掌握Java入门知识,提升编程实践能力。附示例代码下载链接。
95 1

热门文章

最新文章

推荐镜像

更多
  • DNS