【问题标题】:Should signature of equals be equals(x: Any) or equals(x: AnyRef)equals 的签名应该是 equals(x: Any) 还是 equals(x: AnyRef)
【发布时间】:2020-12-02 07:40:19
【问题描述】:

如果Scala中的equals方法要实现Java原有的boolean Object.equals(Object x)方法,我觉得应该写成def equals(that: AnyRef): Boolean

IntelliJ 会生成 def equals(that: Any): Boolean。网上我也遇到过用Any代替AnyRef的例子。

我应该定义参数Any还是AnyRef的类型?

我问这个是因为我想实现写this eq that的方法,但是如果that的类型是Any就不行,我需要先在AnyRef上进行模式匹配或特定的类。如果我在equals 定义中使用AnyRef,它显然可以工作,但我不确定我在Scala 中做的是否正确。

【问题讨论】:

  • 您的用例到底是什么?在大多数情况下,您不需要定义自己的 equals
  • 我这样做了,出于不相关的原因,我需要如上所述的引用相等。尝试使用this eq that 提出了澄清equals 确切定义的需求和问题。
  • 默认的equals不是已经被引用了吗?同样,为什么您需要自己重新实现它?
  • 我需要它final,我不希望子类更改引用比较。
  • 那么asInstanceOf[AnyRef] 是最简单的解决方案。它总是安全的,它只会触发基元的装箱。 - 无论如何,我只想给你我的拙见,听起来你的设计很糟糕,但无论如何。

标签: scala autoboxing language-interoperability


【解决方案1】:

如上所述,AnyRef 的默认 equals 是引用相等(继承自 java.lang.Objectequals 方法的签名是 (Ljava/lang/Object;)Z(即 java.lang.Object => Boolean 的 Java 字节码(技术上是 @987654327 @(第一个j.l.Othis))。

Scala 编译器在编译方法参数中的Any/AnyVal/AnyRef 时做了一些奇怪的事情。考虑:

class Foo {
  override def equals(that: Any): Boolean =
    that match {
      case r: AnyRef => this eq r
      case _ => false
    }
}

并在 REPL 中使用 :javap 来检查字节码:

  public boolean equals(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Z
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=5, args_size=2
         0: aload_1
         1: astore_3
         2: aload_3
         3: instanceof    #4                  // class java/lang/Object
         6: ifeq          27
         9: aload_3
        10: astore        4
        12: aload_0
        13: aload         4
        15: if_acmpne     22
        18: iconst_1
        19: goto          23
        22: iconst_0
        23: istore_2
        24: goto          35
        27: goto          30
        30: iconst_0
        31: istore_2
        32: goto          35
        35: iload_2
        36: ireturn

它实际上编译为一个 equals 方法,该方法只需要 Objects(然后执行一个 instanceof...)。

这确实提出了一个问题,即当我们尝试比较 AnyRefAnyVal 是否相等时会发生什么:

class Foo {
  override def equals(that: Any): Boolean =
    that match {
      case c: Char => c == 'M'
      case r: AnyRef => this eq r
      case _ => false
    }
}

object Bar {
  def cmpFooWithChar(f: Foo, c: Char): Boolean = f == c
}

Foo.equals:javap 中显示:

    3: instanceof    #18                 // class java/lang/Character

java.lang.Characterscala.Char 的内容。

Bar$:

public boolean cmpFooWithChar($line15.$read$$iw$$iw$Foo, char);
  descriptor: (L$line15/$read$$iw$$iw$Foo;C)Z
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=4, args_size=3
       0: aload_1
       1: iload_2
       2: invokestatic  #39                 // Method scala/runtime/BoxesRunTime.boxToCharacter:(C)Ljava/lang/Character;
       5: astore_3
       6: dup
       7: ifnonnull     18
      10: pop
      11: aload_3
      12: ifnull        25
      15: goto          29
      18: aload_3
      19: invokevirtual #43                 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
      22: ifeq          29
      25: iconst_1
      26: goto          30
      29: iconst_0
      30: ireturn

注意编译器如何处理== 操作:

  • Char 装箱到Character
  • 检查装箱对象或Foo 是否为null
  • 如果两者都不为空,则调用equals 方法

正在发生什么样的魔法?嗯,

def cmp(f: Foo, a: Any): Boolean = f == a

编译为(Foo, java.lang.Object) => Boolean 的签名。所以,有趣的是,确实如此:

def cmp(f: Foo, v: AnyVal): Boolean

是的,你没看错:AnyValAnyRefAny,因为函数参数类型在编译为字节码时都等价于 java.lang.Object,编译器会自动将 Int 装箱。

Scala 编译器留在instanceof java.lang.Object 指令中有点奇怪,尽管我怀疑 JIT 会优化检查。这确实有一个有趣的效果:如果我们在Foo.equals 的第二个实现中交换CharAnyRef 的情况,Char 的情况就变成了有效的死代码,因为它在instanceof java.lang.Object 之后。然而,我怀疑编译器的死代码检查对 Scala 类型进行操作,而不考虑 JVM 类型将是什么。

对于这些示例,看到 ScalaJS 发出的 JS 或 Scala Native 发出的 LLVM IR 会非常有趣。

【讨论】:

  • 如果您真的对多余的instanceof 感到偏执,那么您实际上可以使用override def equals(that: Any): Boolean = this eq that.asInstanceOf[AnyRef] 逃脱(至少在针对JVM 的Scala 代码中):这将是100% 安全的,尽管如此可能会触发几乎所有 linter,您应该准备好向任何看到您的代码的人解释自动装箱以及为什么在这种(可能只有这种)情况下裸铸是安全的。
猜你喜欢
  • 2011-03-20
  • 1970-01-01
  • 2013-04-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-09
相关资源
最近更新 更多