【问题标题】:How to see the distribution of keys in a HashMap?如何查看 HashMap 中键的分布?
【发布时间】:2015-06-17 08:57:35
【问题描述】:

使用哈希映射时,将密钥均匀分布在桶上很重要。

如果所有键最终都在同一个存储桶中,那么您基本上会得到一个列表。

有没有办法在 Java 中“审计”一个 HashMap 以查看密钥的分布情况?

我尝试对其进行子类型化并迭代 Entry<K,V>[] table,但它不可见。

【问题讨论】:

  • 您可以在调试器中运行测试程序并使用调试器检查HashMap 中的内容。
  • 您可以对密钥运行 DES 加密算法来生成哈希值,从而确保结果是随机的。
  • @RahulTripathi 这是很多开销,如果没有必要,我不想这样做。
  • 获取源代码,使用内置的“转储分发”功能创建自己的克隆。

标签: java hashmap


【解决方案1】:

您可以使用反射来访问隐藏字段:

HashMap map = ...;

// get the HashMap#table field
Field tableField = HashMap.class.getDeclaredField("table");
tableField.setAccessible(true);

Object[] table = (Object[]) tableField.get(map);
int[] counts = new int[table.length];

// get the HashMap.Node#next field
Class<?> entryClass = table.getClass().getComponentType();
Field nextField = entryClass.getDeclaredField("next");
nextField.setAccessible(true);

for (int i = 0; i < table.length; i++) {
    Object e = table[i];
    int count = 0;
    if (e != null) {
        do {
            count++;
        } while ((e = nextField.get(e)) != null);
    }
    counts[i] = count;
}

现在您有了每个存储桶的条目计数数组。

【讨论】:

  • table[0].getClass(); -- 给出NullPointerException
  • @18446744073709551615 如果第一个桶是空的,这似乎会发生。一个安全的选项是table.getClass().getComponentType(),我会编辑答案。
【解决方案2】:

我尝试对它进行子类型化并迭代 Entry[] 表,但它不可见

使用反射 API!

public class Main {
    //This is to simulate instances which are not equal but go to the same bucket.
    static class A {
            @Override
            public boolean equals(Object obj) { return false;}

            @Override
            public int hashCode() {return 42; }
        }

    public static void main(String[] args) {
            //Test data  
            HashMap<A, String> map = new HashMap<A, String>(4);
            map.put(new A(), "abc");
            map.put(new A(), "def");

            //Access to the internal table  
            Class clazz = map.getClass();
            Field table = clazz.getDeclaredField("table");
            table.setAccessible(true);
            Map.Entry<Integer, String>[] realTable = (Map.Entry<Integer, String>[]) table.get(map);

            //Iterate and do pretty printing
            for (int i = 0; i < realTable.length; i++) {
                System.out.println(String.format("Bucket : %d, Entry: %s", i, bucketToString(realTable[i])));
            }
    }

    private static String bucketToString(Map.Entry<Integer, String> entry) throws Exception {
            if (entry == null) return null;
            StringBuilder sb = new StringBuilder();

            //Access to the "next" filed of HashMap$Node
            Class clazz = entry.getClass();
            Field next = clazz.getDeclaredField("next");
            next.setAccessible(true); 

            //going through the bucket
            while (entry != null) {
                sb.append(entry);
                entry = (Map.Entry<Integer, String>) next.get(entry);
                if (null != entry) sb.append(" -> ");
            }
            return sb.toString();
        }
}

最后你会在 STDOUT 中看到类似这样的内容:

 Bucket : 0, Entry: null 
 Bucket : 1, Entry: null 
 Bucket : 2, Entry: Main$A@2a=abc -> Main$A@2a=def 
 Bucket : 3, Entry: null

【讨论】:

  • 注意:这对于生产代码来说是一个非常糟糕的主意,但是如果您将某些东西组合在一起进行一次性测试,那也没关系。
  • 我实际上希望 HashMap 提供一些我忽略的功能,但这是第二好的。
【解决方案3】:

Client.java

public class Client{
        public static void main(String[] args) {

            Map<Example, Number> m = new HashMap<>();
            Example e1  = new Example(100);  //point 1
            Example e2  = new Example(200);  //point2
            Example e3  = new Example(300);  //point3
            m.put(e1, 10);
            m.put(e2, 20);
            m.put(e3, 30);
            System.out.println(m);//point4
        }
    }

Example.java

public class Example {
    int s;
    Example(int s) {
        this.s =s;
    }
    @Override
    public int hashCode() {
        // TODO Auto-generated method stub
        return 5;
    }
}

现在在 Client.java 的第 1 点、第 2 点和第 3 点,我们在 hashmap m 中插入 3 个 Example 类型的键。由于在 Example.java 中 hashcode() 被覆盖,所有三个键 e1、e2、e3 将返回相同的哈希码,因此在 hashmap 中返回相同的桶。

现在的问题是如何查看密钥的分布。

方法:

  1. 在 Client.java 的 point4 处插入一个调试点。
  2. 调试 java 应用程序。
  3. 检查 m.
  4. 在 m 中,您会发现 HashMap$Node 类型的表数组,大小为 16。
  5. 这实际上是哈希表。每个索引都包含一个条目对象的链接列表,这些条目对象被插入到 hashmap 中。每个非空索引都有一个哈希变量,对应于 Hashmap 的 hash() 方法返回的哈希值。然后将此哈希值发送到 HashMap 的 indexFor() 方法以找出表数组的索引,将在其中插入 Entry 对象。 (请参阅@Rahul 在 cmets 中的链接以了解哈希和 indexFor 的概念)。
  6. 对于上述情况,如果我们检查表,您会发现除了一个键以外,其他所有键都为空。
  7. 我们已经插入了三个键,但我们只能看到一个,即所有三个键都被插入到同一个桶中,即表的同一个索引。
  8. 检查table数组元素(本例为5),key对应e1,value对应10(point1)
  9. next 变量指向链表的下一个节点,即下一个条目对象,在我们的例子中是 (e2, 200)。

因此,您可以通过这种方式检查哈希图。

另外我建议你通过 hashmap 的内部实现来理解 HashMap。

希望对您有所帮助..

【讨论】:

    【解决方案4】:

    HashMap 使用密钥对象的hashCode() 方法生成的密钥,所以我猜你真的在问这些哈希码值的分布有多均匀。您可以使用Map.keySet() 获取关键对象。

    现在,HashMap 的 OpenJDK 和 Oracle 实现不直接使用密钥哈希码,而是在将提供的哈希分配到存储桶之前应用另一个哈希函数。但是你不应该依赖或使用这个实现细节。所以你应该忽略它。因此,您只需确保键值的 hashCode() 方法分布良好。

    检查一些示例键值对象的实际哈希码不太可能告诉您任何有用的信息,除非您的哈希 cide 方法非常很差。您最好对哈希码方法进行基本的理论分析。这并不像听起来那么可怕。您可以(实际上,别无选择,只能这样做)假设提供的 Java 类的哈希码方法分布良好。然后,您只需要检查用于组合数据成员的哈希码的方法对于数据成员的预期值表现良好。仅当您的数据成员具有以特殊方式高度相关的值时,这可能是一个问题。

    【讨论】:

      猜你喜欢
      • 2012-04-16
      • 1970-01-01
      • 2017-07-04
      • 2023-03-09
      • 2020-07-03
      • 1970-01-01
      • 2011-04-07
      • 2021-04-11
      • 2016-10-01
      相关资源
      最近更新 更多