HashMap 的长度为什么是 2 的幂次方

本文详细解释了HashMap为何选择数组长度为2的幂次方,涉及快速计算索引、均匀分布元素和扩容效率的提升,并通过实例说明了hashCode的使用和equals()方法的作用,以及Java中Integer对象池的影响。
摘要由CSDN通过智能技术生成

先给出GPT的答案

HashMap 选择数组长度为 2 的幂次方的原因主要有以下几点:

  1. 快速计算索引:HashMap 使用取模运算来计算元素在数组中的索引,取模运算中的除数应当是 2 的幂次方。当数组长度为 2 的幂次方时,可以使用位运算(& 操作)来替代取模运算,提高计算效率。
  2. 均匀分布:当数组长度为 2 的幂次方时,HashMap 使用元素的哈希码的低几位来计算索引,这样可以保证元素在数组中的分布更加均匀,减少哈希冲突的概率。
  3. 扩容效率(快速计算各元素的新下标):当数组长度为 2 的幂次方时,HashMap 进行扩容时可以通过位运算来重新计算元素的索引,而不需要重新计算哈希码,提高了扩容的效率。

综上所述,HashMap 选择数组长度为 2 的幂次方是为了提高计算效率、均匀分布元素、以及提高扩容的效率。

接下来介绍上述原因

快速计算索引

举个例子

假设我们有一个 HashMap,长度为 16(2 的 4 次方)。现在我们有两个 key,分别是 "apple" 和 "banana"。

  1. 计算 key 的 hashCode:
    • "apple" 的 hashCode 是 101,二进制表示为 1100101
    • "banana" 的 hashCode 是 98,二进制表示为 1100010
  2. 计算数组下标:
    • 对于长度为 16 的数组,我们可以通过 hashCode & (length - 1) 来计算数组下标
    • 对于 "apple",1100101 & 1111 = 0101 = 5,所以 "apple" 存储在数组下标为 5 的位置
    • 对于 "banana",1100010 & 1111 = 0010 = 2,所以 "banana" 存储在数组下标为 2 的位置

通过这种方式,我们可以快速地计算出 key 在数组中的位置,提高了查找和插入的效率。如果 HashMap 的长度不是 2 的幂次方,那么计算数组下标的方式会变得更加复杂,影响了性能。因此,选择长度为 2 的幂次方是为了优化 HashMap 的性能。

如果长度不是16比如是14,那么我们计算apple的下标则需要101%14,这样的计算效率远远不如上面的&运算。

上面的运算过程我用正常来模拟一下apple的下标101%16=5,length=16那么length-1=15,15的二进制1111(&对于二进制数的按位与运算,只有当两个对应位都为 1 时,结果才为 1;否则为 0。)

1100101

       1111

0000101=5

可以看出是一样的,但对计算机来说采用&会更快。

  • 如果 length 是 16,length-1 的二进制表示为 1111
  • 如果 length 是 32,length-1 的二进制表示为 11111
  • 如果 length 是 64,length-1 的二进制表示为 111111

所以数组长度为 2 的幂次方可以快速计算索引。

均匀分布

假设 length 是哈希表的长度,length 必须是 2 的幂次方,例如 16、32、64 等。现在我们来解释 h&(length-1) 的含义:

  1. 计算 length-1 的二进制表示:
    • 如果 length 是 16,length-1 的二进制表示为 1111
    • 如果 length 是 32,length-1 的二进制表示为 11111
    • 如果 length 是 64,length-1 的二进制表示为 111111
  2. 计算 h&(length-1) 的结果:
    • 将 h 和 length-1 进行按位与运算,即将 h 和 length-1 的对应位进行按位与操作
    • 结果即为 h 对应的数组索引,范围在 0 到 length-1 之间

这种操作的好处在于,由于 length 是 2 的幂次方,length-1 的二进制表示中只有低位是 1,其余位都是 0。因此,h&(length-1) 的结果相当于取 h 的二进制表示的低位,这样可以保证计算出的数组索引在 0 到 length-1 之间,且分布均匀,有利于减少哈希冲突。

扩容效率(快速计算各元素的新下标)

先说一下HashMap的扩容过程:

当元素个数大于

threshold=DEFAULT_INITIAL_CAPACITY(初始为16) * DEFAULT_LOAD_FACTOR(0.75)

时,会进行扩容,使用2倍容量的数组代替原有数组。采用尾插入的方式将原数组元素拷贝到新数组。

在哈希表扩容时,元素不需要重新计算哈希值。在扩容过程中,元素的哈希值仍然是不变的,只是根据新的数组长度和原数组长度的关系,通过位运算来确定元素在新数组中的位置。

