Черты в Scala похожи на интерфейсы, но гораздо более мощные. Они допускают реализации некоторых методов, полей, стеков и т. Д. Но вы когда-нибудь задумывались, как они реализованы поверх JVM? Как можно расширить несколько черт и куда идут реализации? В этой статье, основываясь на моем ответе на StackOverflow , я приведу несколько примеров характеристик и объясню, как scalacих реализовать, каковы недостатки и что вы получаете бесплатно. Часто мы будем смотреть на скомпилированные классы и декомпилировать их в Java. Это не обязательное знание, но я надеюсь, что оно вам понравится. Все примеры скомпилированы для Scala 2.10.1.
Простая черта без реализации методов
Следующая черта:
trait Solver {
def answer(question: String): Int
}
компилируется до самого скучного интерфейса Java:
public interface Solver {
int answer(String);
}
В чертах Scala нет ничего особенного. Это также означает, что если вы отправляете
Solver.classкак часть своей библиотеки, пользователи могут безопасно реализовать такой интерфейс из кода Java. Что касается
java/
javac, то это обычный Java-скомпилированный интерфейс.
Черты, имеющие некоторые реализованные методы
Хорошо, но что, если у черты на самом деле есть несколько тел методов?
trait Solver {
def answer(s: String): Int
def ultimateAnswer = answer("Answer to the Ultimate Question of Life, the Universe, and Everything")
}
Здесь
ultimateAnswerметод на самом деле имеет некоторую реализацию (тот факт, что он вызывает абстрактный
answer()метод не имеет значения), но
answer()остается нереализованным. Что будет
scalacпроизводить?
public interface Solver {
int answer(java.lang.String);
int ultimateAnswer();
}
Ну, это все еще интерфейс, и реализация ушла. Если реализация ушла, что произойдет, когда мы расширим такую черту?
class DummySolver extends Solver {
override def answer(s: String) = 42
}
Мы должны реализовать,
answer()но
ultimateAnswerуже доступны через
Solver. Страшно посмотреть, как это выглядит под капотом?
DummySolver.class:
public class DummySolver implements Solver {
public DummySolver() {
Solver$class.$init$(this);
}
public int ultimateAnswer() {
return Solver$class.ultimateAnswer(this);
}
public int answer(String s) {
return 42;
}
}
Это … странно … Видимо, мы пропустили один новый файл класса
Solver$class.class. Да, класс назван
Solver$class, это действительно даже в Java. Это
не
Solver.class выражение, которое возвращает
Class[Solver], это, очевидно, обычный Java-класс, названный
Solver$classс помощью множества статических методов. Вот как это выглядит:
public abstract class Solver$class {
public static int ultimateAnswer(Solver $this) {
return $this.answer("Answer to the Ultimate Question of Life, the Universe, and Everything");
}
public static void $init$(Solver solver) {}
}
Ты видишь трюк? Scala создал помощника,
Solver$classи методы, которые реализованы в trait, фактически помещаются в этот скрытый класс. Кстати, это не объект-компаньон, это просто вспомогательный класс, невидимый для вас. Но настоящий трюк — это
$thisпараметр, вызываемый как
Solver$class.ultimateAnswer(this). Поскольку технически мы находимся в другом объекте, мы должны каким-то образом получить дескриптор
реального
Solver экземпляра. Это как ООП, но сделано вручную.
Итак, мы узнали, что реализации методов из признаков извлекаются в специальный вспомогательный класс. На этот класс ссылаются каждый раз, когда мы вызываем такой метод. Таким образом, мы не копируем тело метода снова и снова в каждый отдельный класс, расширяющий данную черту.
Расширение нескольких черт с реализациями
Представьте себе расширение нескольких признаков, каждый из которых реализует отдельный набор методов:
trait Foo {
def foo = "Foo"
}
trait Bar {
def bar = "Bar"
}
class Buzz extends Foo with Bar
По аналогии вы, наверное, уже знаете, как
Foo.classи
Bar.classвыглядите так:
public interface Foo {
String foo();
}
public interface Bar {
String bar();
}
Реализации скрыты в таинственных
...$class.classфайлах, как и прежде:
public abstract class Foo$class {
public static String foo(Foo) {
return "Foo";
}
public static void $init$(Foo) {}
}
public abstract class Bar$class {
public static String bar(Bar) {
return "Bar";
}
public static void $init$(Bar) {}
}
Обратите внимание , что статические методы в
Foo$classпринимать экземпляры
в Fooто время как
Bar$classтребуется ссылка
Bar. Это работает, потому что
Buzzреализует
Fooи
Bar:
public class Buzz implements Foo, Bar {
public Buzz() {
Foo.class.$init$(this);
Bar.class.$init$(this);
}
public String bar() {
return Bar$class.bar(this);
}
public String foo() {
return Foo$class.foo(this);
}
}
И то, foo()и
другое
bar()передают
this, но это нормально.
Черты с полями
Поля — еще одна интересная особенность черт. На этот раз давайте воспользуемся реальным примером из
проекта Spring Data . Как вы знаете, интерфейсы могут требовать реализации определенных методов. Однако они не могут заставить вас предоставить определенные поля (или предоставить поля самостоятельно). Это ограничение становится болезненным при работе с
Auditable<U, ID>расширением интерфейса
Persistable<ID>. Хотя эти интерфейсы просто существуют , чтобы убедиться , что некоторые поля присутствуют в вашем
@Entityклассе, а именно
createdBy,
createdDate,
lastModifiedBy,
lastModifiedDateи
idэто не может быть выражено чисто. Вместо этого вы должны реализовать следующие методы в каждом расширении сущности
Auditable:
U getCreatedBy();
void setCreatedBy(final U createdBy);
DateTime getCreatedDate();
void setCreatedDate(final DateTime creationDate);
U getLastModifiedBy();
void setLastModifiedBy(final U lastModifiedBy);
DateTime getLastModifiedDate();
void setLastModifiedDate(final DateTime lastModifiedDate);
ID getId();
boolean isNew();
Более того, каждый отдельный класс, конечно, должен определять поля, выделенные выше. Не чувствует
СУХОЙ вообще. К счастью, черты могут нам сильно помочь
. В признаке мы можем определить, какие поля должны быть созданы в каждом классе, расширяющем эту особенность:
trait IAmAuditable[ID <: java.io.Serializable] extends Auditable[User, ID] {
var createdBy: User = _
def getCreatedBy = createdBy
def setCreatedBy(createdBy: User) {
this.createdBy = createdBy
}
var createdDate: DateTime = _
def getCreatedDate = createdDate
def setCreatedDate(creationDate: DateTime) {
this.createdDate = creationDate
}
var lastModifiedBy: User = _
def getLastModifiedBy = lastModifiedBy
def setLastModifiedBy(lastModifiedBy: User) {
this.lastModifiedBy = lastModifiedBy
}
var lastModifiedDate: DateTime = _
def getLastModifiedDate = lastModifiedDate
def setLastModifiedDate(lastModifiedDate: DateTime) {
this.lastModifiedDate = lastModifiedDate
}
var id: ID = _
def getId = id
def isNew = id == null
}
Но подождите, в Scala есть встроенная поддержка геттеров / сеттеров в стиле POJO! Таким образом, мы можем сократить это до:
class IAmAuditable[ID <: java.io.Serializable] extends Auditable[User, ID] {
@BeanProperty var createdBy: User = _
@BeanProperty var createdDate: DateTime = _
@BeanProperty var lastModifiedBy: User = _
@BeanProperty var lastModifiedDate: DateTime = _
@BeanProperty var id: ID = _
def isNew = id == null
}
Генерируемые компилятором методы получения и установки автоматически реализуют интерфейс. Отныне каждый класс, желающий предоставить возможности аудита, может расширить эту черту:
@Entity
class Person extends IAmAuditable[String] {
//...
}
Все поля, геттеры и сеттеры есть. Но как это реализовано? Давайте посмотрим на сгенерированный
Person.class:
public class Person implements IAmAuditable<java.lang.String> {
private User createdBy;
private DateTime createdDate;
private User lastModifiedBy;
private DateTime lastModifiedDate;
private java.io.Serializable id;
public User createdBy() //...
public void createdBy_$eq(User) //...
public User getCreatedBy() //...
public void setCreatedBy(User) //...
public DateTime createdDate() //...
public void createdDate_$eq(DateTime) //...
public DateTime getCreatedDate() //...
public void setCreatedDate(DateTime) //...
public boolean isNew();
//...
}
На самом деле есть гораздо больше, но вы поняли идею. Таким образом, поля не реорганизованы в отдельный класс, но этого следовало ожидать. Вместо этого поля копируются в каждый отдельный класс, расширяющий эту особенность. В Java мы могли бы использовать абстрактный базовый класс для этого, но было бы неплохо зарезервировать наследование для реальных отношений
is-a и не использовать его для держателей фиктивных полей. В Scala есть такой признак, который объединяет общие поля, которые мы можем использовать в нескольких классах.
А как насчет совместимости Java? Ну,
IAmAuditableкомпилируется до интерфейса Java, поэтому у него вообще нет полей. Если вы реализуете это из Java, вы ничего особенного не получите.
Мы рассмотрели основные варианты использования черт в Scala и их реализацию. В следующей статье мы рассмотрим, как работают миксины и составные модификации.