Содержание


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

Признаки и поведение объектов

Адаптация Java-интерфейсов для Scala

Comments

Серия контента:

Этот контент является частью # из серии # статей: Путеводитель по Scala для Java-разработчиков

Следите за выходом новых статей этой серии.

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

Следите за выходом новых статей этой серии.

Знаменитый ученый сэр Исаак Ньютон однажды сказал: "Если я видел дальше других, то потому, что стоял на плечах гигантов". Будучи увлеченным историком и политологом, я бы перефразировал Ньютона так: "Если я видел дальше других, то потому, что стоял на плечах истории". Этот вариант отражает другое высказывание, сделанное историком Джорджем Сантаяной: "Те, кто не помнит историю, обречены на ее повторение". Другими словами, если не оглядываться назад и не извлекать уроков из ошибок, сделанных нами и нашими предшественниками, то вряд ли можно надеяться на существенный прогресс в будущем.

Наверное, вы уже задаетесь вопросом, каким образом подобное философствование может относиться к Scala? В частности, это связано с понятием наследования. Не забывайте, что язык Java появился почти 20 лет назад в эпоху расцвета объектно-ориентированного программирования. В него были заложены многие характерные черты C++ – основного языка программирования того времени – с очевидной целью переманить разработчиков C++ на платформу Java. При проектировании языка использовались принципы, которые выглядели бесспорными 20 лет назад, однако сейчас можно утверждать, что они оказались не столь идеальными, как представлялось создателям Java.

Например, 20 лет назад разработчикам казалось логичным не включать в Java такие возможности C++ как закрытое и множественное наследование. С тех пор многие Java-разработчики успели пожалеть об этом решении. В этой части путеводителя по Scala мы обратимся к истории данных видов наследования в Java. Вы узнаете, каким образом Scala удается изменить историю в положительную для разработчиков сторону.

Наследование в C++ и в Java

История – это лишь признанная людьми версия произошедших событий.
Наполеон Бонапарт

Те из вас, кто прошел тернистый путь изучения C++, должны помнить, что закрытое наследование (private inheritance) – это способ наследования поведения базового класса без явного признания отношения "IS-A". Другими словами, если базовый класс помечен как "закрытый" (private), то дочерний класс может наследовать его методы, явно не становясь при этом его разновидностью. Данный вариант наследования сам по себе никогда не пользовался большой популярностью. Сама идея наследования, которое не поддерживает безопасное преобразование к базовому типу, казалась нелепой.

Множественное наследование, напротив, большинством расценивалось как неотъемлемая часть объектно-ориентированного программирования. Например, при моделировании современной иерархии средств передвижения логично объявить класс SeaPlane (самолет морского базирования) в качестве наследника классов Boat (корабль), который содержит методы startEngine() и sail(), а также Plane (самолет), предоставляющий методы startEngine() и fly(). В конце концов, поведение SeaPlane должно иметь черты как Boat, так и Plane, не так ли?

Именно так рассуждали разработчики в эпоху расцвета C++. Напротив, в мире Java принято считать, что множественное наследование обладает не меньшим числом недостатков, чем закрытое наследование. Любой Java-разработчик скажет вам, что SeaPlane должен наследовать интерфейсы Floatable и Flyable (плавающий и летающий объекты, соответственно), а также, вполне вероятно, и интерфейс EnginePowered (моторизованный объект). Наследование интерфейсов означает, что класс может реализовывать все необходимые методы, не опасаясь подводных камней виртуального множественного наследования, при котором приходится решать, какой именно базовый метод startEngine() следует вызывать внутри метода startEngine() класса SeaPlane.

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

Еще раз к вопросу о повторном использовании поведения классов

Все события... можно приблизительно разделить на те, которые, скорее всего, никогда не происходили и те, которые не имеют никакого значения.
Уильям Ральф Инг

В основе платформы Java лежит спецификация JavaBeans, определяющая правила создания объектов POJO, от которых зависит множество компонентов среды Java-приложений. Вы, разумеется, знаете, что свойства объектов в Java реализуются при помощи пар get- и set-методов, как показано в листинге 1.

