代码之家  ›  专栏  ›  技术社区  ›  Eugene

HashMap重设/调整容量

  •  24
  • Eugene  · 技术社区  · 6 年前

    A HashMap

    如果初始容量大于最大条目数除以负载系数,则不会进行再灰化操作 发生。

    注意文件上是怎么说的 再灰化 ,不是 调整大小 -即使重新洗牌只会发生在重新调整大小的时候;也就是说,桶的内部尺寸变大了一倍。

    当然了 哈希图 提供了这样一个构造函数,我们可以在其中定义 .

    构造一个具有指定初始容量和默认负载因子(0.75)的空哈希映射。

    // 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 -下一个二次幂),这样我们就保证了文档部分没有再灰烬。好的,让我们测试一下,但是首先介绍一个方法,它将进入 看看这些数值:

    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; 属性-当bucket转换为树时。不太好,因为我们也需要打 MIN_TREEIFY_CAPACITY = 64 调整大小 或者桶的大小增加了一倍;因此,条目的重新灰化发生了。

    我只能暗示为什么 这句话中的文档是错误的,因为 java-8,一个bucket没有转换成树;因此,从java-8开始,这个属性就不再是真的了。因为我不确定这一点,所以我不会把这个作为一个答案。

    1 回复  |  直到 6 年前
        1
  •  13
  •   Stuart Marks    6 年前

    文件上的那行,

    如果初始容量大于最大条目数除以负载系数,则不会发生再灰化操作。

    实际上可以追溯到jdk8中添加树bin实现之前( JEP 180 ). 你可以在 JDK 1.6 HashMap documentation . 事实上,本文可以追溯到JDK1.2引入集合框架(包括HashMap)时。您可以在web上找到JDK1.2文档的非官方版本,也可以从 archives 如果你想亲眼看看的话。

    如果单个bucket中的条目数超过TREEIFY\u阈值(当前为8),但表长度小于MIN\u TREEIFY\u容量(当前为64),则发生。

    你可以在报纸上看到这个决定 treeifyBin() HashMap的方法。

        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
    

    当一个bucket中有超过TREEIFY\u个THRESHOLD条目时,就达到了代码中的这一点。如果表大小等于或大于最小树型容量,则此箱子被树型化;否则,只需调整表的大小。

    请注意,在较小的表大小下,这可能会留下比TREEIFY\u阈值更多的条目。这并不难证明。首先,一些反射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个条目放入一个单独的bucket。然后,每次添加另一个条目时,表都会调整大小(翻倍),但所有条目最终都在同一个bucket中。这最终生成一个大小为64的表,其中一个bucket具有长度为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