为了账号安全,请及时绑定邮箱和手机立即绑定

HashMap 重新散列/调整容量

HashMap 重新散列/调整容量

白衣非少年 2021-11-03 16:11:55
AHashMap的文档中有这样一句话:如果初始容量大于最大条目数除以负载因子,则不会发生重新哈希操作。请注意文档如何说rehash,而不是调整大小- 即使 rehash 只会在调整大小时发生;那是当桶的内部大小变成两倍大时。当然也HashMap提供了这样一个构造函数,我们可以在其中定义这个初始容量。构造一个具有指定初始容量和默认负载因子 (0.75) 的空 HashMap。好的,看起来很简单:// these are NOT chosen randomly...    List<String> list = List.of("DFHXR", "YSXFJ", "TUDDY",           "AXVUH", "RUTWZ", "DEDUC", "WFCVW", "ZETCU", "GCVUR");int maxNumberOfEntries = list.size(); // 9double loadFactor = 0.75;int capacity = (int) (maxNumberOfEntries / loadFactor + 1); // 13所以容量是13(在内部是16- 下一个 2 的幂),这样我们就可以保证文档部分没有重新哈希。好的,让我们测试一下,但首先介绍一个方法,该方法将进入 aHashMap并查看值: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);好吧,resize被调用,因此条目被重新哈希,而不是文档所说的。如上所述,密钥不是随机选择的。这些设置是为了触发static final int TREEIFY_THRESHOLD = 8;属性 - 当一个桶转换为一棵树时。不是真的,因为我们还需要击中MIN_TREEIFY_CAPACITY = 64树才能出现;直到resize发生这种情况,或者桶的大小增加了一倍;因此会发生条目的重新散列。我只能暗示为什么HashMap那句话中的文档是错误的,因为在java-8之前,bucket 没有转换为 Tree;因此,该属性将保持不变,从 java-8 开始就不再正确了。由于我不确定这一点,因此我不会将其添加为答案。
查看完整描述

1 回答

?
蝴蝶不菲

TA贡献1810条经验 获得超4个赞

文档中的那一行,


如果初始容量大于最大条目数除以负载因子,则不会发生重新哈希操作。


确实可以追溯到在 JDK 8 ( JEP 180 ) 中添加 tree-bin 实现之前。您可以在JDK 1.6 HashMap 文档中看到此文本。事实上,本文可以追溯到 JDK 1.2 引入集合框架(包括 HashMap)时。您可以在网络上找到 JDK 1.2 文档的非官方版本,或者如果您想亲自查看,也可以从档案中下载一个版本。


我相信在添加 tree-bin 实现之前,此文档是正确的。但是,正如您所观察到的,现在有些情况是不正确的。如果条目数除以负载因子超过容量(实际上是表长度),则策略不仅可以调整大小。正如您所指出的,如果单个存储桶中的条目数超过 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 被树化;否则,表格只是调整大小。


请注意,在小表大小下,这可能会使 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



查看完整回答
反对 回复 2021-11-03
  • 1 回答
  • 0 关注
  • 209 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信