A HashMap
在其文档中有这样的短语:
如果初始容量大于最大条目数除以负载因子,则将 不会 发生任何哈希操作。
请注意文档中说的是 rehash ,而不是 resize- 即使仅在调整大小时才会发生rehash;也就是说,当存储桶的内部尺寸变大两倍时。
当然HashMap
提供了这样的构造函数,我们可以在其中定义此 初始容量 。
构造一个具有指定初始容量和默认负载因子(0.75)的空HashMap。
OK,看起来很简单:
// these are NOT chosen randomly...
List<String> list = List.of("DFHXR", "YSXFJ", "TUDDY",
"AXVUH", "RUTWZ", "DEDUC", "WFCVW", "ZETCU", "GCVUR");
int maxNumberOfEntries = list.size(); // 9
double loadFactor = 0.75;
int capacity = (int) (maxNumberOfEntries / loadFactor + 1); // 13
因此容量为13
(内部为16
-2的下一个幂),这样,我们保证文档不涉及任何修改。好的,我们对此进行测试,但首先介绍一种方法,该方法将进入HashMap
并查看值:
private static <K, V> void debugResize(Map<K, V> map, K key, V value) throws Throwable {
Field table = map.getClass().getDeclaredField("table");
table.setAccessible(true);
Object[] nodes = ((Object[]) table.get(map));
// first put
if (nodes == null) {
// not incrementing currentResizeCalls because
// of lazy init; or the first call to resize is NOT actually a "resize"
map.put(key, value);
return;
}
int previous = nodes.length;
map.put(key, value);
int current = ((Object[]) table.get(map)).length;
if (previous != current) {
++HashMapResize.currentResizeCalls;
System.out.println(nodes.length + " " + current);
}
}
现在让我们测试一下:
static int currentResizeCalls = 0;
public static void main(String[] args) throws Throwable {
List<String> list = List.of("DFHXR", "YSXFJ", "TUDDY",
"AXVUH", "RUTWZ", "DEDUC", "WFCVW", "ZETCU", "GCVUR");
int maxNumberOfEntries = list.size(); // 9
double loadFactor = 0.75;
int capacity = (int) (maxNumberOfEntries / loadFactor + 1);
Map<String, String> map = new HashMap<>(capacity);
list.forEach(x -> {
try {
HashMapResize.debugResize(map, x, x);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
});
System.out.println(HashMapResize.currentResizeCalls);
}
好吧,它resize
被调用了,因此条目被重新定义,而不是文档所说的。
如前所述,密钥不是随机选择的。对它们进行了设置,以便它们可以触发static final int TREEIFY_THRESHOLD = 8;
属性-
将存储桶转换为树时。真的不是,因为我们还需要打MIN_TREEIFY_CAPACITY = 64
树才能出现。直到比实际resize
情况还大,或者铲斗的尺寸增加了一倍;因此,会发生条目的重新哈希处理。
我只能暗示为什么HashMap
这句话的文档是错误的,因为 在 Java-8 之前
,存储桶没有转换为Tree;因此,从java-8开始,该属性将不再存在。由于我不确定这一点,因此我没有将其添加为答案。
文档中的这一行,
如果初始容量大于最大条目数除以负载因子,则将不会进行任何哈希操作。
确实可以追溯到在JDK 8(JEP 180)中添加树形实现之前。您可以在JDK
1.6
HashMap文档中
看到此文本。实际上,本文是在引入Collections
Framework(包括HashMap)时一直追溯到JDK 1.2。您可以在网上找到JDK
1.2文档的非官方版本,或者如果您想亲自查看的话,可以从档案中下载一个版本。
我相信在添加树形实现之前,该文档是正确的。但是,正如您所观察到的,在某些情况下它是不正确的。该策略不仅是如果条目数除以负载因子而超出容量(实际上是表长度),那么可能会发生大小调整。如您所述,如果单个存储桶中的条目数超过TREEIFY_THRESHOLD(当前为8),但表长度小于MIN_TREEIFY_CAPACITY(当前为64),则
也会 发生大小调整。
您可以在treeifyBin()
HashMap
的方法中看到此决定。
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
当单个存储桶中的条目超过TREEIFY_THRESHOLD时,到达代码中的这一点。如果表大小等于或大于MIN_TREEIFY_CAPACITY,则此bin被树化;否则,仅调整表的大小。
请注意,在较小的表大小下,这可能会使垃圾箱中的条目多于TREEIFY_THRESHOLD。这并不难证明。首先,一些反射式的HashMap转储代码:
// run with --add-opens java.base/java.util=ALL-UNNAMED
static Class<?> classNode;
static Class<?> classTreeNode;
static Field fieldNodeNext;
static Field fieldHashMapTable;
static void init() throws ReflectiveOperationException {
classNode = Class.forName("java.util.HashMap$Node");
classTreeNode = Class.forName("java.util.HashMap$TreeNode");
fieldNodeNext = classNode.getDeclaredField("next");
fieldNodeNext.setAccessible(true);
fieldHashMapTable = HashMap.class.getDeclaredField("table");
fieldHashMapTable.setAccessible(true);
}
static void dumpMap(HashMap<?, ?> map) throws ReflectiveOperationException {
Object[] table = (Object[])fieldHashMapTable.get(map);
System.out.printf("map size = %d, table length = %d%n", map.size(), table.length);
for (int i = 0; i < table.length; i++) {
Object node = table[i];
if (node == null)
continue;
System.out.printf("table[%d] = %s", i,
classTreeNode.isInstance(node) ? "TreeNode" : "BasicNode");
for (; node != null; node = fieldNodeNext.get(node))
System.out.print(" " + node);
System.out.println();
}
}
现在,让我们添加一串都属于同一存储桶的字符串。选择这些字符串,使它们的哈希值(由HashMap计算)均为0 mod 64。
public static void main(String[] args) throws ReflectiveOperationException {
init();
List<String> list = List.of(
"LBCDD", "IKBNU", "WZQAG", "MKEAZ", "BBCHF", "KRQHE", "ZZMWH", "FHLVH",
"ZFLXM", "TXXPE", "NSJDQ", "BXDMJ", "OFBCR", "WVSIG", "HQDXY");
HashMap<String, String> map = new HashMap<>(1, 10.0f);
for (String s : list) {
System.out.println("===> put " + s);
map.put(s, s);
dumpMap(map);
}
}
从初始表大小1和可笑的负载因子开始,这会将8个条目放入单独的存储桶中。然后,每次添加另一个条目时,将调整表的大小(加倍),但所有条目最终都位于同一存储桶中。这最终导致表的大小为64,其中一个存储桶具有长度为14的线性节点链(“基本节点”),然后添加下一个条目,最终将其转换为树。
该程序的输出如下:
===> put LBCDD
map size = 1, table length = 1
table[0] = BasicNode LBCDD=LBCDD
===> put IKBNU
map size = 2, table length = 1
table[0] = BasicNode LBCDD=LBCDD IKBNU=IKBNU
===> put WZQAG
map size = 3, table length = 1
table[0] = BasicNode LBCDD=LBCDD IKBNU=IKBNU WZQAG=WZQAG
===> put MKEAZ
map size = 4, table length = 1
table[0] = BasicNode LBCDD=LBCDD IKBNU=IKBNU WZQAG=WZQAG MKEAZ=MKEAZ
===> put BBCHF
map size = 5, table length = 1
table[0] = BasicNode LBCDD=LBCDD IKBNU=IKBNU WZQAG=WZQAG MKEAZ=MKEAZ BBCHF=BBCHF
===> put KRQHE
map size = 6, table length = 1
table[0] = BasicNode LBCDD=LBCDD IKBNU=IKBNU WZQAG=WZQAG MKEAZ=MKEAZ BBCHF=BBCHF KRQHE=KRQHE
===> put ZZMWH
map size = 7, table length = 1
table[0] = BasicNode LBCDD=LBCDD IKBNU=IKBNU WZQAG=WZQAG MKEAZ=MKEAZ BBCHF=BBCHF KRQHE=KRQHE ZZMWH=ZZMWH
===> put FHLVH
map size = 8, table length = 1
table[0] = BasicNode LBCDD=LBCDD IKBNU=IKBNU WZQAG=WZQAG MKEAZ=MKEAZ BBCHF=BBCHF KRQHE=KRQHE ZZMWH=ZZMWH FHLVH=FHLVH
===> put ZFLXM
map size = 9, table length = 2
table[0] = BasicNode LBCDD=LBCDD IKBNU=IKBNU WZQAG=WZQAG MKEAZ=MKEAZ BBCHF=BBCHF KRQHE=KRQHE ZZMWH=ZZMWH FHLVH=FHLVH ZFLXM=ZFLXM
===> put TXXPE
map size = 10, table length = 4
table[0] = BasicNode LBCDD=LBCDD IKBNU=IKBNU WZQAG=WZQAG MKEAZ=MKEAZ BBCHF=BBCHF KRQHE=KRQHE ZZMWH=ZZMWH FHLVH=FHLVH ZFLXM=ZFLXM TXXPE=TXXPE
===> put NSJDQ
map size = 11, table length = 8
table[0] = BasicNode LBCDD=LBCDD IKBNU=IKBNU WZQAG=WZQAG MKEAZ=MKEAZ BBCHF=BBCHF KRQHE=KRQHE ZZMWH=ZZMWH FHLVH=FHLVH ZFLXM=ZFLXM TXXPE=TXXPE NSJDQ=NSJDQ
===> put BXDMJ
map size = 12, table length = 16
table[0] = BasicNode LBCDD=LBCDD IKBNU=IKBNU WZQAG=WZQAG MKEAZ=MKEAZ BBCHF=BBCHF KRQHE=KRQHE ZZMWH=ZZMWH FHLVH=FHLVH ZFLXM=ZFLXM TXXPE=TXXPE NSJDQ=NSJDQ BXDMJ=BXDMJ
===> put OFBCR
map size = 13, table length = 32
table[0] = BasicNode LBCDD=LBCDD IKBNU=IKBNU WZQAG=WZQAG MKEAZ=MKEAZ BBCHF=BBCHF KRQHE=KRQHE ZZMWH=ZZMWH FHLVH=FHLVH ZFLXM=ZFLXM TXXPE=TXXPE NSJDQ=NSJDQ BXDMJ=BXDMJ OFBCR=OFBCR
===> put WVSIG
map size = 14, table length = 64
table[0] = BasicNode LBCDD=LBCDD IKBNU=IKBNU WZQAG=WZQAG MKEAZ=MKEAZ BBCHF=BBCHF KRQHE=KRQHE ZZMWH=ZZMWH FHLVH=FHLVH ZFLXM=ZFLXM TXXPE=TXXPE NSJDQ=NSJDQ BXDMJ=BXDMJ OFBCR=OFBCR WVSIG=WVSIG
===> put HQDXY
map size = 15, table length = 64
table[0] = TreeNode LBCDD=LBCDD IKBNU=IKBNU WZQAG=WZQAG MKEAZ=MKEAZ BBCHF=BBCHF KRQHE=KRQHE ZZMWH=ZZMWH FHLVH=FHLVH ZFLXM=ZFLXM TXXPE=TXXPE NSJDQ=NSJDQ BXDMJ=BXDMJ OFBCR=OFBCR WVSIG=WVSIG HQDXY=HQDXY
问题内容: 我正在阅读Java 1.6 API提供的HashMap类的代码,无法完全理解以下操作的需要(位于put和get方法的主体中): 该方法具有以下主体: 通过对提供的哈希码执行位操作,可以有效地重新计算哈希。即使API声明如下,我也无法理解这样做的必要性: 这很关键,因为HashMap使用2的幂的哈希表,否则哈希表在低位无差异时会遇到冲突。 我确实知道键值参数存储在数据结构数组中,并且该数
如果我在hashmap中输入一个键和值,并且基于键hashcode生成的索引大于15,并且映射大小仍然小于阈值(即12),会发生什么? 提前谢谢。
可能重复: HashMap#hash(int)方法的解释 有人能详细解释一下这个方法吗,谢谢。
问题内容: 当大小超过maxthreshold值时,如何在哈希表或哈希表中进行重新哈希处理? 是否所有对都已复制到新的存储桶阵列中? 编辑: 重新哈希后,同一存储桶(位于链接列表中)中的元素会发生什么情况?我的意思是说,他们在重新哈希处理后会留在同一个桶中吗? 问题答案: 问题中的最大阈值称为负载系数。 建议负载系数约为0.75。负载因子定义为(m / n),其中n是哈希表的总大小,m是在需要增加
我正在为我的data structures类创建自己的哈希表adt,遇到了一个问题。我使用以下函数对哈希表中的(key,value)项进行哈希(key是一个字符串,value可以是任何数据类型,它是通用的): 使用线性探测插入到表中可以很好地工作,但是,如果我选择扩展表,哈希(键)函数将无法充分用于哈希表的get(键)操作,因为容量已更改(在正确映射表扩展之前,它将映射到不正确的位置)。有没有什么
问题内容: 如标题所示,这是一个有关实现细节的问题-那是内部数组的大小加倍时。有点罗word,但我确实试图证明我已尽力了解这一点… 这是在此特定存储桶/存储箱中的条目以某种方式存储的时候发生的-因此具有确切的顺序,在问题的上下文中, 这一点很重要 。 通常,也可以从其他地方调用,但是让我们只看这种情况。 假设您将这些字符串作为键放入(在右侧, 后面是 -内部重新哈希。)是的,它们是精心生成的,而不