如上所述,AnyRef 的默认 equals 是引用相等(继承自 java.lang.Object,equals 方法的签名是 (Ljava/lang/Object;)Z(即 java.lang.Object => Boolean 的 Java 字节码(技术上是 @987654327 @(第一个j.l.O是this))。
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...)。
这确实提出了一个问题,即当我们尝试比较 AnyRef 和 AnyVal 是否相等时会发生什么:
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
- 检查装箱对象或
Foo 是否为null
- 如果两者都不为空,则调用
equals 方法
正在发生什么样的魔法?嗯,
def cmp(f: Foo, a: Any): Boolean = f == a
编译为(Foo, java.lang.Object) => Boolean 的签名。所以,有趣的是,确实如此:
def cmp(f: Foo, v: AnyVal): Boolean
是的,你没看错:AnyVal、AnyRef 和 Any,因为函数参数类型在编译为字节码时都等价于 java.lang.Object,编译器会自动将 Int 装箱。
Scala 编译器留在instanceof java.lang.Object 指令中有点奇怪,尽管我怀疑 JIT 会优化检查。这确实有一个有趣的效果:如果我们在Foo.equals 的第二个实现中交换Char 和AnyRef 的情况,Char 的情况就变成了有效的死代码,因为它在instanceof java.lang.Object 之后。然而,我怀疑编译器的死代码检查对 Scala 类型进行操作,而不考虑 JVM 类型将是什么。
对于这些示例,看到 ScalaJS 发出的 JS 或 Scala Native 发出的 LLVM IR 会非常有趣。