Как черты Scala компилируются в байт-код Java?
Я поэкспериментировал со Scala некоторое время и знаю, что черты могут действовать как эквивалент Scala как интерфейсов, так и абстрактных классов. Как именно черты компилируются в байт-код Java?
Я нашел несколько коротких объяснений, что заявленные черты, когда это возможно, компилируются точно так же, как интерфейсы Java, а в противном случае - с дополнительным классом. Однако я до сих пор не понимаю, как в Scala достигается линеаризация классов - функция, недоступная в Java.
Есть хороший источник, объясняющий, как черты компилируются в байт-код Java?
4 ответа
Я не эксперт, но вот мое понимание:
Черты скомпилированы в интерфейс и соответствующий класс.
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$) в соответствии с правилами линеаризации, определенными в спецификации языка.
Для обсуждения давайте рассмотрим следующий пример 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
которые больше не существуют, так что гипотетическая прямая совместимость на самом деле не работает.
Очень хорошее объяснение этого в:
Путеводитель по Scala для Java-разработчиков: Особенности и поведение - Особенности в JVM
Цитата:
В этом случае он [компилятор] сбрасывает реализации метода и объявления полей, определенные в признаке, в класс, который реализует признак
В контексте Scala 12 и Java 8 вы можете увидеть другое объяснение в коммите 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) }
}
Если метод init выбран для встраивания, вкладчик 2.12.0 скопирует свое тело в статический супер-аксессор
T.m$
и оттуда в экспедитор миксинC.m
,Это совершают особые случаи инлайнера:
- Мы не встраиваемся в статические супер-аксессоры и миксины.
- Вместо этого, при вставке вызова экспедитора mixin, инлайнер также следует через два форвардера и указывает тело метода trait.