Черты в 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 и их реализацию. В следующей статье мы рассмотрим, как работают миксины и составные модификации.