Содержание


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

Наследование функциональности

Наследование в Scala: объекты и функции

Comments

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

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

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

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

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

В течение последних 20 лет (по крайней мере, лучшей их части) центральным звеном объектно-ориентированного дизайна было понятие наследования. Языки программирования, не поддерживающие наследования, например Visual Basic, всячески высмеивались и признавались негодными для серьезных проектов. В то же время остальные языки реализуют механизм наследования настолько по-разному, что это зачастую становилось причиной продолжительных дебатов. Предметом одного из таких споров было множественное наследование. Одни, например создатели С++, считали его необходимым, в то время как другие, в частности создатели С# и Java, были убеждены в его неоправданности. Недавно появились такие языки как Ruby и Scala, которые занимают промежуточное положение по этому вопросу. Частично эти моменты обсуждались в предыдущей статье при рассмотрении признаков (traits) в Scala (см. Ресурсы).

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

Простые объекты в Scala

Как и в предыдущих статьях, для изучения механизма наследования в Scala будет использоваться класс Person. Код класса приведен в листинге 1.

Листинг 1. Привет, я - экземпляр класса Person
// Это Scala
class Person(val firstName:String, val lastName:String, val age:Int)
{
  def toString = "[Person: firstName="+firstName+" lastName="+lastName+
                         " age="+age+"]"
}

Person представляет собой пример простого класса Scala (plain old scala object), содержащего поля, доступные для чтения. Как вы помните, для того чтобы добавить возможность изменения свойств, достаточно поменять ключевые слова val на var в объявлении главного конструктора класса.

Использование класса Person также не таит в себе никаких подводных камней (листинг 2).

Листинг 2. PersonApp
// Это Scala
object PersonApp
{
  def main(args : Array[String]) : Unit =
  {
    val bindi = new Person("Tabinda", "Khan", 38)
    System.out.println(bindi)
  }
}

Пока это не более чем базовый пример использования классов Scala – в нем нет ничего особенного увлекательного.

Абстрактные методы в Scala

Вскоре становится очевидно, что класс Person не предоставляет никаких средств для выражения одного из главных аспектов, характеризующих людей – их деятельности. Большинство людей склоняются к тому, что человек предназначен для чего-то большего, чем просто существование в виде объекта, занимающего некоторую область в пространстве. Поэтому имеет смысл добавить в класс Person метод, служащий для наполнения жизни объекта смыслом (листинг 3).

Листинг 3. Пора заняться делом
// Это Scala
class Person(val firstName:String, val lastName:String, val age:Int)
{
  override def toString = "[Person: firstName="+firstName+" lastName="+lastName+
                          " age="+age+"]"

  def doSomething = // хм... а что делать-то?
}

Сразу же возникает вопрос: а что собственно делают люди? Некоторые рисуют, другие поют, третьи программируют, четвертые играют в видео-игры, а кто-то и вовсе бездельничает (если не верите – спросите кого-нибудь из тинэйджеров). Поэтому имеет смысл создать классы-наследники Person вместо того, чтобы пытаться реализовать все эти действия в самом классе Person. Пример наследника приведен в листинге 4.

Листинг 4. Этот объект не сильно занят
// Это Scala
class Person(val firstName:String, val lastName:String, val age:Int)
{
  override def toString = "[Person: firstName="+firstName+" lastName="+lastName+
                          " age="+age+"]"

  def doSomething = // хм... а что делать-то?
}

class Student(firstName:String, lastName:String, age:Int)
  extends Person(firstName, lastName, age)
{
  def doSomething =
  {
    System.out.println("Я усердно занимаюсь, мам, клянусь! (ребята, передайте пива!)")
  }
}

Однако при компилировании этого кода возникает ошибка, обусловленная тем, что метод Person.doSomething не закончен. Он должен быть либо реализован (в этом случае логично генерировать исключения, говорящие о необходимости перегрузки метода в классе-наследнике), либо объявлен как абстрактный, т. е. метод без кода, подобному абстрактным методам в Java. Мы выберем второй вариант (листинг 5).

Листинг 5. Абстрактный класс Person
// Это Scala
abstract class Person(val firstName:String, val lastName:String, val age:Int)
{
  override def toString = "[Person: firstName="+firstName+" lastName="+lastName+
                          " age="+age+"]"

