31x+y=31a+b 可以变形为:
31x-31a=b-y。
即,31(x-a)=b-y。
这个时候就清晰很多了,很明显,上面的等式有一个特殊解:
x-a=1,b-y=31。
因为,由上可得:对于任意两个字符串 xy 和 ab,如果它们满足 x-a=1,即第一个字符的 ASCII 码值相差为 1,同时满足 b-y=31,即第二个字符的 ASCII 码值相差为 -31。那么这两个字符的 hashCode 一定相等。
都已经说的这么清楚了,这样的组合对照着 ASCII 码表来找,不是一抓一大把吗?
Aa 和 BB,对不对?
Ab 和 BC,是不是?
Ac 和 BD,有没有?
好的。现在,我们可以生成两个 HashCode 一样的字符串了。
我们在稍微加深一点点难度。假设我要构建 2 个以上 HashCode 一样的字符串该怎么办?
我们先分析一下。
Aa 和 BB 的 HashCode 是一样的。我们把它两一排列组合,那不还是一样的吗?
比如这样的:AaBB,BBAa。
再比如我之前《震惊!ConcurrentHashMap里面也有死循环?》这篇文章中出现过的例子,AaAa,BBBB:
你看,神奇的事情就出现了。
我们有了 4 个 hashCode 一样的字符串了。
有了这 4 个字符串,我们再去和 Aa,BB 进行组合,比如 AaBBAa,BBAaBB......
4*2=8 种组合方式,我们又能得到 8 个 hashCode 一样的字符串了。
等等,我好像发现了什么规律似的。
如果我们以 Aa,BB 为种子数据,经过多次排列组合,可以得到任意个数的 hashCode 一样的字符串。字符串的长度随着个数增加而增加。
文字我还说不太清楚,直接 show you code 吧,如下:
public class CreateHashCodeSomeUtil { /** * 种子数据:两个长度为 2 的 hashCode 一样的字符串 */ private static String[] SEED = new String[]{"Aa", "BB"}; /** * 生成 2 的 n 次方个 HashCode 一样的字符串的集合 */ public static List<String> hashCodeSomeList(int n) { List<String> initList = new ArrayList<String>(Arrays.asList(SEED)); for (int i = 1; i < n; i++) { initList = createByList(initList); } return initList; } public static List<String> createByList(List<String> list) { List<String> result = new ArrayList<String>(); for (int i = 0; i < SEED.length; ++i) { for (String str : list) { result.add(SEED[i] + str); } } return result; } }
通过上面的代码,我们就可以生成任意多个 hashCode 一样的字符串了。
就像这样:
所以,别再问出这样的问题了:
有了这些 hashCode 一样的字符串,我们把这些字符串都放到HashMap 中,代码如下:
public class HashMapTest { public static void main(String[] args) { Map<String, String> hashMap = new HashMap<String, String>(); hashMap.put("Aa", "Aa"); hashMap.put("BB", "BB"); hashMap.put("AaAa", "AaAa"); hashMap.put("AaBB", "AaBB"); hashMap.put("BBAa", "BBAa"); hashMap.put("BBBB", "BBBB"); hashMap.put("AaAaAa", "AaAaAa"); hashMap.put("AaAaBB", "AaAaBB"); hashMap.put("AaBBAa", "AaBBAa"); hashMap.put("AaBBBB", "AaBBBB"); hashMap.put("BBAaAa", "BBAaAa"); hashMap.put("BBAaBB", "BBAaBB"); hashMap.put("BBBBAa", "BBBBAa"); hashMap.put("BBBBBB", "BBBBBB"); } }
最后这个 HashMap 的长度会经过两次扩容。扩容之后数组长度为 64:
但是里面只被占用了三个位置,分别是下标为 0,31,32 的地方
画图如下:
看到了吧,刺不刺激,长度为 64 的数组,存 14 个数据,只占用了 3 个位置。
这空间利用率,也太低了吧。
所以,这样就算是 hack 了 HashMap。恭喜你,掌握了一项黑客攻击技术:hash 冲突 Dos 。
如果你想了解的更多。可以看看石头哥的这篇文章:《没想到 Hash 冲突还能这么玩,你的服务中招了吗?》。
看到上面的图,不知道大家有没有觉得有什么不对劲的地方?
如果没有,那么我再给你提示一下:数组下标为 32 的位置下,挂了一个长度为 8 的链表。
是不是,恍然大悟了。在 JDK 8 中,链表转树的阈值是多少?