为什么String.equals()在后台不使用hashCode()相等性检查?

时间:2019-01-06 21:59:48

标签: java string equals hashcode

为什么不使用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()

PS,我感觉我的测量方法可能是错误的,因此欢迎提出任何意见。另外,哈希值保存在String中,这也可能会产生一些误导性的结果...

UPD1 :正如@AdamSiemion所提到的,每次调用基准测试方法时都需要重新创建字符串,以避免散列代码的兑现:

  @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

因此,对于“相同大小,最后一个符号不同”的情况,我们仍然可以加快30%的速度。

UPD2 如@DanielPryden所述 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 不会创建新的String。因此,我们需要明确地这样做:

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()慢。这具有总体意义(如下面的评论中提到的@Carcigenicate):equals()需要通过char []进行完全遍历以生成哈希。我认为可能是hashCode()背后的某些内在因素使其变得更快,但事实并非如此。

因此,如果检查预先计算的hashCode()的存在并进行比较,仍然可以加快equals()的速度:

hash

在相同的字符串(用于检查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; } 字段的情况下,我们会得到一些小的(?)减速,但是在长度相同但内容不同且已经预先计算出哈希值的字符串的情况下,也会得到加速

不幸的是,由于无法更改String类的源代码,因此无法对其进行测试。

2 个答案:

答案 0 :(得分:1)

我同意比较hashCode(仅在已经计算过的情况下)看起来像可能,因为String对象是不可变的,因此可以提高性能。

注意事项:

  1. 仅当hashCode已经存在(已经计算)时,才进行增强。如果尚未计算hashCode,则计算可能需要比比较2个字符串的字符更长的时间。这是因为,在进行比较时,只要发现差异,我们就可以立即停止。例如,将“ aaxxxxxxxxxx”与“ aazzzzzzzzzzzzz”进行比较时,如果字符串不相等,我们可以在第二个字符之后停止。但是计算hashCode将需要遍历所有字符。

  2. 也许作者的决定基于关于如何使用字符串的统计信息。他们可能已经看到,hashCode的其他比较可能会使系统变慢。

    例如,如果大多数字符串与哈希映射/表一起使用,则hashCode已被比较和使用。剩下要比较的所有字符串都具有相同的hashCode,因此无需再次比较hashCode

  3. 对于同一对象,hash字段可能同时在多个线程中计算,尤其是如果equals()使用它的话。这需要加以考虑。

  4. 另一个要考虑的问题是内存使用情况。如果int字段为零,也许JVM中进行了优化以不使用内存?一旦不为零,会增加内存消耗吗?

最好有一种方法来调整和测量这个值(字符串是最终的)。也许使用一些字节码操作或使用其他类加载器...

以下是所建议内容的代码调整(OpenJDK和Oracle外观相同):

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) {

            // THE HASHCODE TWEAK
            if (hash != 0 &&
                anotherString.hash != 0 &&
                hash == anotherString.hash)
            {
                return true;
            }

            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;
}

答案 1 :(得分:0)

您的性能测试调用hashCode()数千次(使用jmh)没有意义,因为String哈希码已缓存:

/** Cache the hash code for the string */
private int hash; // Default to 0

public int hashCode() {
  int h = hash;
  if (h == 0 && value.length > 0) {
    char val[] = value;

    for (int i = 0; i < value.length; i++) {
      h = 31 * h + val[i];
    }
    hash = h;
  }
  return h;
}

因此,一旦计算出String哈希码,调用hashCode()几乎没有成本-这与大多数Java类相反,大多数Java类在每次调用hashCode()时都会重新计算哈希码。

通常equals()hashCode()快,因为通常使用短路评估。例如,如果您有一个包含10个字段的呼叫,并且两个提供的实例的第一个字段中的值不同,equals()将不会检查其余9个字段,而hashCode()(通常)是从所有字段中计算得出的10个字段。