Листинг 1. POJO-класс Person
//Это Java      
public class Person
{
    private String lastName;
    private String firstName;
    private int age;
    
    public Person(String fn, String ln, int a)
    {
        lastName = ln; firstName = fn; age = a;
    }
    
    public String getFirstName() { return firstName; }
    public void setFirstName(String v) { firstName = v; }
    public String getLastName() { return lastName; }
    public void setLastName(String v) { lastName = v; }
    public int getAge() { return age; }
    public void setAge(int v) { age = v; }
}

Этот принцип выглядит достаточно простым для понимания и реализации. Однако представьте себе, что необходимо добавить поддержку уведомлений, т.е. сторонние классы должны иметь возможность регистрации на получение уведомлений в виде обратных вызовов об изменениях свойств конкретного объекта POJO. В соответствии со спецификацией JavaBean для этого необходимо реализовать интерфейс PropertyChangeListener с единственным методом propertyChange(). Если вам также требуется добавить возможность запрета/разрешения изменения свойства со стороны объекта-слушателя (PropertyChangeListener), то POJO-объект должен дополнительно реализовывать интерфейс VetoableChangeListener, содержащий метод vetoableChange().

По крайней мере, в теории это работает именно так.

На самом деле, интерфейс PropertyChangeListener должен реализовываться всеми потенциальными объектами-слушателями, а объект, рассылающий уведомления (в данном примере это класс Person), должен предоставлять открытые методы, принимающие на вход экземпляры PropertyChangeListener, а также имя свойства, изменения которого интересуют слушателя. Таким образом, итоговый вариант класса Person, показанный в листинге 2, выглядит несколько более сложным.

Листинг 2. Вторая версия класса Person
//Это Java      
public class Person
{
    // Весь код остался без изменений за тем исключением, что 
    // в каждом методе приходится выполнять следующие действия:
    // public setFoo(T newValue)
    // {
    //     T oldValue = foo;
    //     foo = newValue;
    //     pcs.firePropertyChange("foo", oldValue, newValue);
    // }
    
    public void addPropertyChangeListener(PropertyChangeListener pcl)
    {
        // Сохранение ссылки на объект pcl
    }
    public void removePropertyChangeListener(PropertyChangeListener pcl)
    {
        // Найти и удалить ссылку на объект pcl
    }
}

Необходимость рассылки уведомлений объектам-слушателям означает, что POJO-класс Person должен хранить ссылки на все подобные объекты в какой-то коллекции (в данном случае, в ArrayList). Объекты должны инстанциироваться, добавляться и удаляться из коллекции, а поскольку эти операции не являются атомарными, то также необходим код для синхронизации доступа к коллекции.

Наконец, при изменении свойства должны быть разосланы уведомления всем заинтересованным слушателям. Обычно это делается путем перебора всего списка слушателей и вызова у каждого метода propertyChange(). При этом в данный метод необходимо передать экземпляр класса PropertyChangeEvent, описывающий измененное свойство и содержащий его старое и новое значения (см. контракт класса PropertyChangeEvent в спецификации JavaBean).

Таким образом, нет ничего удивительного в том, что очень немногие реальные классы POJO полностью поддерживают механизм уведомления об изменениях – это заставляет писать огромное количество кода, причем его приходится повторять в каждом классе POJO.

Работа, работа и еще раз работа... где же легкий путь?

Самое интересное заключается в том, что если бы Java перенял возможность закрытого наследования из C++, то это помогло бы облегчить ряд проблем, связанных со спецификацией JavaBean. Например, базовый класс POJO мог бы предоставлять методы add() и remove(), класс коллекции, а также метод "firePropertyChanged()", который рассылал бы слушателям уведомления об изменении свойства.

Разумеется, все это можно реализовать и в Java, но из-за того что Java не поддерживает закрытое наследование, классы POJO (например, Person) должны быть наследниками базового класса Bean и, как следствие, должно поддерживаться их преобразование к Bean. Это означает, что они не смогут наследовать никакой другой базовый класс. Для решения этой проблемы можно было бы применить множественное наследование, но это означало бы возврат к виртуальному наследованию – тому, чего мы однозначно стараемся избежать.

