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

我很惊讶Scala在不同版本中是二进制不兼容的事实。 现在,因为在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)中,它会,因此如果您编译Scala代码使用2.12编译器,我希望您能够以这种方式从Java中使用它(至少对于像这样的简单情况)。

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

你的期望相互矛盾。

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

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

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

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

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

以下是一些经过多年改变并破坏向后兼容性的事情:

  • 使用MethodHandle进行lambda的编码,当它们在Java 7中添加时。他们不能“第一次就MethodHandle做”,因为当时的MethodHandle甚至都不存在。
  • (在即将发布的2.12中。)lambdas的编码, 再次 ,使它们与Java 8的编码相同。 他们不可能“第一次得到这个权利”,因为当时lambdas甚至不存在于Java中。
  • (在即将发布的2.12中。)使用interface s中的default方法对traits进行编码。 他们不可能“第一次就这么做”,因为当时在Java中甚至不存在default方法。

如果Java平台得到正确的尾调用或者至少是正确的尾部递归,我很确定,ABI将再次改变以利用这些function。 如果我们在JVM中获得Value Types,Scala中的Value Classes的编码可能会改变。

然而 ,在dotc的编译器中 ,团队正在尝试一种新的二进制兼容性方法: TASTy 。 TASTy是Typed抽象语法树的序列化格式。 这个想法是TASTy保证二进制兼容性,但最终输出不保证。 TASTy包含重新编译程序的所有必要信息,因此如果要组合由不同编译器编译的两个闭源库,那不是问题,因为您可以丢弃已编译的代码并从TASTy重新编译。

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

回到您关于特征的具体问题:

目前,单个特征编码为:

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

那么,以下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; } } 

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

它可能会像这样简化:

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

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