  def doSomething; // Обратите внимание на точку с запятой
                   // Она необязательна, но я лично предпочитаю
                   // ее ставить по стилистическим соображениям
}

class Student(firstName:String, lastName:String, age:Int)
  extends Person(firstName, lastName, age)
{
  def doSomething =
  {
    System.out.println("Я усердно занимаюсь, мам, клянусь! (ребята, передайте пива!)")
  }
}

Ключевое слово abstract явно указывает компилятору на то, что этот класс является абстрактным. Поведение компилятора при этом аналогично компилятору Java.

Появление функций

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

Как вы помните из предыдущих статей, в Scala функции рассматриваются аналогично обычным значениям, например значениям базовых типов Int, Float и Double. Это свойство языка можно использовать при моделировании типа Person, указав, что doSomething является не перегружаемым методом, а функциональным значением (function value), которое может быть вызвано, заменено, либо расширено. Пример подобного подхода показан в листинге 6.

Листинг 6. Работяга
// Это Scala    
class Person(val firstName:String, val lastName:String, val age:Int)
{
  var doSomething : (Person) => Unit = 
    (p:Person) => System.out.println("Я - " + p + " и мне пока нечем заняться!");
    
  def work() =
    doSomething(this)
    
  override def toString = "[Person: firstName="+firstName+" lastName="+lastName+
                          " age="+age+"]"
}

object App
{
  def main(args : Array[String]) =
  {
    val bindi = new Person("Tabinda", "Khan", 38)
    System.out.println(bindi)
    
    bindi.work()
    
    bindi.doSomething =
      (p:Person) => System.out.println("Я редактирую учебники")
      
    bindi.work()
    
    bindi.doSomething =
      (p:Person) => System.out.println("Я читаю HTML-книги")
      
    bindi.work()
  }
}

Использование функций в качестве базовых средств проектирования приложения широко применяется в динамических языках, в частности, Ruby, Groovy, ECMAScript (более известном под именем JavaScript), а также во многих функциональных языках. Подобное возможно и в других языках, например в C++ для этого есть указатели на функции и на члены классов, а в Java – реализации интерфейсов через анонимные внутренние классы. Однако, как правило, это требует значительно больше усилий, чем в Scala (а также, в Ruby, Groovy и ECMAScript). Во многом это определяется тем, что последние поддерживают расширения понятия "функций высшего порядка", хорошо известного в контексте функционального программирования (ссылки на источники дополнительной информации по функциям высшего порядка приведены в разделе Ресурсы).

Эквивалентность функций и значений в Scala позволяет использовать функции везде, где может потребоваться изменение функциональности на этапе выполнения приложения. Это подход имеет много общего с паттерном Роль – вариацией паттерна проектирования, рассмотренного в знаменитой книге Банды Четырех (Gang of Four). В соответствии с этим принципом роли объектов, такие как статус занятости экземпляра Person, следует представлять в виде динамически изменяемых значений, а не фиксировать статически в иерархии типов.

Доступ к конструктору базового класса

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

Параметры основного конструктора в Scala описываются непосредственно в объявлении class. При этом можно также использовать ключевое слово val для создания методов чтения (аксессоров), а var – для методов модификации параметров.

Декомпилировав класс Person из листинга 5 при помощи javap, можно увидеть, как он преобразуется в класс Java (листинг 7).

Листинг 7. Результат декомпиляции класса Person
// Результат работы javap
C:\Projects\scala-inheritance\code>javap -classpath classes Person
Compiled from "person.scala"
public abstract class Person extends java.lang.Object implements scala.ScalaObje
ct{
    public Person(java.lang.String, java.lang.String, int);
    public java.lang.String toString();
    public abstract void doSomething();
    public int age();
    public java.lang.String lastName();
    public java.lang.String firstName();
    public int $tag();
}

Основные правила, по которым работает JVM, по-прежнему остаются в силе: классы-наследники Person все так же передают параметры конструктору базового класса, вне зависимости от синтаксиса языка (бывают исключения, но для большинства языков это верно, так как JVM не нравится, когда языки не следуют данному правилу). Разумеется, Scala удовлетворяет этому принципу, так как, с точки зрения Scala, важна полная совместимость с JVM и базовыми классами, написанными на Java. Таким образом, в Scala необходимы синтаксические конструкции, при помощи которых можно было бы вызывать конструктор базового класса из наследников. В то же время они не должны идти вразрез с возможностями добавления методов доступа и модификации полей базового класса.