Описанная выше проблема достаточно хорошо известна, и в мире Java, как правило, решается при помощи вспомогательного класса, в данном случае PropertyChangeSupport, экземпляры которого создаются внутри POJO. Несмотря на то, что POJO по-прежнему должен предоставлять стандартный набор открытых методов, каждый из них будет перенаправлять вызовы к классу Support, который возьмет на себя всю черновую работу. В листинге 3 приведен класс Person, использующий экземпляр PropertyChangeSupport.

Листинг 3. Третий вариант класса Person
//Это Java      
import java.beans.*;

public class Person
{
    private String lastName;
    private String firstName;
    private int age;

    private PropertyChangeSupport propChgSupport =
        new PropertyChangeSupport(this);
    
    public Person(String fn, String ln, int a)
    {
        lastName = ln; firstName = fn; age = a;
    }
    
    public String getFirstName() { return firstName; }
    public void setFirstName(String newValue)
    {
        String old = firstName;
        firstName = newValue;
        propChgSupport.firePropertyChange("firstName", old, newValue);
    }
    
    public String getLastName() { return lastName; }
    public void setLastName(String newValue)
    {
        String old = lastName;
        lastName = newValue;
        propChgSupport.firePropertyChange("lastName", old, newValue);
    }
    
    public int getAge() { return age; }
    public void setAge(int newValue)
    {
        int old = age;
        age = newValue;
        propChgSupport.firePropertyChange("age", old, newValue);
    }

    public void addPropertyChangeListener(PropertyChangeListener pcl)
    {
        propChgSupport.addPropertyChangeListener(pcl);
    }
    public void removePropertyChangeListener(PropertyChangeListener pcl)
    {
        propChgSupport.removePropertyChangeListener(pcl);
    }
}

Не знаю как вы, но лично я, видя подобный код, практически испытываю желание вернуться обратно к языку ассемблера. Но самое неприятное заключается в том, что в точности этот код приходится повторять в каждом классе POJO. Половина строк кода в листинге 3 относится непосредственно к логике конкретного POJO, а потому не может быть использована повторно (разумеется, если не принимать во внимание печально знаменитый принцип "скопировать-и-вставить").

Далее мы обратимся к Scala и посмотрим, что этот язык может предложить для решения данной проблемы.

Признаки и повторное использование методов классов в Scala

Каждый человек обязан тщательно обдумывать характерные черты своего характера. Он также должен уметь управлять ими, при этом не удивляясь, если ему бы больше подошли черты характера кого-то еще.
Сисеро

Scala поддерживает дополнительную языковую конструкцию, которая занимает промежуточное место между интерфейсом и классом. Она получила название признака (trait). Необычной особенностью признаков является то, что они сочетают черты как интерфейсов (в том, что класс может иметь множество признаков), так и классов (в том, что они могут содержать код, определяющий поведение объекта). Кроме того, подобно классам и интерфейсам, признаки могут содержать собственные методы. Однако, в отличие от классов и интерфейсов, описание методов не проверяется до тех пор, пока признак не становится частью некоторого класса. Другими словами, можно определять методы, корректность которых не будет проверена, пока они не будут включены в конструкцию, приписывающую данный признак конкретному классу.

Признаки могут показаться сложной концепцией, но, увидев их в действии, вы не должны испытать проблем. В качестве первого примера рассмотрим Scala-версию POJO-класса Person (листинг 4).

Листинг 4. POJO-класс Person в Scala
//Это Scala
class Person(var firstName:String, var lastName:String, var age:Int)
{
}

Кроме того, Scala позволяет гарантировать наличие get- и set-методов, которые необходимы для работы с классом в среде Java. Для этого достаточно добавить аннотацию scala.reflect.BeanProperty к параметрам класса (firstName, lastName и age). На данный момент мы оставим эти методы за скобками в целях упрощения общей картины.

В листинге 5 показан модифицированный вариант класса Person, рассылающий уведомления объектам-слушателям (экземплярам PropertyChangeListener).

