特征和接口二进制兼容吗?

Are traits and interfaces binary compatible?

令我感到惊讶的是 Scala 在不同版本中是 binary incompatible。现在,由于在 Java 8 中我们有默认方法实现,这与 trait 提供给我们的几乎相同,在 Java 代码中使用特征是否安全?我尝试自己使用它:

trait TestTrait {
  def method(v : Int)
  def concrete(v : Int) = println(v)
}

public class Test implements TestTrait{ // Compile-error. Implement concrete(Int)
    @Override
    public void method(int v) {
        System.out.println(v);
    }
}

但是它拒绝编译。编译器抱怨没有实现 concrete(Int)。虽然我在TestTrait.

中指定了实现

当 Scala 2.11 编译器编译特征时,它不会生成具有默认方法的接口,因为生成的代码必须使用 Java 6. 在 Scala 2.12 中(需要 Java 8) 它会,所以如果你用 2.12 编译器编译你的 Scala 代码,我希望你应该能够以这种方式从 Java 使用它(至少对于像这样的简单情况)。

请注意,像这样的更改正是导致不同 Scala 版本二进制不兼容的原因:如果您尝试使用 Scala 2.12 中使用 Scala 2.11 编译的特征,它会尝试调用接口的默认方法,而这些方法不存在。

您的期望相互矛盾。

您 "surprised" 发现 Scala 在主要版本之间是二进制不兼容的,这表明您的期望恰恰相反:Scala 应该 甚至是二进制兼容的在主要版本之间。

但与此同时,您希望 Scala 对特性使用一种编码,这种编码依赖于在设计 Scala 2.11 的二进制格式时甚至不存在的特性。 Scala 2.11 的第一个 Release Candidate,即不允许进行更多更改的时间点,是在 Java 8 发布之前的两周。要求每个 Scala 用户在发布之前安装 Java 8,这太荒谬了。

因此,一方面,您期望完全的二进制兼容性,即完全没有变化。另一方面,您希望使用最新最好的功能,即尽可能快地进行更改。你不能两者兼得。你必须选择。

而且,正如 Alexey 在他的回答中已经指出的那样,正是 这样的改进,需要 破坏二进制兼容性。

如果您具有二进制兼容性,那么如果您找到更好的二进制表示形式,则无法更改您的二进制表示形式。当目标平台的新功能可用时,您将无法使用它们。这是非常严格的,特别是对于像 Scala 这样的语言,它突破了可以在 JVM 上合理编码的界限。强制编译器设计者在第一次就获得 "everything right" 是非常苛刻的。

以下是多年来发生的一些变化并破坏了向后兼容性:

  • lambdas的编码,使用MethodHandles,当它们被添加到Java7时。他们不能有"gotten this right the first time",因为那时MethodHandles甚至不存在。
  • (在即将到来的 2.12.)lambdas 的编码,再次,因此它们与 Java 8 的编码相同。他们不可能有 "gotten this right the first time",因为当时 Java.
  • 甚至不存在 lambdas
  • (在即将到来的 2.12 中。)在 interface 中使用 default 方法对特征进行编码。他们不可能有 "gotten this right the first time",因为当时 default 方法在 Java.
  • 中甚至不存在

如果 Java 平台获得适当的尾调用或至少适当的尾递归,我敢肯定,ABI 将再次更改以利用这些功能。而如果我们在 JVM 中获取 Value Types,Scala 中 Value 类 的编码很可能会发生变化。

不过,在dotc, the compiler for Dotty, the team is trying a new approach to binary compatiblity: TASTy。 TASTy 是类型化抽象语法树的序列化格式。这个想法是保证 TASTy 的二进制兼容性,但不保证最终输出。 TASTy 包含重新编译程序所需的所有信息,所以如果你想合并两个由不同编译器编译的闭源库,那不是问题,因为你可以扔掉编译后的代码并从 TASTy 重新编译。

TASTy 将始终与编译后的代码一起发布。例如。对于 Scala-JVM,序列化的 TASTy 将在 .class 文件或 .jar 的元数据部分中传送,对于 Scala.js 在编译源文件内的注释或二进制数组中,对于 Scala -native 在已编译的 .dll.exe.so.dylib 等的元数据部分。

回到你关于特质的具体问题:

目前,单个特征编码为:

  • 一个 interface 包含 all 特征方法(抽象和具体)的抽象声明
  • 一个静态 class 包含所有特征的具体方法的静态方法,带一个额外的参数 $this
  • 在混合特征的继承层次结构中的每个点,特征中所有具体方法的合成转发器方法转发到静态class
  • 的静态方法

因此,以下 Scala 代码:

trait A {
  def foo(i: Int) = i + 1
  def abstractBar(i: Int): Int
}

trait B {
  def baz(i: Int) = i - 1
}

class C extends A with B {
  override def abstractBar(i: Int) = i * i
}

会这样编码:

interface A {
    int foo(int i);
    int abstractBar(int i);
}

abstract class A$class {
    static void $init$(A $this) {}
    static int foo(A $this, int i) { return i + 1; }
}

interface B {
    int baz(int i);
}

abstract class B$class {
    static void $init$(B $this) {}
    static int baz(B $this, int i) { return i - 1; }
}

class C implements A, B {
    public C() {
        A$class.$init$(this);
        B$class.$init$(this);
    }

    @Override public int baz(int i) { return B$class.baz(this, i); }
    @Override public int foo(int i) { return A$class.foo(this, i); }
    @Override public int abstractBar(int i) { return i * i; }
}

但在 Scala 2.12 中目标为 Java 8,它看起来更像这样:

interface A {
    static void $init$(A $this) {}
    static int foo$(A $this, int i) { return i + 1; }
    default int foo(int i) { return A.foo$(this, i); };
    int abstractBar(int i);
}

interface B {
    static void $init$(B $this) {}
    static int baz$(B $this, int i) { return i - 1; }
    default int baz(int i) { return B.baz$(this, i); }
}

class C implements A, B {
    public C() {
        A.$init$(this);
        B.$init$(this);
    }

    @Override public int abstractBar(int i) { return i * i; }
}

如您所见,静态方法和转发器的旧设计已被保留,它们只是被折叠到界面中。特征的具体方法现在已作为 static 方法移入接口本身,转发器方法不是在每个 class 中合成,而是定义一次为 default 方法,静态 $init$ 方法(表示 trait 主体中的代码)也已移至界面中,从而无需伴随静态 class。

大概可以这样简化:

interface A {
    static void $init$(A $this) {}
    default int foo(int i) { return i + 1; };
    int abstractBar(int i);
}

interface B {
    static void $init$(B $this) {}
    default int baz(int i) { return i - 1; }
}

class C implements A, B {
    public C() {
        A.$init$(this);
        B.$init$(this);
    }

    @Override public int abstractBar(int i) { return i * i; }
}

我不确定为什么没有这样做。乍一看,当前的编码可能会给我们一些向前兼容性:你可以使用新编译器编译的特征和旧编译器编译的 classes,那些旧的 classes 将简单地覆盖default 它们从具有相同接口的接口继承的转发器方法。除此之外,转发器方法将尝试调用 A$classB$class 上的静态方法,它们不再存在。