Scala特质如何编译成Java字节码?

47

我已经玩过Scala一段时间了,我知道traits可以作为Scala的接口和抽象类的等效物。那么traits到底是如何编译成Java字节码的呢?

我找到了一些简短的解释,它们指出当可能时,traits会被编译成与Java接口完全相同的形式,否则会编译成包含另一个类的接口。然而,我仍然不明白Scala如何实现类线性化,这是Java没有的特性。

是否有好的资源可以解释traits如何编译成Java字节码?


1
我并不是在开玩笑,建议尝试使用类文件反编译器 javap。如果需要更高层次的概述,请参阅博客文章 *Interop Between Java and Scala*(2009-02-09)。 - Ben Hardy
这里提供了更高层次的概述:http://www.codecommit.com/blog/java/interop-between-java-and-scala - Ben Hardy
我还没有尝试使用javap。感谢提供链接,我希望能够得到更多细节,但这是一个很好的起点。 - Justin Ardini
请注意,与 javap 命令一样,您需要添加 -c 标志才能使其输出字节码。否则它只会显示方法定义的摘要。或者使用 -v 获取更多信息。 - Nick
这里可能有一些好的答案:https://dev59.com/Pmsz5IYBdhLWcg3w0rVC#7637888 - Jus12
4个回答

67

我不是专家,但这是我的理解:

Traits会被编译成对应的接口和类。

trait Foo {
  def bar = { println("bar!") }
}

变成了等同于...

public interface Foo {
  public void bar();
}

public class Foo$class {
  public static void bar(Foo self) { println("bar!"); }
}

这就留下了一个问题:Foo$class中的静态条形方法是如何被调用的?这个魔法是由编译器在混入Foo特质的类中完成的。

class Baz extends Foo

变成像这样...

public class Baz implements Foo {
  public void bar() { Foo$class.bar(this); }
}

类的线性化只是根据语言规范中定义的线性化规则实现方法(在Xxxx$class类中调用静态方法)的适当版本。


谢谢!最后两个位是我不清楚的地方。解释得很好。 - Justin Ardini
我冒昧修复了Scala或Java代码的语法高亮显示,这个功能可能在回答发布时甚至还不存在。 - skiwi
我认为最近的Scala(2.11)将Foo$class生成为抽象类,即:public abstract class Foo$class - David Soroko

5
为了讨论方便,让我们看一个使用多个特质的Scala示例,其中包含抽象方法和具体方法:
trait A {
  def foo(i: Int) = ???
  def abstractBar(i: Int): Int
}

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

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

目前(即从Scala 2.11开始),单个trait被编码为:

  • 一个包含所有trait方法的抽象声明(包括抽象和具体方法)的interface
  • 一个抽象静态类,其中包含针对所有trait具体方法的静态方法,它需要一个额外的参数$this(在早期版本的Scala中,这个类不是抽象的,但实例化它没有意义)
  • 在trait混合的每个继承层次结构中,为trait中的所有具体方法创建合成的转发器方法,将其转发到静态类的静态方法

这种编码的主要优点是,没有具体成员的trait(与接口同构)实际上被编译为接口。

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

interface B {
    int baz(int i);
}

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

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

然而,Scala 2.12需要Java 8,因此能够在接口中使用默认方法和静态方法,结果看起来更像这样:
interface A {
    static void $init$(A $this) {}
    static int foo$(A $this, int i) { return ???; }
    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 ???; }
    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 ???; }
}

如您所见,旧设计中的静态方法和转发器已经保留,它们仅被折叠到接口中。特质的具体方法现在已经作为static方法移动到了接口本身中,转发器方法不会被合成到每个类中,而是定义为default方法,并且静态$init$方法(表示特质主体中的代码)也已经移动到接口中,进而使伴随静态类变得不必要。

这样可能更简单:

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

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

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

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

我不确定为什么没有这样做。乍一看,当前的编码方式可能会给我们带来一些向前兼容性:您可以使用新编译器编译的特性与旧编译器编译的类一起使用,这些旧类将简单地用相同的默认转发方法覆盖它们从接口继承的default转发方法。但是,转发方法将尝试调用A$classB$class上的静态方法,而这些方法已经不存在了,因此这种假想的向前兼容性实际上并不起作用。

由于“abstract override”,您提出的方法无法实现。给定“abstract class A { def x: Int }; trait T { override def x = 2 }; trait S { override abstract def x = super.x * 2 }”,如何在不复制代码并破坏前向兼容性的情况下实现“class B extends A with T with S”?静态方法引用相同的实现,忽略虚拟调度,这正是我们需要的。目前,它们只是一个绕过虚拟调度的“invokespecial”指令,但可能需要更改,因此可以使用它。 - HTNW

2

2
在Scala 12和Java 8的背景下,您可以在提交8020cd6中看到另一个解释:

更好的2.12特质编码内联支持

在2.12周期的后期进行了一些特质编码的更改,但内联器并没有以最佳方式支持它。

在2.12.0中,具体的特质方法被编码为

interface T {
  default int m() { return 1 }
  static int m$(T $this) { <invokespecial $this.m()> }
}
class C implements T {
  public int m() { return T.m$(this) }
}

如果选择内联一个特质方法,2.12.0内联器会将其主体复制到静态超级访问器`T.m$`,然后再从中复制到混合转发器`C.m`。此提交对内联器进行了特殊处理:
- 不要内联到静态超级访问器和混合转发器中。 - 相反,当内联一个混合转发器的调用时,内联器还会跟随两个转发器并内联特质方法的主体。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接