Листинг 5. Класс Person, рассылающий уведомления, в Scala
//Это Scala
object PCL
    extends java.beans.PropertyChangeListener
{
    override def propertyChange(pce:java.beans.PropertyChangeEvent):Unit =
    {
        System.out.println("Bean changed its " + pce.getPropertyName() +
            " from " + pce.getOldValue() +
            " to " + pce.getNewValue())
    }
}
object App
{
    def main(args:Array[String]):Unit =
    {
        val p = new Person("Jennifer", "Aloi", 28)

        p.addPropertyChangeListener(PCL)
        
        p.setFirstName("Jenni")
        p.setAge(29)
        
        System.out.println(p)
    }
}

Обратите внимание на то, что при помощи ключевого слова object нам удалось зарегистрировать статический метод в качестве получателя уведомлений. В Java подобное было бы невозможно без явного создания класса и его объекта-синглетона. Это еще одно подтверждение того, что при разработке Scala был учтен негативный опыт создания приложений на Java.

Далее необходимо добавить метод addPropertyChangeListener() в класс Person, а также вызывать метод propertyChange() каждого слушателя при изменении свойств объекта. В Scala для этого достаточно определить признак для Person, причем его можно будет использовать многократно для разных классов (листинг 6). В данном примере признак будет называться BoundPropertyBean, поскольку свойства, об изменении которых посылаются уведомления, в спецификации JavaBeans именуются связанными (bound properties).

Листинг 6. Да здравствует повторное использование функциональности!
//Это Scala
trait BoundPropertyBean
{
    import java.beans._

    val pcs = new PropertyChangeSupport(this)
    
    def addPropertyChangeListener(pcl : PropertyChangeListener) =
        pcs.addPropertyChangeListener(pcl)
    
    def removePropertyChangeListener(pcl : PropertyChangeListener) =
        pcs.removePropertyChangeListener(pcl)
    
    def firePropertyChange(name : String, oldVal : _, newVal : _) : Unit =
        pcs.firePropertyChange(new PropertyChangeEvent(this, name, oldVal, newVal))
}

В этом примере по-прежнему используется класс PropertyChangeSupport из пакета java.beans, поскольку, во-первых, он предоставляет примерно 60% необходимых возможностей, а во-вторых, это гарантирует, что поведение объектов Scala будет соответствовать поведению POJO-объектов Java, которые используют его напрямую. При этом все дальнейшие модификации данного вспомогательного класса будут автоматически доступны через признак BoundPropertyBean. Разница по сравнению с Java заключается в том, что POJO-класс Person более не обращается к классу PropertyChangeSupport напрямую (листинг 7).

Листинг 7. Вторая версия класса Person в Scala
//Это Scala
class Person(var firstName:String, var lastName:String, var age:Int)
    extends Object
    with BoundPropertyBean
{
    override def toString = "[Person: firstName=" + firstName +
        " lastName=" + lastName + " age=" + age + "]"
}

После компиляции вы увидите, что Scala-класс Person, аналогично своей Java-версии, содержит открытые методы addPropertyChangeListener(), removePropertyChangeListener() и firePropertyChange()). Однако в Scala эти методы были приписаны классу Person благодаря всего лишь одной строке кода, содержащей ключевое слово with. Его использование в объявлении Person означает, что он наследует признак BoundPropertyBean.

К сожалению, это еще не все. На данный момент класс Person предоставляет методы для добавления, удаления и уведомления слушателей, однако они пока не вызываются при изменении свойства firstName, поскольку его реализация была сгенерирована автоматически. Кроме того, на момент написания этой статьи в Scala не поддерживался никакой механизм для генерации методов доступа и изменения свойств, которые бы по умолчанию использовали экземпляр PropertyChangeSupport. Таким образом, придется вызывать его вручную, как показано в листинге 8.

Листинг 8. Третий вариант класса Person в Scala
//Это Scala
class Person(var firstName:String, var lastName:String, var age:Int)
    extends Object
    with BoundPropertyBean
{
    def setFirstName(newvalue:String) =
    {
        val oldvalue = firstName
        firstName = newvalue
        firePropertyChange("firstName", oldvalue, newvalue)
    }

    def setLastName(newvalue:String) =
    {
        val oldvalue = lastName
        lastName = newvalue
        firePropertyChange("lastName", oldvalue, newvalue)
    }

    def setAge(newvalue:Int) =
    {
        val oldvalue = age
        age = newvalue
        firePropertyChange("age", oldvalue, newvalue)
    }

    override def toString = "[Person: firstName=" + firstName +
        " lastName=" + lastName + " age=" + age + "]"
}

