<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,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑

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


相关文章
|
1天前
|
存储 Java
面试官:素有Java锁王称号的‘StampedLock’你知道吗?我:这什么鬼?
面试官:素有Java锁王称号的‘StampedLock’你知道吗?我:这什么鬼?
42 23
|
2天前
|
Java 程序员 API
Java 8新特性之Lambda表达式与Stream API的深度解析
【5月更文挑战第12天】本文将深入探讨Java 8中的两个重要新特性:Lambda表达式和Stream API。我们将从基本概念入手,逐步深入到实际应用场景,帮助读者更好地理解和掌握这两个新特性,提高Java编程效率。
22 2
|
3天前
|
存储 安全 Java
Java一分钟之-Map接口与HashMap详解
【5月更文挑战第10天】Java集合框架中的`Map`接口用于存储唯一键值对,而`HashMap`是其快速实现,基于哈希表支持高效查找、添加和删除。本文介绍了`Map`的核心方法,如`put`、`get`和`remove`,以及`HashMap`的特性:快速访问、无序和非线程安全。讨论了键的唯一性、`equals()`和`hashCode()`的正确实现以及线程安全问题。通过示例展示了基本操作和自定义键的使用,强调理解这些概念对编写健壮代码的重要性。
6 0
|
4天前
|
存储 安全 Java
Java容器类List、ArrayList、Vector及map、HashTable、HashMap
Java容器类List、ArrayList、Vector及map、HashTable、HashMap
|
4天前
|
XML JavaScript Java
详解Java解析XML的四种方法
详解Java解析XML的四种方法
|
4天前
|
Java
解析java中的数组
解析java中的数组
11 3
|
4天前
|
Java
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
15 0
|
4天前
|
安全 Java 程序员
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
8 0
|
5天前
|
存储 Java 程序员
Java面向对象编程的基础概念解析
Java面向对象编程的基础概念解析
13 0
|
7天前
|
分布式计算 Java API
Java8 Lambda实现源码解析
Java8的lambda应该大家都比较熟悉了,本文主要从源码层面探讨一下lambda的设计和实现。
69179 4

推荐镜像

更多