如何为 JVM 目标编译扩展 类 的 Scala 特征?

How are scala traits that extend classes compiled for a JVM target?

我从 this question 得知 Scala 为 trait 生成

trait A {
  def a = { ... }
}

类似于以下 Java 代码的结构

public interface A {
  public void a();
}
public class A$class {
  public static void a(A self) { ... }
}

但是,在 Scala 中,trait 可以扩展 class:

class B {
  def b = { ??? }
}
trait A extends B {
  def a = { ??? }
}

这如何翻译成 Java 等价物,接口不能从 类 继承? 是否为 B 生成了额外的接口? 此 Scala 功能对 Java 互操作性有任何影响吗?

嗯,你可以编译

class B {
  def b = { ??? }
}
trait A extends B {
  def a = { ??? }
}

并检查字节码:

> javap -c -l -p B
Compiled from "B.scala"
public class B {
  public scala.runtime.Nothing$ b();
    Code:
       0: getstatic     #16                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
       3: invokevirtual #19                 // Method scala/Predef$.$qmark$qmark$qmark:()Lscala/runtime/Nothing$;
       6: areturn
    LineNumberTable:
      line 2: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       7     0  this   LB;

  public B();
    Code:
       0: aload_0
       1: invokespecial #25                 // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 4: 0
      line 1: 4
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   LB;
}
> javap -c -l -p A
Compiled from "B.scala"
public interface A {
  public static scala.runtime.Nothing$ a$(A);
    Code:
       0: aload_0
       1: invokespecial #15                 // InterfaceMethod a:()Lscala/runtime/Nothing$;
       4: areturn
    LineNumberTable:
      line 5: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0 $this   LA;

  public scala.runtime.Nothing$ a();
    Code:
       0: getstatic     #22                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
       3: invokevirtual #25                 // Method scala/Predef$.$qmark$qmark$qmark:()Lscala/runtime/Nothing$;
       6: areturn
    LineNumberTable:
      line 5: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       7     0  this   LA;

  public static void $init$(A);
    Code:
       0: return
    LineNumberTable:
      line 4: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       1     0 $this   LA;
}

如您所见,scalac 使 A 只是一个具有默认方法的接口(它们自 Java 8 起可用)。你可能想知道,如果你想在 A 中调用一些 B 方法会发生什么,因为我们在字节码中看不到 A extends B:

trait A extends B {
  def a = { b; ??? }
}

现在A改为:

> javap -c -l -p A
Compiled from "B.scala"
public interface A {
  public static scala.runtime.Nothing$ a$(A);
    Code:
       0: aload_0
       1: invokespecial #15                 // InterfaceMethod a:()Lscala/runtime/Nothing$;
       4: areturn
    LineNumberTable:
      line 5: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0 $this   LA;

  public scala.runtime.Nothing$ a();
    Code:
       0: aload_0
       1: checkcast     #18                 // class B
       4: invokevirtual #21                 // Method B.b:()Lscala/runtime/Nothing$;
       7: athrow
    LineNumberTable:
      line 5: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       8     0  this   LA;

  public static void $init$(A);
    Code:
       0: return
    LineNumberTable:
      line 4: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       1     0 $this   LA;
}

我们可以看到代码使用 checkcasts 将 this 转换为 B 然后调用 'B 的方法 - 这意味着 scalac 将不得不确保然后实例化 A 它也将扩展 B!那么让我们检查一下当我们这样做时会发生什么:

class B {
  def b = { ??? }
}
trait A extends B {
  def a = { b; ??? }
}
class C extends A

而 C 是

> javap -c -l -p C
Compiled from "B.scala"
public class C extends B implements A {
  public scala.runtime.Nothing$ a();
    Code:
       0: aload_0
       1: invokestatic  #16                 // InterfaceMethod A.a$:(LA;)Lscala/runtime/Nothing$;
       4: areturn
    LineNumberTable:
      line 7: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   LC;

  public C();
    Code:
       0: aload_0
       1: invokespecial #22                 // Method B."<init>":()V
       4: aload_0
       5: invokestatic  #26                 // InterfaceMethod A.$init$:(LA;)V
       8: return
    LineNumberTable:
      line 7: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       9     0  this   LC;
}

如您所见,这非常 context-dependent 字节码是如何结束的 - 编译器将处理各种事情以使其最终适合所有内容,但前提是它知道您如何使用它。

因此,如果您希望与 Java 进行互操作,请坚持使用简单的 traits 和 classes 作为您的通用接口,并让 Scala 实例化更复杂的实现。从 Java 自己做这件事可能会以许多意想不到的方式对你造成伤害。