【问题标题】:Are traits and interfaces binary compatible?特征和接口二进制兼容吗?
【发布时间】:2016-08-24 09:28:55
【问题描述】:

我对@9​​87654322@ 在不同版本中是binary incompatible 这一事实感到有点惊讶。现在,因为在Java 8 中我们有默认方法实现,这与traits 几乎相同,为我们提供了在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中指定了实现。

【问题讨论】:

  • 只有没有具体成员的特征才能与 Java 互操作。删除你的具体方法的实现,它就可以工作了。

标签: java scala interface traits


【解决方案1】:

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

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

【讨论】:

  • 我尝试使用使用 2.12.0-M5 的程序使用 2.11.4 编译的特征。它工作得很好:link。为什么?我使用了两个 maven 项目,其中一个配置了旧版本。
【解决方案2】:

您的期望相互矛盾。

您“惊讶”地看到 Scala 在主要版本之间是二进制不兼容的,这表明您期望相反:即使在主要版本之间,Scala 应该也是二进制兼容的。

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

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

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

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

以下是多年来发生变化并破坏向后兼容性的几件事:

  • 使用MethodHandles 的 lambda 编码,当它们被添加到 Java 7 中时。他们不可能“第一次就做到这一点”,因为那时 MethodHandles 甚至不存在。
  • (在即将发布的 2.12 中。)lambda 的编码,再次,因此它们与 Java 8 的编码相同。他们不可能“第一次就做对了”,因为当时 Java 中甚至不存在 lambda。
  • (在即将发布的 2.12 中。)使用 default 方法在 interfaces 中对特征进行编码。他们不可能“第一次就做对了”,因为当时 Java 中甚至不存在 default 方法。

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

然而,在dotc, the compiler for Dotty,团队正在尝试一种新的二进制兼容性方法:TASTy。 TASTy 是类型化抽象语法树的序列化格式。这个想法是保证 TASTy 的二进制兼容性,但不保证最终输出。 TASTy 包含了重新编译程序所需的所有信息,所以如果你想组合由不同编译器编译的两个闭源库,这不是问题,因为你可以扔掉编译好的代码并从 TASTy 重新编译。

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

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

目前,单个特征被编码为:

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

所以,下面的 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; }
}

但在面向 Java 8 的 Scala 2.12 中,它看起来更像这样:

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; }
}

如您所见,保留了带有静态方法和转发器的旧设计,它们只是折叠到界面中。 trait 的具体方法现在已作为static 方法移入接口本身,转发器方法并非在每个类中合成,而是定义一次为default 方法,以及静态$init$ 方法(表示trait body) 也被移到了接口中,因此不需要附带的静态类。

大概可以这样简化:

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; }
}

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-04
    • 2013-02-21
    • 2011-08-30
    • 2015-10-11
    • 1970-01-01
    • 2017-09-16
    相关资源
    最近更新 更多