Scala 特征是如何编译成 Java 字节码的?

Posted

技术标签:

【中文标题】Scala 特征是如何编译成 Java 字节码的?【英文标题】:How are Scala traits compiled into Java bytecode? 【发布时间】:2011-02-03 04:17:33 【问题描述】:

我已经玩过 Scala 一段时间了,我知道特质可以充当接口和抽象类的 Scala 等价物。特征究竟是如何编译成 Java 字节码的?

我发现了一些简短的解释,说明特征尽可能像 Java 接口一样编译,否则与附加类进行接口。但是,我仍然不明白 Scala 是如何实现类线性化的,这是 Java 中没有的功能。

有没有很好的资料可以解释特征如何编译成 Java 字节码?

【问题讨论】:

我真的不是想在这里翻转,而是尝试使用类文件反汇编器javap。如需更高级的概述,请参阅博文 Interop Between Java and Scala (2009-02-09)。 这提供了一个更高级的概述:codecommit.com/blog/java/interop-between-java-and-scala 我没试过用过javap。我很欣赏这个链接,我希望能提供更多细节,但这是一个很好的起点。 请注意,和javap 命令一样,您需要添加-c 标志以使其输出字节码。否则,它只显示方法定义的摘要。或使用-v 了解更多信息。 这里有一些好的答案***.com/a/7637888/243233 【参考方案1】:

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

特征被编译成一个接口和相应的类。

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 中的静态 bar 方法是如何被调用的?这个魔法是由编译器在 Foo 特征被混合到的类中完成的。

class Baz extends Foo

变成...

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

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

【讨论】:

谢谢!最后 2 位是我不清楚的地方。很好的解释。 我冒昧地修复了 scala resp java 代码的语法高亮,这个功能在这个答案发布时甚至可能不可用。 我认为最近的 Scala (2.11) 生成 Foo$class 作为抽象,即:public abstarct class Foo$class 【参考方案2】:

为了便于讨论,让我们看一下下面的 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 开始),单个特征被编码为:

一个interface 包含所有特征方法(抽象和具体)的抽象声明 一个包含所有 trait 的具体方法的静态方法的抽象静态类,带有一个额外的参数 $this(在旧版本的 Scala 中,这个类不是抽象的,但实例化它没有意义) 在继承层次结构中混入特征的每个点,特征中所有具体方法的合成转发器方法转发到静态类的静态方法

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

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

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

大概可以这样简化:

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

我不确定为什么没有这样做。乍一看,当前的编码可能会给我们一点前向兼容性:您可以使用用新编译器编译的特征和旧编译器编译的类,这些旧类将简单地覆盖它们从与相同的接口。除了,转发器方法将尝试调用不再存在的 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 ,如果不将代码从S 复制到B 并破坏前向兼容,您将如何实现class B extends A with T with S?静态方法引用相同的实现,忽略虚拟调度,这就是我们需要的。目前它们只是绕过 virt 调度的invokespecial 指令,但可能需要更改,这允许它。【参考方案3】:

一个很好的解释是:

The busy Java developer's guide to Scala: Of traits and behaviors - Traits in the JVM

引用:

在这种情况下,它 [编译器]将 trait 中定义的方法实现和字段声明放到实现 trait 的类中

【讨论】:

【参考方案4】:

在Scala 12和Java 8的上下文中,你可以在commit 8020cd6看到另一种解释:

对 2.12 特征编码的更好内联支持

特征编码的一些变化出现在 2.12 周期的后期,而 inliner 没有适应以最好的方式支持它。

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

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) 

如果选择一个 trait 方法进行内联,2.12.0 内联会 将其主体复制到静态超级访问器T.m$,然后从那里复制到 mixin 转发器C.m

这个提交是内联的特殊情况:

我们不会内联静态超级访问器和 mixin 转发器。 相反,当内联对 mixin 转发器的调用时,内联器还会通过两个转发器并内联 trait 方法体。

【讨论】:

以上是关于Scala 特征是如何编译成 Java 字节码的?的主要内容,如果未能解决你的问题,请参考以下文章

实现把C语言编译成java字节码的编译器 一个将C语言编译成java字节码的实例

jvm 字节码学习

《Java虚拟机精讲》读书笔记-第二章 字节码的编译原理

什么是字节码?采⽤字节码的好处是什么?

Class类文件结构

Scala学习-01-变量与类型