为什么不在equals()方法中使用hashCode()来预先检查相等性呢?
快速测试草案:
@Fork(value = 1)
@Warmup(time = 1)
@Measurement(time = 1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class Main {
@Param({
"o", // differ size
"oooooooooooooooooo1", // same size, differ last symbol
"oooooooooooooooooo2" // same content
})
String string1;
@Param({
"oooooooooooooooooo2"
})
String string2;
@Benchmark
public void stringEquals(Blackhole bh) {
bh.consume(string1.equals(string2));
}
@Benchmark
public void myEquals(Blackhole bh) {
bh.consume(myEquals(string1, string2));
}
boolean myEquals(String str1, String str2){
if (str1.hashCode()==str2.hashCode()) {
return str1.equals(str2);
}
return false;
}
}
结果:
Benchmark (string1) (string2) Mode Cnt Score Error Units
Main.myEquals o oooooooooooooooooo2 avgt 5 5.552 ± 0.094 ns/op
Main.myEquals oooooooooooooooooo1 oooooooooooooooooo2 avgt 5 5.626 ± 0.173 ns/op
Main.myEquals oooooooooooooooooo2 oooooooooooooooooo2 avgt 5 14.347 ± 0.234 ns/op
Main.stringEquals o oooooooooooooooooo2 avgt 5 6.441 ± 1.076 ns/op
Main.stringEquals oooooooooooooooooo1 oooooooooooooooooo2 avgt 5 13.596 ± 0.348 ns/op
Main.stringEquals oooooooooooooooooo2 oooooooooooooooooo2 avgt 5 13.663 ± 0.126 ns/op
如您所见,对于“大小相同,最后一个符号不同”的情况,我们获得了很好的加速。
我认为在String.equals()
的实现中,hashCode()
的相等性检查应该替换为length()
的相等性检查,因为它们需要相同的时间:
@Benchmark
public void emptyTest(Blackhole bh) {
bh.consume(0);
}
@Benchmark
public void stringLength(Blackhole bh) {
bh.consume(string2.length());
}
@Benchmark
public void stringHashCode(Blackhole bh) {
bh.consume(string2.hashCode());
}
Benchmark (string2) Mode Cnt Score Error Units
Main.emptyTest oooooooooooooooooo2 avgt 5 3.702 ± 0.086 ns/op
Main.stringHashCode oooooooooooooooooo2 avgt 5 4.832 ± 0.421 ns/op
Main.stringLength oooooooooooooooooo2 avgt 5 5.175 ± 0.156 ns/op
PS 我觉得我的测量方法可能有误,所以欢迎任何评论。此外,哈希保存在String内部,这也可能产生一些误导性的结果...
更新1: 正如@AdamSiemion提到的,我们需要在每次调用基准测试方法时重新创建字符串,以避免哈希码的缓存:
String str1, str2;
@Setup(value = Level.Invocation)
public void setup(){
str1 = string1;
str2 = string2;
}
@Benchmark
public void stringEquals(Blackhole bh) {
bh.consume(str1.equals(str2));
}
@Benchmark
public void myEquals(Blackhole bh) {
bh.consume(myEquals(str1, str2));
}
Benchmark (string1) (string2) Mode Cnt Score Error Units
Main.myEquals o oooooooooooooooooo2 avgt 5 29.417 ± 1.430 ns/op
Main.myEquals oooooooooooooooooo1 oooooooooooooooooo2 avgt 5 29.635 ± 2.053 ns/op
Main.myEquals oooooooooooooooooo2 oooooooooooooooooo2 avgt 5 37.628 ± 0.974 ns/op
Main.stringEquals o oooooooooooooooooo2 avgt 5 29.905 ± 2.530 ns/op
Main.stringEquals oooooooooooooooooo1 oooooooooooooooooo2 avgt 5 38.090 ± 2.933 ns/op
Main.stringEquals oooooooooooooooooo2 oooooooooooooooooo2 avgt 5 36.966 ± 1.642 ns/op
所以,对于“大小相同,最后一个符号不同”的情况,我们仍然有近30%的加速。
更新2 正如@DanielPryden提到的,str1 = string1
不会创建新的字符串。因此我们需要明确地这样做:
@Setup(value = Level.Invocation)
public void setup(){
str1 = new String(string1);
str2 = new String(string2);
}
Benchmark (string1) (string2) Mode Cnt Score Error Units
Main.myEquals o oooooooooooooooooo2 avgt 5 61.662 ± 3.068 ns/op
Main.myEquals oooooooooooooooooo1 oooooooooooooooooo2 avgt 5 85.761 ± 7.766 ns/op
Main.myEquals oooooooooooooooooo2 oooooooooooooooooo2 avgt 5 92.156 ± 8.851 ns/op
Main.stringEquals o oooooooooooooooooo2 avgt 5 30.789 ± 0.731 ns/op
Main.stringEquals oooooooooooooooooo1 oooooooooooooooooo2 avgt 5 38.602 ± 1.212 ns/op
Main.stringEquals oooooooooooooooooo2 oooooooooooooooooo2 avgt 5 38.921 ± 1.816 ns/op
所以,现在我们得到了预期的结果:使用
hashCode()
总是比equals()
慢。这是有道理的(正如@Carcigenicate在下面的评论中提到的):hashCode()
需要完全遍历char[]来生成哈希值。我曾经认为可能是hashCode()
底层的某些内在机制使其更快,但事实并非如此。因此,如果检查预先计算的
hash
是否存在并进行比较,则仍然可以加速equals()
。public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length
// new code begins
&& (hash==0 || anotherString.hash==0 || hash==anotherString.hash)) {
// new code ends
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
当检查 hash
字段时,如果字符串相等,我们会遇到一些轻微的减速,但是对于长度相同但内容不同且已经预先计算哈希值的字符串,将获得加速效果。
不幸的是,由于无法更改 String 类的源代码,因此我无法进行测试。
equals
检查可能会需要第二次迭代。你将会进行一次完整的迭代,只是为了看是否需要执行另一个迭代,除非该哈希值已经被预先计算过。 - CarcigenicateSystem.out.println("FB".hashCode() == "Ea".hashCode());
,然后您需要进一步测试以确定它们是否实际上相等。 - Elliott Frisch=
运算符的赋值操作永远不会创建一个新对象(除了装箱转换,这里不适用)。str1 = string1
意味着您现在有两个变量都引用同一个字符串对象。所有的子类型都属于java.lang.Object
的类型都是引用类型。类型为String
的变量不是一个字符串,而是一个指向字符串的引用。 - Daniel Pryden