先给出GPT的答案
HashMap 选择数组长度为 2 的幂次方的原因主要有以下几点:
- 快速计算索引:HashMap 使用取模运算来计算元素在数组中的索引,取模运算中的除数应当是 2 的幂次方。当数组长度为 2 的幂次方时,可以使用位运算(
&
操作)来替代取模运算,提高计算效率。 - 均匀分布:当数组长度为 2 的幂次方时,HashMap 使用元素的哈希码的低几位来计算索引,这样可以保证元素在数组中的分布更加均匀,减少哈希冲突的概率。
- ※扩容效率(快速计算各元素的新下标):当数组长度为 2 的幂次方时,HashMap 进行扩容时可以通过位运算来重新计算元素的索引,而不需要重新计算哈希码,提高了扩容的效率。
综上所述,HashMap 选择数组长度为 2 的幂次方是为了提高计算效率、均匀分布元素、以及提高扩容的效率。
接下来介绍上述原因
快速计算索引
举个例子
假设我们有一个 HashMap,长度为 16(2 的 4 次方)。现在我们有两个 key,分别是 "apple" 和 "banana"。
- 计算 key 的 hashCode:
- "apple" 的 hashCode 是 101,二进制表示为 1100101
- "banana" 的 hashCode 是 98,二进制表示为 1100010
- 计算数组下标:
- 对于长度为 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) 的含义:
- 计算 length-1 的二进制表示:
- 如果 length 是 16,length-1 的二进制表示为 1111
- 如果 length 是 32,length-1 的二进制表示为 11111
- 如果 length 是 64,length-1 的二进制表示为 111111
- 计算 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 通常是任意类型的对象,只要该对象符合以下两个要求:
- 可以作为 key:作为 HashMap 的 key,对象必须实现 hashCode() 和 equals() 方法,以确保能够正确地进行哈希计算和键的比较。
- 不可变性:为了保证 HashMap 的正确性,key 对象应该是不可变的,即创建后不能被修改。这样可以避免在哈希计算和比较过程中出现问题。
在实际开发中,常见的 HashMap key 类型包括但不限于以下几种:
- 基本数据类型的包装类:如 Integer、Long、Double 等。
- 字符串:String 类型是 HashMap 中常用的 key 类型,因为 String 是不可变的Java是值传递还是引用传递-CSDN博客,且实现了 hashCode() 和 equals() 方法。
- 自定义对象:开发者也可以自定义类作为 HashMap 的 key 类型,只要该类实现了 hashCode() 和 equals() 方法,并且保证对象的不可变性。
看到equals()在补充一个
在 Java 中,equals()
方法和 ==
运算符有着不同的作用和用法:
equals()
方法:equals()
方法是用来比较两个对象的内容是否相等的方法,通常需要在类中进行重写以实现自定义的比较逻辑。- 对于基本数据类型(如 int、double 等),
equals()
方法并不适用,因为基本数据类型直接比较的是值。 - 对于引用类型(对象),默认情况下,
equals()
方法比较的是对象的引用地址,即两个对象是否指向同一块内存地址。如果需要比较对象的内容,需要在类中重写equals()
方法。 - 例如,
String
类已经重写了equals()
方法,用于比较字符串的内容是否相等。
==
运算符:==
运算符用于比较两个对象的引用地址是否相等,即判断两个对象是否是同一个对象。- 对于基本数据类型,
==
运算符用于比较值是否相等。 - 对于引用类型,
==
运算符比较的是对象的引用地址,即判断两个对象是否指向同一块内存地址。
总的来说,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。