Как черты Scala компилируются в байт-код Java? - PullRequest
45 голосов
/ 01 апреля 2010

Я некоторое время играл со Scala, и я знаю, что черты могут действовать как эквивалент Scala как интерфейсов, так и абстрактных классов. Как именно черты компилируются в байт-код Java?

Я нашел несколько коротких объяснений того, что указанные характеристики скомпилированы, когда это возможно, точно так же, как интерфейсы Java, и в противном случае взаимодействуют с дополнительным классом. Однако я до сих пор не понимаю, как в Scala достигается линеаризация классов - функция, недоступная в Java.

Есть ли хороший источник, объясняющий, как черты компилируются в байт-код Java?

Ответы [ 4 ]

66 голосов
/ 01 апреля 2010

Я не эксперт, но вот мое понимание:

Черты скомпилированы в интерфейс и соответствующий класс.

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

становится эквивалентом ...

public interface Foo {
  public void bar();
}

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

Что оставляет вопрос: как вызывается метод static bar в классе Foo $? Эта магия выполняется компилятором в классе, в который смешана черта Foo.

class Baz extends Foo

становится чем-то вроде ...

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

Линеаризация класса просто реализует соответствующую версию метода (вызывая статический метод в классе класса Xxxx $) в соответствии с правилами линеаризации, определенными в спецификации языка.

5 голосов
/ 08 сентября 2016

Для обсуждения давайте рассмотрим следующий пример 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, содержащие абстрактные объявления для всех методов черты (как абстрактных, так и конкретных)
  • абстрактный статический класс, содержащий статические методы для всех конкретных методов черты, с дополнительным параметром $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 ???; }
}

Как видите, старый дизайн со статическими методами и средствами пересылки был сохранен, они просто свернуты в интерфейс. Конкретные методы этой черты теперь перенесены в сам интерфейс как методы 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$class и B$class, которые больше не существуют, так что гипотетическая прямая совместимость фактически не работает.

1 голос
/ 26 декабря 2016

В контексте Scala 12 и Java 8 вы можете увидеть другое объяснение в commit 8020cd6 :

Лучшая поддержка inliner для кодирования черты 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$ и оттуда в экспедитор mixin C.m.

Это коммит особых случаев инлайнера:

  • Мы не подключаемся к статическим супер-аксессорам и микси-форвардерам.
  • Вместо этого, при вставке вызова экспедитора mixin, инлайнер также следует через два форвардера и указывает тело метода черты.
1 голос
/ 08 октября 2012

Очень хорошее объяснение этого в:

Путеводитель по Scala для Java-разработчиков: черты и поведение - черты в JVM

Цитата:

В этом случае он [компилятор] сбрасывает реализации метода и объявления полей, определенные в признаке, в класс, который реализует признак

...