Пример полезного признака

Признаки лишь с большой натяжкой можно назвать элементом функционального , они, скорее всего, представляют собой результат десятилетнего анализа объектно-ориентированного подхода. Кстати говоря, возможно, вы уже используете признаки классов, не отдавая себе в этом отчета (пример подобной ситуации приведен в листинге 9).

Листинг 9. Скажите "до свидания" функции main()!
//Это Scala
object App extends Application
{
    val p = new Person("Jennifer", "Aloi", 29)

    p.addPropertyChangeListener(PCL)
    
    p.setFirstName("Jenni")
    p.setAge(30)
    
    System.out.println(p)
}

Признак Application определяет метод main() – тот самый, который приходилось вручную писать для запускаемых Java-классов. Кроме того, он обладает еще одной привлекательной функцией, а именно таймером, который засекает время выполнения приложения. Для этого достаточно установить системное свойство scala.time при запуске приложения. Пример показан в листинге 10.

Листинг 10. Главное - это следить за временем
$ scala -Dscala.time App
Bean changed its firstName from Jennifer to Jenni
Bean changed its age from 29 to 30
[Person: firstName=Jenni lastName=Aloi age=30]
[total 15ms]

Признаки в JVM

На определенном уровне развития любая технология становится неотличимой от магии.
Артур Ч. Кларк

Вполне вероятно, что вы уже задаете себе вопрос, как эти "волшебные" полу-интерфейсы, полуклассы, называемые признаками, работают внутри JVM. В этом может помочь разобраться наш старый знакомый javap. В листинге 11 показан результат дезассемблирования класса Person.

Листинг 11. Результат декомпиляции класса Person
$ javap -classpath C:\Prg\scala-2.7.0-final\lib\scala-library.jar;classes Person
Compiled from "Person.scala"
public class Person extends java.lang.Object implements BoundPropertyBean,scala.
ScalaObject{
    public Person(java.lang.String, java.lang.String, int);
    public java.lang.String toString();
    public void setAge(int);
    public void setLastName(java.lang.String);
    public void setFirstName(java.lang.String);
    public void age_$eq(int);
    public int age();
    public void lastName_$eq(java.lang.String);
    public java.lang.String lastName();
    public void firstName_$eq(java.lang.String);
    public java.lang.String firstName();
    public int $tag();
    public void firePropertyChange(java.lang.String, java.lang.Object, java.lang
.Object);
    public void removePropertyChangeListener(java.beans.PropertyChangeListener);

    public void addPropertyChangeListener(java.beans.PropertyChangeListener);
    public final void pcs_$eq(java.beans.PropertyChangeSupport);
    public final java.beans.PropertyChangeSupport pcs();
}

Обратите внимание на объявление POJO-класса Person. Он реализует интерфейс BoundPropertyBean, который представляет собой не что иное, как результат преобразования одноименного признака в процессе компиляции. Логично задать вопрос: а что в таком случае происходит с методами признака? Не забывайте, что компилятор имеет право выполнять любые преобразования кода, не изменяющие семантику программы в соответствии со спецификацией Scala. Поэтому в данном случае он просто переносит реализации всех методов и объявления полей признака в реализующий класс, т. е. в Person. Это становится особенно хорошо заметно, если запустить javap с ключом -private (хотя это и так следует из последних двух строк вывода декомпилятора, в которых происходит обращение к полю pcs, определенному в признаке), листинг 12.

