AI 帮我避免了 Map 计算哈希值的一个大坑

一、背景

一个业务场景需要对 Map 计算哈希值作为缓存 key 的构成部分。思路是将 Map 转为字符串,然后对字符串取 Hash 值。 不过这里有个很大的坑,即 Map 中 Entry 的顺序问题,即仅仅 Key 的顺序不同而值相同时哈希值应该相同。 如果使用 AI 大概率会注意到这个问题,如果直自己写很容易忽略这个问题。

二、问题复现

构造两个“键值”等价的 Map,发现其 hash 值不同。

代码语言:javascript
复制
import com.google.common.base.Charsets;
import com.google.common.hash.Hashing;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

public class Main {
public static void main(String[] args) {
Map<String, String> map1 = new LinkedHashMap<>();
map1.put("key1", "value1");
map1.put("key2", "value2");

    Map&lt;String, String&gt; map2 = new LinkedHashMap&lt;&gt;();
    map2.put(&#34;key2&#34;, &#34;value2&#34;);
    map2.put(&#34;key1&#34;, &#34;value1&#34;);

    String mapString1 = map1.entrySet()
            .stream()
            .map(e -&gt; e.getKey() + &#34;=&#34; + e.getValue())
            .collect(Collectors.joining(&#34;, &#34;));

    String mapString2 = map2.entrySet()
            .stream()
            .map(e -&gt; e.getKey() + &#34;=&#34; + e.getValue())
            .collect(Collectors.joining(&#34;, &#34;));

    String hash1 = Hashing.sha256()
            .hashString(mapString1, Charsets.UTF_8)
            .toString();

    String hash2 = Hashing.sha256()
            .hashString(mapString2, Charsets.UTF_8)
            .toString();

    System.out.println(&#34;The hash of the first map is: &#34; + hash1);
    System.out.println(&#34;The hash of the second map is: &#34; + hash2);
    System.out.println(hash1.equals(hash2));
}

}

输出的结果:

The hash of the first map is: 500ccf8d28b6857c91a01a1e6d751e89d3872af97a8079cecd1dce0497ab084d
The hash of the second map is: d7d67e36f33ebf3f51e1d0c6e016a17a2d839e897951bb98fa5cd190a135b331
false

有些同学可能会说,你这里用到的是 LinkedHashMap 当然会有问题。 但问题在于,通常我们定义接口时,参数为 Map 类型,两个“键值”等价,其 Entry 的顺序未必相等。

三、解决办法

对 Entry 或者 map 后的字符串排序,保证“键值”等价的两个 Map 在 hash 前拼接出的字符串相同。

代码语言:javascript
复制
import com.google.common.base.Charsets;
import com.google.common.hash.Hashing;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

public class MapHashDemo {
public static void main(String[] args) {
Map<String, String> map1 = new LinkedHashMap<>();
map1.put("key1", "value1");
map1.put("key2", "value2");

    Map&lt;String, String&gt; map2 = new LinkedHashMap&lt;&gt;();
    map2.put(&#34;key2&#34;, &#34;value2&#34;);
    map2.put(&#34;key1&#34;, &#34;value1&#34;);


    String hash1 = getSha256(map1);

    String hash2 = getSha256(map2);

    System.out.println(&#34;The hash of the first map is: &#34; + hash1);
    System.out.println(&#34;The hash of the second map is: &#34; + hash2);
    System.out.println(hash1.equals(hash2));
}


private static String getSha256(Map&lt;String, String&gt; map) {
    String mapString1 = map.entrySet()
            .stream()
            .map(e -&gt; e.getKey() + &#34;=&#34; + e.getValue())
            .sorted()
            .collect(Collectors.joining(&#34;, &#34;));

    return Hashing.sha256()
            .hashString(mapString1, Charsets.UTF_8)
            .toString();
}

}

The hash of the first map is: 500ccf8d28b6857c91a01a1e6d751e89d3872af97a8079cecd1dce0497ab084d
The hash of the second map is: 500ccf8d28b6857c91a01a1e6d751e89d3872af97a8079cecd1dce0497ab084d
true

这样哪怕 Map 中的 Entry 顺序不一致,计算出来的 sha256 的哈希值也相同。

三、AI 助力

另外,再次倡议大家日常工作和学习中可以尝试使用 ChatGPT 、 New Bing 这类 AI 工具,这类简单的代码可以让它们给出 Demo,它们给出的 Demo 时有很大概率可以给出正确的代码。

如何使用 com.google.common.hash.Hashing 计算一个 Map 的 hash 值?

ChatGPT 给出的答案:


com.google.common.hash.Hashing是 Google Guava库中的一个类,它提供了一系列的哈希函数。 计算一个Map的哈希值,我们可以先将Map的key和value转化为字符串,然后使用Hashing的哈希函数进行计算。 以下是一个简单的例子,假设我们有一个 Map :

代码语言:javascript
复制
import com.google.common.base.Charsets;
import com.google.common.hash.Hashing;
import java.util.Map;

public class Main {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");

    String mapString = map.entrySet()
                          .stream()
                          .map(e -&gt; e.getKey() + &#34;=&#34; + e.getValue())
                          .sorted()
                          .collect(Collectors.joining(&#34;, &#34;));

    String hash = Hashing.sha256()
                         .hashString(mapString, Charsets.UTF_8)
                         .toString();

    System.out.println(&#34;The hash of the map is: &#34; + hash);
}

}

这个例子中,我们首先将Map的每一项(key和value)转化为一个字符串,格式为"key=value",然后将这些字符串排序并用逗号连接起来,生成一个总的字符串。最后,我们用Guava的Hashing.sha256()函数计算这个字符串的哈希值。

**需要注意的是,由于 Map 的元素没有固定的顺序,所以在生成字符串的时候我们需要对其进行排序,以保证相同的 Ma p能够得到相同的哈希值。**同时,使用哈希函数时,需要选择适当的字符集,这里我们选择了UTF-8。

此外,还要注意哈希冲突的问题,尽管对于大多数情况来说,哈希冲突的概率非常小,但理论上还是存在可能的。所以在使用哈希值时,需要根据具体情况判断是否可以接受这种潜在的冲突风险。

四、总结

大家在对 Map 计算其哈希值作为缓存 key 时,需要特别注意这个问题。
建议大家在日常开发中一些简单的代码示例,一些验证性的代码,推荐大家优先让 AI 帮我们编写,有时候会有意外收获。


创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。