В качестве конкретного примера рассмотрим класс Student из листинга 5, изменив его, как показано в листинге 8.

Листинг 8. Плохой студент!
// Это Scala
// Этот класс не скомпилируется
class Student(val firstName:String, val lastName:String, val age:Int)
  extends Person(firstName, lastName, age)
{
  def doSomething =
  {
    System.out.println("Я усердно занимаюсь, мам, клянусь! (ребята, передайте пива!)")
  }
}

Разумеется, компилятору это совсем не понравится, так как мы добавили в класс Student множество методов (firstName, lastName и age), которые конфликтуют с одноименными методами в базовом классе Person. При этом компилятор не знает, пытаемся ли мы перегрузить методы базового класса или добавить новые (в любом случае это было бы неудачным решением, так как новые методы были бы скрыты за старыми). Вскоре будет показано, как можно перегружать базовые методы, но в данном примере нас интересует нечто другое.

Необходимо отметить, что в Scala параметры конструктора класса Person отнюдь не обязаны в точности соответствовать параметрам конструктора Student (в примерах это сделано исключительно в целях упрощения). Соответствие параметров регулируется теми же правилами, что и в Java. Кроме того, как и в Java, конструктору класса-наследника могут потребоваться дополнительные параметры (листинг 9).

Листинг 9. Требовательный студент!
// Это Scala
class Student(firstName:String, lastName:String, age:Int, val subject:String)
  extends Person(firstName, lastName, age)
{
  def doSomething =
  {
    System.out.println("Я усердно занимаюсь, мам, клянусь! (ребята, передайте пива!)")
  }
}

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

Синтаксические различия

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

Листинг 10. Кто я?
// Это Scala
abstract class Person(val firstName:String, val lastName:String, val age:Int)
{
  def doSomething
  
  def weight : Int
    
  override def toString = "[Person: firstName="+firstName+" lastName="+lastName+
                          " age="+age+"]"
}

class Student(firstName:String, lastName:String, age:Int, val subject:String)
  extends Person(firstName, lastName, age)
{
  def weight : Int =
    age // студенты очень тощие

  def doSomething =
  {
    System.out.println("Я усердно занимаюсь, мам, клянусь! (ребята, передайте пива!)")
  }
}

class Employee(firstName:String, lastName:String, age:Int)
  extends Person(firstName, lastName, age)
{
  val weight : Int = age * 4 // Сотрудники весьма упитанные!

  def doSomething =
  {
    System.out.println("Я очень занят, дорогая, клянусь! (ребята, передайте пива!)")
  }
}

В данном примере weight – это метод без параметров, всегда возвращающий значение типа Int. Поскольку он выглядит очень похоже на методы, реализующие свойства в Java, то в Scala его можно определять двумя способами: как метод (см. класс Student), либо как поле с аксессором (см. класс Employee). Благодаря этой синтаксической особенности языка обеспечивается определенная гибкость при реализации наследников абстрактного класса. В Java подобная гибкость возможна только при условии, что доступ к каждому полю осуществляется через его get- и set-методы. Плохо это или хорошо, но немногие Java-разработчики следуют этому принципу, поэтому данная возможность используется нечасто. Кроме того, в Scala это подход также работает для скрытых/приватных членов класса.

От @Override к override

Очень часто в классе-наследнике требуется изменить поведение метода, определенного в базовом классе. В Java для этого достаточно добавить метод с тем же именем и сигнатурой. У этого подхода есть один недостаток: в случае опечатки или определенной неоднозначности в сигнатуре метода код по-прежнему будет компилироваться, однако его поведение будет некорректным.

Для решения этой проблемы в Java 5 используется аннотация @Override. Встретив эту аннотацию, компилятор проверяет, что метод, определенный в классе-наследнике, на самом деле перегружает метод базового класса. В Scala ключевое слово override является обязательной частью синтаксиса, так что его отсутствие приведет к ошибке на этапе компиляции. Например, унаследованный метод toString() должен выглядеть, как показано в листинге 11.

Листинг 11. Пример унаследованного метода
// Это Scala
class Student(firstName:String, lastName:String, age:Int, val subject:String)
  extends Person(firstName, lastName, age)
{
  def weight : Int =
    age // Все студенты тощие

  def doSomething =
  {
    System.out.println("Я усердно занимаюсь, мам, клянусь! (ребята, передайте пива!)")
  }
  
  override def toString = "[Student: firstName="+firstName+
                          " lastName="+lastName+" age="+age+
                          " subject="+subject+"]"
}