Листинг 12. Результат декомпиляции класса Person с ключом -private
$ javap -private -classpath C:\Prg\scala-2.7.0-final\lib\scala-library.jar;classes Person
Compiled from "Person.scala"
public class Person extends java.lang.Object implements BoundPropertyBean,scala.
ScalaObject{
    private final java.beans.PropertyChangeSupport pcs;
    private int age;
    private java.lang.String lastName;
    private java.lang.String firstName;
    public Person(java.lang.String, java.lang.String, int);
    public java.lang.String toString();
    public void setAge(int);
    public void setLastName(java.lang.String);
    public void setFirstName(java.lang.String);
    public void age_$eq(int);
    public int age();
    public void lastName_$eq(java.lang.String);
    public java.lang.String lastName();
    public void firstName_$eq(java.lang.String);
    public java.lang.String firstName();
    public int $tag();
    public void firePropertyChange(java.lang.String, java.lang.Object, java.lang.Object);
    public void removePropertyChangeListener(java.beans.PropertyChangeListener);

    public void addPropertyChangeListener(java.beans.PropertyChangeListener);
    public final void pcs_$eq(java.beans.PropertyChangeSupport);
    public final java.beans.PropertyChangeSupport pcs();
}

Кстати говоря, этот пример также объясняет, почему проверку методов признака можно отложить до момента их использования. Методы признака, по сути, не являются членами никакого класса до тех пор, пока признак не будет реализован, поэтому компилятор до этого момента может их игнорировать. Это полезная особенность, так как позволяет обращаться к объекту super признака, даже не зная, какой класс будет базовым для реализации данного признака.

Немного об экземплярах признаков

Функциональность признака BoundPropertyBean используется, в частности, при создании экземпляра PropertyChangeSupport. Конструктор этого класса принимает на вход объект POJO, об изменении которого должны рассылаться уведомления. Признак передает "this" в качестве этого параметра. Однако объект признака не определен до того момента, пока он не будет реализован классом, поэтому значением "this" будет не BoundPropertyBean (тип признака), а Person. Данное отложенное определение типов является одной из особенностей признаков. Это достаточно тонкий момент, однако подобное "позднее связывание" можно рассматривать как мощную возможность языка.

В признаке Application все волшебство заключается в двух аспектах: в методе main(), представляющем собой точку входа в приложения Java, и в обращении к системному свойству -Dscala.time для определения того, нужно ли засекать продолжительность выполнения приложения. Поскольку Application является признаком, метод main() относится к дочернему классу App, поэтому для выполнения метода необходимо сначала создать синглетон App. Это, в свою очередь, означает, что сначала надо сформировать код этого класса. Сам метод признака вызывается и засекает время исполнения приложения только после того, как все эти действия выполнены.

Эта схема может показаться слегка неуклюжей, однако она работает, правда с тем ограничением, что у приложения не будет доступа к параметрам командной строки, переданным в метод main(). Пример также демонстрирует, как функциональность признака "делегируется" реализующему его классу.

Признаки и коллекции

Либо вы занимаетесь решением проблемы, либо являетесь ее частью.
Генри Дж. Тиллман

Главной привлекательной чертой признаков является то, что они сочетают в себе конкретный код реализации с абстрактными объявлениями. В этом кроется ряд преимуществ для разработчика. Например, рассмотрим классический пример: интерфейс List и класс ArrayList из стандартного набора коллекций в Java. Интерфейс List гарантирует, что итерация по списку будет производиться в том же порядке, в котором элементы были добавлены. Выражаясь более формально, итерирование обладает позиционной семантикой.

Одной из реализаций List является класс ArrayList, который хранит элементы в виде массива объектов, в то время как LinkedList использует для этого связанный список. ArrayList лучше приспособлен для произвольного доступа к списку, а LinkedList оказывается эффективнее при вставке/удалении элементов (за исключением удаления из конца списка). Тем не менее, в основном эти классы ведут себя совершенно одинаково, поэтому оба являются наследниками общего базового класса AbstractList.

Если бы признаки поддерживались в Java, то они идеально подошли бы для решения подобной проблемы, которую можно охарактеризовать как "необходимость реализовать возможность повторного использования функциональности без необходимости использования общего базового класса". Признаки могут выступать в качестве разновидности механизма закрытого наследования C++. С их помощью можно было бы избежать путаницы, связанной с вопросом, должен ли некоторый потомок List реализовывать интерфейс самостоятельно (при этом можно забыть реализовать интерфейс RandomAccess) или же лучше расширять базовый класс AbstractList. Подобную возможность иногда называют "примесями" (mixin) в C++, правда их нельзя путать с примесями в Ruby или в Scala, которые мы обсудим ниже.