具体来说,在哈希表扩容时,元素的哈希值已经确定,只需要根据原数组的长度和新数组的长度的关系,通过位运算来确定元素在新数组中的位置。如果新增的那个 bit 为 1,则索引变为“原索引+oldCap”,否则索引不变。

下面举个例子:

对于上面的 "apple",1100101 & 1111 = 0101 = 5

扩容后length变为32,length-1=31

那么apple的新下标为1100101 & 11111 = 0000101=5

没有发生变换

如果有个orange他的hashcode是 1111101

原来的下标是1111101 & 1111 = 1101 = 13

扩容后length变为32,length-1=31

那么orange的新下标为1111101 & 11111 = 0011101 = 29

我们发现下标的运算结果只和标红的那个1有关,我们只需要找到与红色对应的紫色为的值是0还是1即可。如果是0,那么下标不发生改变。如果是1,新增oldCap也就是29-13=16

然后介绍一下HashMap

在 Java 中,HashMap 是使用数组和链表(或红黑树)来实现的。具体来说,HashMap 内部使用一个数组来存储键值对,每个数组元素称为桶(bucket),每个桶可以存储一个链表或红黑树。当链表的长度大于8时,链表会转换成红黑树。当链表的长度小于6时,红黑树又会转换为链表。(这样可以防止频繁转换)

我们给出HashMap的源码

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    private static final long serialVersionUID = 362498820763181265L;

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

}
TREEIFY_THRESHOLD和UNTREEIFY_THRESHOLD就是上面提到的8和6。

hashmap的key通常啥类型

HashMap 的 key 通常是任意类型的对象,只要该对象符合以下两个要求:

  1. 可以作为 key:作为 HashMap 的 key,对象必须实现 hashCode() 和 equals() 方法,以确保能够正确地进行哈希计算和键的比较。
  2. 不可变性:为了保证 HashMap 的正确性,key 对象应该是不可变的,即创建后不能被修改。这样可以避免在哈希计算和比较过程中出现问题。

在实际开发中,常见的 HashMap key 类型包括但不限于以下几种:

  1. 基本数据类型的包装类:如 Integer、Long、Double 等。
  2. 字符串:String 类型是 HashMap 中常用的 key 类型,因为 String 是不可变的Java是值传递还是引用传递-CSDN博客,且实现了 hashCode() 和 equals() 方法。
  3. 自定义对象:开发者也可以自定义类作为 HashMap 的 key 类型,只要该类实现了 hashCode() 和 equals() 方法,并且保证对象的不可变性。

看到equals()在补充一个

在 Java 中,equals() 方法和 == 运算符有着不同的作用和用法:

  1. equals() 方法:
    • equals() 方法是用来比较两个对象的内容是否相等的方法,通常需要在类中进行重写以实现自定义的比较逻辑。
    • 对于基本数据类型(如 int、double 等),equals() 方法并不适用,因为基本数据类型直接比较的是值。
    • 对于引用类型(对象),默认情况下,equals() 方法比较的是对象的引用地址,即两个对象是否指向同一块内存地址。如果需要比较对象的内容,需要在类中重写 equals() 方法。
    • 例如,String 类已经重写了 equals() 方法,用于比较字符串的内容是否相等。
  2. == 运算符:
    • == 运算符用于比较两个对象的引用地址是否相等,即判断两个对象是否是同一个对象。
    • 对于基本数据类型,== 运算符用于比较值是否相等。
    • 对于引用类型,== 运算符比较的是对象的引用地址,即判断两个对象是否指向同一块内存地址。

总的来说,equals() 方法用于比较对象的内容是否相等,而 == 运算符用于比较对象的引用地址是否相等。

然后给出一个题:(又新的知识点)

Integer a =1

Integer b = 1

return a==b

结果是true

为啥是true,不是比较地址吗,是的,他俩地址相同,马上就说

Integer a =128

Integer b = 128

return a==b

结果是false

在 Java 中,如果值在 -128 到 127 之间(包括 -128 和 127),Java 会使用一个内部的 Integer 对象池来缓存这些值,以提高性能。因此,当两个Integer 类型的变量的值在 -128 到 127 之间时,它们会共享同一个 Integer 对象,即它们的引用地址是相同的。

但是,当 Integer类型的值超出 -128 到 127 的范围时,Java 不会使用对象池,而是会创建新的 Integer 对象。因此,当 a 和 b 的值都是 128 时,它们不会共享同一个对象,而是分别指向不同的对象,因此返回 a == b 的结果为 false。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值