equals的签名应等于equalsx:AnyRef或equalsx:AnyRef

问题描述

如果Scala中的equals方法应该实现原始的Java boolean Object.equals(Object x)方法,我认为应该将其编写为def equals(that: AnyRef): Boolean

IntelliJ代替生成def equals(that: Any): Boolean。在线上,我还遇到了使用Any而不是AnyRef的示例。

我应该定义参数AnyAnyRef的类型吗?

我问这个问题是因为我想实现写this eq that方法,但是如果that的类型是Any则不起作用,我需要对-首先匹配AnyRef或特定类。如果我在AnyRef定义中使用equals,则显然可以正常工作,但不确定在Scala中是否做对了。

解决方法

如前所述,equals的默认AnyRef是引用相等(从java.lang.Object继承,equals方法的签名是(Ljava/lang/Object;)Z(即Java java.lang.Object => Boolean的字节码(技术上为(java.lang.Object,java.lang.Object) => Boolean(第一个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个(然后执行Object ...)的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.Character就是scala.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
  • 检查盒装对象还是Foonull
  • 如果两者都不为空,则调用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将优化检查。这确实产生了有趣的效果:如果我们在Char的第二个实现中交换AnyRefFoo.equals情况,则Char情况实际上变成了无效代码,因为它在{之后{1}}。我怀疑编译器的无效代码检查是对Scala类型进行的,而与JVM类型无关。

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