Классическим примером, приводимым в документации по Scala, является признак Ordered, который определяет набор методов с забавными именами, служащих для сортировки и сравнения элементов (листинг 13).

Листинг 13. Всем построиться!
//Это Scala
trait Ordered[A] {
  def compare(that: A): Int
  
  def <  (that: A): Boolean = (this compare that) <  0
  def >  (that: A): Boolean = (this compare that) >  0
  def <= (that: A): Boolean = (this compare that) <= 0
  def >= (that: A): Boolean = (this compare that) >= 0
  def compareTo(that: A): Int = compare(that)
}

В этом примере признак Ordered, являющийся параметризованным типом (а-ля шаблон в Java 5), содержит единственный метод compare, который принимает на вход объект A и возвращает значение, меньшее 1, если А "меньше", чем данный экземпляр; значение, большее 1, если А "больше" данного экземпляра; или 0, если они равны. Кроме того, на основе метода compare() признак определяет операторы сравнения ("<", ">" и т.д.), а также метод compareTo(), знакомый по интерфейсу java.util.Comparable.

Scala and Java compatibility

Рисунок стоит тысячи слов, а интерфейс – тысячи рисунков.
Бен Шнайдерман

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

Как уже упоминалось в этой и предыдущих статьях серии, скомпилированный код Scala-приложения далеко не всегда соответствует правилам Java. В частности, в представлении приведенных выше методов "с забавными именами", таких как "+" или "\", используются символы, применение которых непосредственно в коде Java запрещено (например, серьезной проблемой является символ "$"). По этой причине создание интерфейса, следующего правилам Java, может упростить обращение к коду на Scala.

Данный конкретный пример выглядит немного искусственно, так как используемые в нем возможности Scala не требуют создания такого уровня абстракции, какой будет показан ниже (при условии, что мы не будем использовать методы с "забавными именами"). Тем не менее, он полезен в качестве иллюстрации принципов совместимости Java и Scala. Допустим, нам необходим стандартный Java-класс, представляющий собой фабрику для создания экземпляров класса Student (подобные фабрики присутствуют во множестве объектных моделей Java). Для этого мы сначала создадим совместимый с Java признак Student (листинг 14).

Листинг 14. Я - студент
//Это Scala
trait Student
{
    def getFirstName : String;
    def getLastName : String;
    def setFirstName(fn : String) : Unit;
    def setLastName(fn : String) : Unit;
    
    def teach(subject : String)
}

В результате компиляции признак превращается в POJI – простой интерфейс Java (Plain Old Java Interface). Вывод javap показан в листинге 15.

Листинг 15. Это POJI!
$ javap Student
Compiled from "Student.scala"
public interface Student extends scala.ScalaObject{
    public abstract void setLastName(java.lang.String);
    public abstract void setFirstName(java.lang.String);
    public abstract java.lang.String getLastName();
    public abstract java.lang.String getFirstName();
    public abstract void teach(java.lang.String);
}

Далее необходимо создать сам класс-фабрику. Обычно в Java фабрики реализуются в виде классов (StudentFactory) с единственным статическим методом, однако, как вы помните, в Scala нет статических методов. Вместо них используются объекты-синглетоны, имеющие обычные методы. Поскольку это именно то, что требуется в этом примере, мы создадим объект StudentFactory и определим в нем метод Factory (листинг 16).

Листинг 16. Создание экземпляров Student
//Это Scala
object StudentFactory
{
    class StudentImpl(var first:String, var last:String, var subject:String)
        extends Student
    {
        def getFirstName : String = first
        def setFirstName(fn: String) : Unit = first = fn
        def getLastName : String = last
        def setLastName(ln: String) : Unit = last = ln
        
        def teach(subject : String) =
            System.out.println("I know " + subject)
    }

    def getStudent(firstName: String, lastName: String) : Student =
    {
        new StudentImpl(firstName, lastName, "Scala")
    }
}

