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

Dan*_*ini 3 java inheritance scala bytecode subtyping

我从这个问题中知道 Scala 为trait诸如

trait A {
  def a = { ... }
}
Run Code Online (Sandbox Code Playgroud)

类似于以下 Java 代码的结构

public interface A {
  public void a();
}
public class A$class {
  public static void a(A self) { ... }
}
Run Code Online (Sandbox Code Playgroud)

但是,在 Scala 中, atrait可以扩展 a class

class B {
  def b = { ??? }
}
trait A extends B {
  def a = { ??? }
}
Run Code Online (Sandbox Code Playgroud)

这在 Java 等价物中如何翻译,其中接口不能从类继承?是否为 生成了额外的接口B?这个 Scala 特性对 Java 互操作性有什么影响吗?

Mat*_*zok 6

嗯,你可以编译

class B {
  def b = { ??? }
}
trait A extends B {
  def a = { ??? }
}
Run Code Online (Sandbox Code Playgroud)

并检查字节码:

> 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;
}
Run Code Online (Sandbox Code Playgroud)
> 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;
}
Run Code Online (Sandbox Code Playgroud)

正如你看到scalacA只是默认方法的接口(他们是因为Java的8个可用)。你可能想知道,如果你想调用某个B方法会发生什么,A因为我们A extends B在字节码中看不到它:

trait A extends B {
  def a = { b; ??? }
}
Run Code Online (Sandbox Code Playgroud)

现在 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;
}
Run Code Online (Sandbox Code Playgroud)

正如我们所看到的,代码使用checkcasts 进行this转换B,然后调用 'B 的方法 - 这意味着scalac必须确保然后实例化A它也是扩展的东西B!所以让我们检查一下当我们这样做时会发生什么:

class B {
  def b = { ??? }
}
trait A extends B {
  def a = { b; ??? }
}
class C extends A
Run Code Online (Sandbox Code Playgroud)

而 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;
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,这与字节码的最终结果非常依赖于上下文 - 编译器将处理各种事情以使其最终适合事物,但前提是它知道您如何使用它。

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

  • Scala 编译器知道许多 JVM 无法使用的东西,这使得它能够使最终结果起作用,而“中间”步骤似乎是“错误的”。Java 编译器无法访问这些元数据,因为它只了解自己的元数据(以及一般的 JVM 数据),因此您可能会遇到这样的情况:A 不是 B 的子类,但 `scalac` 知道它何时会实例化`A`,那么它总是会生成 A 和 B 的实例子类型。JVM 字节码无法证明 `A 扩展 B`,但 scalac 知道 instanceOf[A] 隐含了 instanceOf[B],因为它会证明这一点。 (3认同)
  • 因此,在运行时,您永远不会遇到不是 B 实例的 A 实例(除非您从 Java 手动实例化 A!),即使字节码本身不提供此类保证。 (2认同)