Как видите, все достаточно очевидно.

Ключевое слово final

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

Следует помнить, что все, что было сказано выше о модификаторах abstract, final и override, относится в равной степени как к обычным методам, так и к методам с "забавными именами" (тем, которые программисты Java, C++ или C# назвали бы операторами). Таким образом, имеет смысл создать базовый класс или признак, определяющий некоторую математическую функциональность. Этот класс (назовем его, например, Mathable) будет определять четыре абстрактных функции "+", "-", "*" и "/", а также другие необходимые операции, такие как pow или abs. После этого другие разработчики могут создавать дополнительные типы, например, класс Matrix, реализуя или расширяя тип Mathable. Эти дочерние типы могут перегружать данные математические методы и внешне выглядеть так же, как типы данных, для которых арифметические операции поддерживаются непосредственно на уровне языка.

В чем же различие?

Если Scala поддерживает такой высокий уровень совместимости с моделью наследования в Java, как было показано в примерах выше, то должна также поддерживаться возможность наследования Scala-классов от Java-классов и наоборот. На самом деле это совершенно необходимо, так как объекты в Scala (как и в любом языке, компилирующемся в байт-код JVM) должны быть наследниками java.lang.Object. Разумеется, поскольку классы в Scala могут быть наследниками не только классов, а еще, например, признаков, то сам механизм наследования и генерации байт-кода может быть реализован по-другому. Тем не менее, так или иначе, должна быть возможность расширять Java-классы в Scala. Как вы помните, признаки в Scala являются некоторой разновидностью интерфейсов, обладающих функциональностью, поэтому при трансляции компилятор разделяет каждый признак на интерфейс и реализацию методов, которая помещается в классы-наследники признака.

На самом деле, иерархия типов в Scala несколько отличается от иерархии Java. Формально базовым типом, который расширяет все классы Scala, в том числе численные типы, такие как Int, Float и Double, является класс scala.Any. Он содержит базовые методы, поддерживаемые всеми типами в Scala - ==, !=, equals, hashCode, toString, isInstanceOf и asInstanceOf (большинство их названий говорит само за себя). Все остальные классы делятся на две категории: "примитивные" типы являются наследниками scala.AnyVal, а "объектные" типы - scala.AnyRef (наследником последнего является scala.ScalaObject).

Обычно разработчикам не приходится задумываться об этих различиях, однако иногда они могут быть причиной интересных эффектов, которые можно наблюдать, наследуя классы одного языка от классов другого. Обратите внимание на класс ScalaJavaPerson в листинге 12...

Листинг 12. Пример межъязыкового наследования
// Это Scala
class ScalaJavaPerson(firstName:String, lastName:String, age:Int)
  extends JavaPerson(firstName, lastName, age)
{
  val weight : Int = age * 2 // Кто-нибудь знает, сколько весят люди,
                             // знающие Scala и Java?

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

... который является наследником класса JavaPerson (листинг 13).

Листинг 13. Этот класс выглядит знакомо, не так ли?
// Это Java
public class JavaPerson
{
    public JavaPerson(String firstName, String lastName, int age)
    {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    
    public String getFirstName()
    {
        return this.firstName;
    }
    public void setFirstName(String value)
    {
        this.firstName = value;
    }
    
    public String getLastName()
    {
        return this.lastName;
    }
    public void setLastName(String value)
    {
        this.lastName = value;
    }
    
    public int getAge()
    {
        return this.age;
    }
    public void setAge(int value)
    {
        this.age = value;
    }
    
    public String toString()
    {
        return "[Person: firstName" + firstName + " lastName:" + lastName +
            " age:" + age + " ]";
    }
    
    private String firstName;
    private String lastName;
    private int age;
}

При компиляции ScalaJavaPerson будет, как и следовало ожидать, расширять JavaPerson, наследуя все его методы, но вдобавок к этому, в соответствии со спецификацией Scala, он будет также реализовывать интерфейс ScalaObject. Обратите внимание, что поскольку ScalaJavaPerson является классом Scala, он может быть присвоен в качестве значения любой переменной типа Any (листинг 14).

Листинг 14. Использование ScalaJavaPerson
// Это Scala    
    val richard = new ScalaJavaPerson("Richard", "Campbell", 45)
    System.out.println(richard)
    val host : Any = richard
    System.out.println(host)

Возникает вопрос: а что произойдет, если инстанциировать JavaPerson в Scala и также попробовать присвоить данный экземпляр переменной типа Any? (листинг 15).

Листинг 15. Использование JavaPerson
// Это Scala    
    val carl = new JavaPerson("Carl", "Franklin", 35)
    System.out.println(carl)
    val host2 : Any = carl
    System.out.println(host2)

При этом не возникает ошибок ни при компиляции, ни на этапе выполнения благодаря совместимости между типом Any в Scala и java.lang.Object в Java (другими словами, экземпляр JavaPerson ведет себя так, как от него ожидают). С некоторыми оговорками можно сказать, что объект любого типа, расширяющего java.lang.Object, может быть присвоен переменной типа Any (мне говорили о некоторых особых случаях, но я лично с ними никогда не сталкивался).

Итак, что же получается в "сухом остатке"? Во всех встречающихся на практике ситуациях можно без опаски создавать классы Scala, наследующие классы Java, и наоборот. Правда при этом большой проблемой будет придумать, как перегрузить в Java методы с "забавными именами", наподобие "^=!#", определенные в Scala.

Заключение

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

Осознав сходства и различия в моделях наследования обоих языков, вы сможете легко создавать Scala-версии Java-приложений. Например, можно реализовать на Scala такие популярные Java-библиотеки и инфраструктуры как JUnit, Servlets, Swing и SWT. Кстати, команда разработчиков Scala уже создала Swing-приложение под названием OOPScala (см. Ресурсы), которое реализует функциональность электронных таблиц при помощи JTable. При этом оно умещается в смехотворное число строк кода – на порядок меньшее, чем потребовалось бы для создания аналога на Java.

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


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


Похожие темы

  • Оригинал статьи: The busy Java developer's guide to Scala: Implementation inheritance (Ted Neward, developerWorks, май 2008 г.). (EN)
  • Слушайте Web-трансляцию Scala revealed (JavaWorld, июнь 2008 г.), в которой авторы developerWorks Эндрю Гловер (Andrew Glover) и Тед Ньюард обсуждают различия между объектно-ориентированными и функциональными языками программирования, а также приводят примеры задач, например, связанных с параллельной обработкой или работой с базами данных, для которых Java и другие "чистые" ОО-языки не являются наилучшим выбором. (EN)
  • Прочитайте всю серию Путеводитель по Scala для Java-разработчиков (Тед Ньювард, developerWorks, 2008 г.). (EN)
  • Прочитайте статью "Scala для беженцев из мира Java, часть 5: Признаки и типы" (Дэниел Спивак, Daniel Spiewak, Code Commit, февраль 2008 г.) – еще одно обсуждение модели наследования в Scala. (EN)
  • Ознакомьтесь со статьей "Размышления на тему реализации наследования при помощи примесей" (Дебасиш Гош, Debasish Ghosh, Ruminations of a Programmer, февраль 2008 г.), в которой обсуждаются компромиссы, определяющие модель наследования в Java в настоящее время. (EN)
  • Прочитайте статью "Путеводитель по Scala: функции высшего порядка" (Scala-lang.org), в которой приводится краткий пример использования функций высшего порядка в Scala. (EN)
  • OOPScala: Swing-application, написанное на Scala. (EN)
  • Прочитайте статью "Функциональное программирование на Java" (Абхиджит Белапуркар, Abhijit Belapurkar, developerWorks, июль 2004 г.), в которой обсуждаются преимущества и возможности применения функционального программирования с точки зрения разработчика Java. (EN)
  • Ознакомьтесь со статьей "Scala в примерах" (Мартин Одерски, Martin Odersky, декабрь 2007 г.), в которой приводится краткое введение в Scala, изобилующее примерами. В том числе в ней описывается приложение Quicksort, также использующееся в нашей серии (формат PDF). (EN)
  • Прочитайте Программирование на Scala (Мартин Одерски, Лекс Спун, Lex Spoon и Билл Веннерс; сигнальный экземпляр опубликован 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=407293
ArticleTitle=Путеводитель по Scala для Java-разработчиков: Наследование функциональности
publish-date=07082009