Вложенный класс StudentImpl реализует признак Student, предоставляя требуемые get- и set-методы. Несмотря на то что признаки могут самостоятельно реализовывать методы, эту возможность нельзя использовать при их представлении в JVM в виде интерфейсов, так как инстанциирование Student будет расценено как попытка создания экземпляра абстрактного типа.

Венцом этого примера, разумеется, должно стать Java-приложение, создающее экземпляры данного типа, определенного в Scala.

Листинг 17. Студент Neo
//Это Java
public class App
{
    public static void main(String[] args)
    {
        Student s = StudentFactory.getStudent("Neo", "Anderson");
        s.teach("Kung fu");
    }
}

Запустите приложение, показанное в листинге 17, и вы увидите результат – строчку " I know Kung fu" (выглядит немного разочаровывающе после такой кропотливой подготовки, не спорю).

Заключение

Люди предпочитают не думать, так как это заставляет их делать выводы, а последние не всегда бывают приятными.
Хелен Келлер

Признаки – это мощный механизм для классификации и определения типов в Scala. Они полезны как для определения клиентских интерфейсов, аналогичных интерфейсам в Java, так и в качестве способа наследования функциональности, содержащейся в признаке. Пожалуй, для корректного описания отношения между признаком и реализующими его классами необходимо новое определение, например, IN-TERMS-OF (в-соответствии-с).

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

Пока это все, любители функционального программирования, до следующего раза!


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи: Путеводитель по Scala для Java-разработчиков: О признаках и манерах. (EN)
  • Слушайте Web-трансляцию Scala revealed (JavaWorld, июнь 2008), в которой авторы developerWorks Эндрю Гловер и Тед Ньюард обсуждают различия между объектно-ориентированными и функциональными языками программирования, а также приводят примеры задач, например, связанных с параллельной обработкой или работой с базами данных, для которых Java и другие "чистые" ОО-языки не являются наилучшим выбором. (EN)
  • Узнайте больше о наследовании и композиции классов на основе примесей, прочитав главу Путеводитель по Scala: Композиция классов на основе примесей" в документации по Scala. (EN)
  • Прочитайте отрывок из книги "Философия Java" под названием Сравнение C++ и Java (Брюс Эккель, Java Coffee Break), в котором подчеркиваются различия между языками C++ и Java. (EN)
  • Ознакомьтесь с заметкой в блоге под названием "Даже динозаврам приходиться напрягаться" (Кэй Хорстманн, Java.net, январь 2008 г.), в которой утверждается, что Java находится в эволюционном тупике, и предлагаются некоторые варианты выхода из него. (EN)
  • Прочитайте обсуждение в форуме Artima под заголовком В чем на самом деле заключаются трудности при работе с Java?" (Билл Веннерс, Artima.com, февраль 2007 г.). По данной теме на момент написания этой статьи было уже 264 ответа. (EN)
  • Размышления Теда Ньюарда на тему проектирования и эволюции языков можно прочитать в заметке Совместимость языков возможна. (EN)
  • Прочитайте статью "Функциональное программирование на Java" (Абхиджит Белапуркар, developerWorks, июль 2004 г.), в которой обсуждаются преимущества и возможности применения функционального программирования с точки зрения разработчика Java. (EN)
  • Ознакомьтесь со статьей "Scala в примерах" (Мартин Одерски, декабрь 2007 г.), в которой приводится короткое введение в Scala, изобилующее примерами. В том числе в ней описывается приложение Quicksort, также использующееся в нашей серии (формат PDF). (EN)
  • Прочитайте Программирование на Scala (Мартин Одерски, Лекс Спун, и Билл Веннерс; сигнальный экземпляр опубликован Artima в феврале 2008 г.) - первое подробное введение в Scala, написанное в соавторстве с Биллом Веннерсом. (EN)
  • Загрузите Scala. На настоящий момент последней является версия 2.7.0-final. (EN)
  • Сотни статей по всем аспектам программирования на Java можно найти на сайте developerWorks, в разделе Технология Java.

Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=389667
ArticleTitle=Путеводитель по Scala для Java-разработчиков: Признаки и поведение объектов
publish-date=05152009