写点什么

Java 中的 String.hashCode() 方法可能有问题?

  • 2018-08-14
  • 本文字数:3179 字

    阅读完需:约 10 分钟

过去几天,我一直在浏览 Reddit 上的一篇文章。这篇文章看得我要抓狂了。文章指出,Java 中的 String.hashCode() 方法(将任意长度的字符串对象映射成 32 位 int 值)生成的哈希值存在冲突。文章作者似乎对这个问题感到很惊讶,并声称 String.hashCode() 的算法是有问题的。用作者自己的话说:

不管使用哪一种哈希策略,冲突都是不可避免的,但其中总有相对较好的哈希也有较差的哈希。你可以认为 String 中的哈希是比较差的那种。

作者的措辞带有相当强烈的意味,并且已经证明了很多奇怪的短字符串在生成哈希时会产生冲突。(文章中提供了很多示例,例如!~ 和"_)。众所周知,32 位字符串哈希函数确实会发生很多冲突,但从经验来看,在真实的场景中,String.hashCode() 能够很好地管理哈希冲突。

那么“差”的哈希是什么样子的呢?而“好”的哈希又是什么样子的?

一点理论

32 位哈希只能占用 2^32 = 4,294,967,296 个唯一值。因为字符串中可以包含任意数量的字符,所以可能的字符串显然要比这个数字多得多。因此,根据鸽子原则,必然会存在冲突。

但冲突的可能性有多大?

著名的生日问题表明,对于 365 个可能的“哈希值”,在哈希冲突可能性达到 50%之前,必须计算出 23 个唯一哈希值。如果有 2^32 个可能的哈希值,那么在达到 50%的哈希冲突可能性之前,必须计算出大约 77,164 个唯一哈希值。根据这个近似公式:

复制代码
from math import exp
def prob(x):
return 1.0 -exp(-0.5 * x * (x-1) / 2**32)
prob(77163) # 0.4999978150170551
prob(77164) # 0.500006797931095

那么对于给定数量的独立哈希,预计会发生多少次冲突?所运的是,维基百科为此提供了一个封闭式方程式:

复制代码
def count(d, n):
return n - d + d * ((d - 1) / d)**n

这种封闭式的解决方案可用于在实际的哈希函数中加入理论拟合。

一点实践

那么 String.hashCode() 符合标准吗?试着运行这段代码:

复制代码
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.nio.charset.StandardCharsets;
public class HashTest {
private static Map<Integer,Set> collisions(Collection values) {
Map<Integer,Set> result=new HashMap<>();
for(T value : values) {
Integer hc=Integer.valueOf(value.hashCode());
Set bucket=result.get(hc);
if(bucket == null)
result.put(hc, bucket = new TreeSet<>());
bucket.add(value);
}
return result;
}
public static void main(String[] args) throws IOException {
System.err.println("Loading lines from stdin...");
Set lines=new HashSet<>();
try (BufferedReader r=new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
for(String line=r.readLine();line!=null;line=r.readLine())
lines.add(line);
}
// Warm up, if you please
System.err.print("Warming up");
for(int i=0;i<10;i++) {
System.err.print(".");
collisions(lines);
}
System.err.println();
System.err.println("Computing collisions...");
long start=System.nanoTime();
Map<Integer,Set> collisions=collisions(lines);
long finish=System.nanoTime();
long elapsed=finish-start;
int maxhc=0, maxsize=0;
for(Map.Entry<Integer,Set> e : collisions.entrySet()) {
Integer hc=e.getKey();
Set bucket=e.getValue();
if(bucket.size() > maxsize) {
maxhc = hc.intValue();
maxsize = bucket.size();
}
}
System.out.println("Elapsed time: "+elapsed+"ns");
System.out.println("Total unique lines: "+lines.size());
System.out.println("Time per hashcode: "+String.format("%.4f", 1.0*elapsed/lines.size())+"ns");
System.out.println("Total unique hashcodes: "+collisions.size());
System.out.println("Total collisions: "+(lines.size()-collisions.size()));
System.out.println("Collision rate: "+String.format("%.8f", 1.0*(lines.size()-collisions.size())/lines.size()));
if(maxsize != 0)
System.out.println("Max collisions: "+maxsize+" "+collisions.get(maxhc));
}
}

我们使用短字符串(words.txt,链接见文末)作为输入:

复制代码
$ cat words.txt | java HashTest
Loading lines from stdin...
Warming up..........
Computing collisions...
Elapsed time: 49117411ns
Total unique lines: 466544
Time per hashcode: 105.2793ns
Total unique hashcodes: 466188
Total collisions: 356
Collision rate: 0.00076306
Max collisions: 3 [Jr, KS, L4]

在这些英文短字符串中,总共有 466,544 个哈希,出现 356 次冲突。从理论上讲,“公平”的哈希函数应该只会产生 25.33 次冲突。因此,String.hashCode() 产生的冲突是公平哈希函数的 14.05 倍: 356.0 / 25.33 ≈ 14.05

不过,每 10,000 个哈希出现 8 次冲突的概率仍然是个不错的成绩。

那么长字符串值的结果怎样?使用莎士比亚全集中的句子(链接见文末)会产生以下输出:

复制代码
$ cat shakespeare.txt | java HashTest
Loading lines from stdin...
Warming up..........
Computing collisions...
Elapsed time: 24106163ns
Total unique lines: 111385
Time per hashcode: 216.4220ns
Total unique hashcodes: 111384
Total collisions: 1
Collision rate: 0.00000897
0.00076306
Max collisions: 2 [ There's half a dozen sweets., PISANIO. He hath been search'd among the dead and living,]

在这些较长的英语字符串中,总共有 111,385 个哈希,出现 1 次冲突。“公平”哈希函数将在这些数据上产生 1.44 次冲突,因此 String.hashCode() 优于公平哈希函数,冲突可能性是公平哈希函数的 69.4%: 1 / 1.44 ≈ 0.694

也就是说,每 100,000 个哈希产生不到 1 个冲突,这个成绩是极好的。

一点解释

显然,String.hashCode() 不具备唯一性,它也不可能具备唯一性。对于短字符串,它与理论平均值差得比较远,但其实做得还算不错。对于长字符串,它可以轻松打败平均理论值。

总得来看,它对于预期字符串而言是具备唯一性的,可以将字符串很好地分布在哈希表中。

最后,我还是认为 String.hashCode() 是具备唯一性的,至少它足够“好”。

延伸阅读

如果你对这个问题感兴趣,我强烈建议你看一看 Stack Overflow 上的答案( https://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and-speed#answer-145633 ),它深入探讨了哈希函数冲突的问题。

重要链接:

Reddit 文章: https://www.reddit.com/r/coding/comments/967hci/stringhashcode_is_not_even_a_little_unique/

相关测试: https://vanilla-java.github.io/2018/07/26/Stringhash-Code-is-not-even-a-little-unique.html

生日问题: https://en.wikipedia.org/wiki/Birthday_problem

words.txt: http://sigpwned.com/wp-content/uploads/2018/08/words.txt

莎士比亚长句: http://sigpwned.com/wp-content/uploads/2018/08/shakespeare.txt

查看英文原文: http://sigpwned.com/2018/08/10/string-hashcode-is-plenty-unique/

2018-08-14 06:068191
用户头像

发布了 731 篇内容, 共 451.6 次阅读, 收获喜欢 2002 次。

关注

评论

发布
暂无评论
发现更多内容

软件测试——教育机构课程顾问常见黑话大全

程序员阿沐

程序员 软件测试 教育 机构 教育培训

阿里巴巴的“双11”高并发秒杀终极版教程!(Java语言设计)

Java架构追梦

Java 阿里巴巴 架构 面试 秒杀架构设计

重磅来袭:Spring之RequestBody的使用姿势小结

学Java关注我

Java 编程 架构 技术 程序人生

分布式锁之Redis实现

Sakura

4月日更

事件分发机制Android,熬夜整理Android面试笔试题,精心整理

欢喜学安卓

android 程序员 面试 移动开发

三步法助你快速定位网站性能问题

华为云开发者联盟

html 网站 网站优化 Performance面板 瀑布图

https如何使用python+flask来实现

华为云开发者联盟

Python flask https ssl HTTP协议

知识分享:SQL注入的流程和步骤

Thrash

sql

2020年12月的面试经历:美团4面+字节4面(均已拿offer),面试真题分享

Java架构师迁哥

Flink + Hudi 在 Linkflow 构建实时数据湖的生产实践

Apache Flink

flink

云管平台如何纳管多云资源?

嘉为蓝鲸

云计算 运维自动化 cmp 混合云 多云管理平台

轻松带你学习java-agent

华为云开发者联盟

Java Trace Java虚拟机 java-agent 挂载

前端DDD总结与思考

白玉兰开源

大前端 DDD

华云大咖说 | 华云数据与数科网维携手共建国产云生态

华云数据

实践案例丨Pt-osc工具连接rds for mysql 数据库失败

华为云开发者联盟

MySQL 数据库 pt-osc工具 rds for mysql

一入爬虫深似海,从此早睡是路人

Thrash

0门槛成为“技术牛人”!星环科技线上分享课“星课堂”开播,快来报名,一探究竟

星环科技

人工智能 数据库 云计算 大数据 直播技术

从源码分析 MySQL 死锁问题入门

比伯

Java 编程 程序员 架构 计算机

5个超好用的Instagram图片下载工具推荐

科技猫

分享 下载 教程 图片 Instagram

手把手教你从数据预处理开始体验图数据库

NebulaGraph

数据库 数据预处理

GitHub开源:17M超轻量级中文OCR模型、支持NCNN推理

不脱发的程序猿

人工智能 GitHub 开源 OCR 4月日更

Golang 对象池

escray

学习 极客时间 Go 语言 4月日更

前端规范之路

白玉兰开源

大前端 开发规范

智汇华云 | ArSDN打通软件定义数据中心的“任督二脉”

华云数据

事件分发源码,Android事件分发机制收藏这一篇就够了,威力加强版

欢喜学安卓

android 程序员 面试 移动开发

Redis的适用场景简单剖析

大数据技术指南

redis 4月日更

什么是Selenium?使用Selenium进行自动化测试

码语者

DevOps selenium

【转载】提高系统开发效率的“银弹”——X-series可视化大规模应用开发工具集

赫杰辉

前端⼤规模构建演进实践

白玉兰开源

架构 大前端

【转载】图形化系统开发组件X-Series(一)——XrossUnit介绍

赫杰辉

4行指令解决pip下载Python第三方库太慢问题(pip更换国内下载源)

不脱发的程序猿

Python pip 4月日更 Python库安装

Java中的String.hashCode()方法可能有问题?_Java_Andy_InfoQ精选文章