深圳幻海软件技术有限公司 欢迎您!

重温数据结构经典:HashCode及HashMap原理

2023-02-28

一、HashCode为什么使用31作为乘数1、选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是一个传统。2、数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31*

一、HashCode为什么使用31作为乘数

1、选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是一个传统。

2、数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 * i == (i << 5) - i,现代的 Java 虚拟机可以自动地完成这个优化。

二、数组与链表的特点

数组的特点是:寻址容易,插入和删除困难。

链表的特点是:寻址困难,插入和删除容易。

三、HashMap原理

  • 允许null健及null值,null健,值为0。
  • HashMap 不保证键值对的顺序,操作时,键值对的顺序会发生变化。
  • HashMap是非线程安全类,在多线程环境下会发生问题。
  • HashMap是JDK 1.2时推出的,底层基于散列算法实现。
  • 在JDK 1.8中涉及比较多:1、散列表实现、2、扰动函数、3、初始化容量、4、负载因子、5、扩容元素拆分、6、链表树化、7、红黑树、8、插入、9、查找、10、删除、11、遍历、12、分段锁等。

(1) 扰动函数

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 1.
  • 2.
  • 3.
  • 4.

扰动函数就是为了增加随机性,让数据元素更加均衡地散列,减少碰撞。

(2) 负载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • 1.

负载因子,可以理解成一辆车可承重重量超过某个阀值时,把货放到新的车上。在 HashMap 中,负载因子决定了数据量多少了以后进行扩容。

0.75 是一个默认构造值,在创建 HashMap 也可以调整,如何希望用更多的空间换取时间,可以把负载因子调得更小一些,减少碰撞。

(3) 扩容元素拆分

扩容最直接的问题,就是需要把元素拆分到新的数组中,在JDK1.7中,会重新计算哈希值,但在JDK1.8后,进行了优化。

  1. 扩容时计算出新的newCap、newThr,这是两个单词的缩写,一个是Capacity ,另一个是阀Threshold。
  2. newCap用于创新的数组桶 new Node[newCap]。
  3. 随着扩容后,原来那些因为哈希碰撞,存放成链表和红黑树的元素,都需要进行拆分存放到新的位置中。

(4) 数据迁移

(5) 数据插入

  1. 首先进行哈希值的扰动,获取一个新的哈希值。(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)。
  2. 判断tab是否为空或者长度为0,如果是则进行扩容操作。
  3. if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length。
  4. 根据哈希值计算下标,如果对应小标正好没有存放数据,则直接插入即可否则需要覆盖。tab[i = (n - 1) & hash])。
  5. 判断tab[i]是否为树节点,否则向链表中插入数据,否则向树中插入节点。
  6. 如果链表中插入节点的时候,链表长度大于等于8,则需要把链表转换为红黑树。treeifyBin(tab, hash)。
  7. 最后所有元素处理完成后,判断是否超过阈值;threshold,超过则扩容。
  8. treeifyBin,是一个链表转树的方法,但不是所有的链表长度为8后都会转成树,还需要判断存放key值的数组桶长度是否小于64 MIN_TREEIFY_CAPACITY。如果小于则需要扩容,扩容后链表上的数据会被拆分散列的相应的桶节点上,也就把链表长度缩短了。

(6) 链表树化

  1. 链表树化的条件有两点;链表长度大于等于8、桶容量大于64,否则只是扩容,不会树化。
  2. 链表树化的过程中是先由链表转换为树节点,此时的树可能不是一颗平衡树。同时在树转换过程中会记录链表的顺序,tl.next = p,这主要方便后续树转链表和拆分更方便。
  3. 链表转换成树完成后,再进行红黑树的转换。先简单介绍下,红黑树的转换需要染色和旋转,以及比对大小。在比较元素的大小中,有一个比较有意思的方法,tieBreakOrder加时赛,这主要是因为HashMap没有像TreeMap那样本身就有Comparator的实现。

(7) 红黑树转链

在转换树的过程中,记录了原有链表的顺序,红黑树转链表时候,直接把TreeNode转换为Node,因为记录了链表关系,所以替换过程很容易。

(8) 查找

  1. 扰动函数的使用,获取新的哈希值。
  2. 下标的计算, tab[(n - 1) & hash])。
  3. 确定了桶数组下标位置,接下来就是对红黑树和链表进行查找和遍历操作了。

(9) 删除

  1. 删除的操作也比较简单,没有太多的复杂的逻辑。
  2. 另外红黑树的操作因为被包装了,只看使用上也是很容易。

(10) 遍历

  1. 这里我们要设定一个既有红黑树又有链表结构的数据场景
  2. 为了可以有这样的数据结构,我们最好把 HashMap 初始长度设定为 64,避免在

链表超过 8 位后扩容,而是直接让其转换为红黑树。

  1. 找到 18 个元素,分别放在不同节点(这些数据通过程序计算得来)。
  2. 桶数组 02 节点:24、46、68。
  3. 桶数组 07 节点:29。
  4. 桶数组 12 节点:150、172、194、271、293、370、392、491、590。

测试代码

public void test_Iterator() {
    Map<String, String> map = new HashMap<String, String>(64);
    map.put("24", "Idx:2");
    map.put("46", "Idx:2");
    map.put("68", "Idx:2");
    map.put("29", "Idx:7");
    map.put("150", "Idx:12");
    map.put("172", "Idx:12");
    map.put("194", "Idx:12");
    map.put("271", "Idx:12");
    System.out.println("排序01:");
    for (String key : map.keySet()) {
        System.out.print(key + " ");
    }
    map.put("293", "Idx:12");
    map.put("370", "Idx:12");
    map.put("392", "Idx:12");
    map.put("491", "Idx:12");
    map.put("590", "Idx:12");
    System.out.println("\n\n排序02:");
    for (String key : map.keySet()) {
        System.out.print(key + " ");
    }    
    map.remove("293");
    map.remove("370");
    map.remove("392");
    map.remove("491");
    map.remove("590");
    System.out.println("\n\n排序03:");
    for (String key : map.keySet()) {
        System.out.print(key + " ");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  1. 添加元素,在 HashMap 还是只链表结构时,输出测试结果 01。
  2. 添加元素,在 HashMap 转换为红黑树的时候,输出测试结果 02。
  3. 删除元素,在 HashMap 转换为链表结构时,输出测试结果 03。

结果如下:

排序0124 46 68 29 150 172 194 271 
排序0224 46 68 29 271 150 172 194 293 370 392 491 590 
排序0324 46 68 29 172 271 150 194 
Process finished with exit code 0
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这一篇 API 源码以及逻辑与上一篇数据结构中扰动函数、负载因子、散列表实现等,内容的结合,算是把 HashMap 基本常用技术点,梳理完成了。但知识绝不止于此,这里还有红黑树的相关技术内容,后续会进行详细。

除了 HashMap 以外还有 TreeMap、ConcurrentHashMap 等,每一个核心类都有一与之相关的核心知识点,每一个都非常值得深入研究。这个烧脑的过程,是学习获得的知识的最佳方式。