Просмотров: 23271

Java 8 vs. Scala: сравнение возможностей


Так все-таки, Java или Scala? Интернет полнится спорами о том, неужто Scala действительно лучше, чем Java, или «кто вам дал право так утверждать»? Урс Петер и Сандер ван ден Берг озаботились тем, чтобы провести подробное сравнение, из которого следует, что новый Java 8 — это все равно, что улучшенный Scala. Парадокс?

Перевод этой известной в англоязычной части Сети статьи под катом — приводится много практических примеров и листингов.

Введение

Выход JDK 8 (новый комплект для разработки на Java) запланирован на 2013 год, и в Oracle уже вполне четко представляют себе, что в него войдет. Саймон Риттер, выступавший на конференции QCon в Лондоне в этом году, обрисовал новые возможности, которые будут представлены в пакете. В частности, нас ожидают: модульная организация (проект Jigsaw), конвергенция JRockit/Hotspot, аннотирование типов и проект Lambda (примечание от автора блога — в восьмой Яве не будет Jigsaw, его официально пока отложили, так как не успеют к кодефризу, он перенесён на Java 9).

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

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

В этой статье мы изучим новые функции Java 8, ориентируясь как на предполагаемый новый синтаксис Java, так и Scala. Мы рассмотрим лямбда-выражения, функции высшего порядка, параллельные коллекции и виртуальные методы расширения, также называемые «типажами» (traits). Кроме того, мы расскажем о новых парадигмах, которые будут интегрированы в Java 8 — в частности, о функциональном программировании.

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

Лямбда-выражения/Функции?

Ну наконец-то в Java 8 появятся лямбда-выражения! Правда, в рамках проекта «Lambda» эти выражения доступны уже с 2009 года (тогда они еще назывались «Java-замыканиями»). Прежде, чем перейти к обсуждению примеров кода, стоит объяснить, почему лямбда-выражения — такое полезное дополнение к инструментарию Java.

Зачем нужны лямбда-выражения

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

В Swing, скажем, это делается при помощи ActionListener :

class ButtonHandler implements ActionListener {
      public void actionPerformed(ActionEvent e) {
            //do something
      }
}
class UIBuilder {
      public UIBuilder() {
            button.addActionListener(new ButtonHandler());
      }
}

Этот пример показывает, как использовать класс ButtonHandler в качестве замены обратного вызова. Класс ButtonHandler присутствует в коде только для того, чтобы содержать единственный метод: actionPerformed . Данный метод определен в интерфейсе ActionListener . Этот код можно упростить, воспользовавшись внутренними анонимными классами:

class UIBuilder {
      public UIBuilder() {
            button.addActionListener(new ActionListener() {
                  public void actionPerformed(ActionEvent event) {
                        //do something
                  }
            }
      }
}

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

Лямбда-выражения в качестве функций

Лямбда-выражение — это функциональный литерал. Он определяет функцию с входными параметрами и тело функции. Синтаксис лямбда-выражений в Java 8 пока еще обсуждается, но, вероятно, будет выглядеть примерно так:

(type parameter) -> function_body

А вот и конкретный пример:

(String s1, String s2) -> s1.length() - s2.length();

Это лямбда-выражение вычисляет разницу в длине двух строк. Этот синтаксис можно расширить, в частности, не определяя типы аргументов (такую ситуацию мы рассмотрим ниже). Кроме того, можно создавать многострочные определения, используя { and } , чтобы группировать выражения.

Идеальный пример использования такого выражения — метод Collections.sort() . Он позволяет нам отсортировать коллекцию строк (Strings) по их длине:

List <String> list = Arrays.asList("looooong", "short", "tiny" );
Collections.sort(list, (String s1, String s2) -> s1.length() - s2.length());
> "tiny", "short", "looooong".

Итак, вместо того чтобы заполнять метод sort реализацией Comparator , как это делалось бы в современном языке Java, для достижения аналогичного результата нам достаточно просто передать приведенное выше лямбда-выражение.

Лямбда-выражения в качестве замыканий

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

String outer = "Java 8"
(String s1) -> s1.length() - outer.length()

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

Выведение типа — и для лямбда-выражений

Выведение типа (type inference) — возможность, появившаяся в Java 7, — также применима и к лямбда-выражениям. В сущности, выведение типа заключается в том, что программист может обойтись без определения типа везде, где компилятор сам может вывести тип — то есть определить его дедуктивным методом.

Если применить выведение типа с лямбда-выражением, предназначенным для сортировки, то получится такой код:

List<String> list = Arrays.asList(...);
Collections.sort(list, (s1, s2) -> s1.length() - s2.length());

Как видите, типы параметров s1  и s2  опущены. Поскольку компилятору известно, что в списке содержится коллекция строк (Strings), ему также известно, что лямбда-выражение, используемое в качестве компаратора (сравнительного механизма), должно иметь два параметра типа String . Следовательно, отпадает необходимость в явном определении типов, хотя, конечно, это не возбраняется.

Основная польза от выведения типов — в сокращении шаблонного (boilerplate) кода. Если компилятор способен вывести тип за нас — зачем нам самим это делать?

Да здравствуют лямбда-выражения, прощайте, анонимные внутренние классы

Рассмотрим, как лямбда-выражения и выведение типов позволяют упростить пример с обратным вызовом, который мы затронули в начале статьи:

class UIBuilder {
  public UIBuilder() {
      button.addActionListener(e -> //process ActionEvent e)
  }
}

Вместо того чтобы определять класс для содержания нашего метода обратного вызова, мы просто передаем лямбда-выражение методу addActionListener . Таким образом, мы не просто сокращаем шаблонный код и улучшаем его читаемость, но и можем прямо выразить единственную инструкцию, которая нас интересует: код обработки события.

Пока мы не слишком увлеклись примерами использования лямбда-выражений, давайте посмотрим, как аналогичные выражения реализованы в Scala.

Лямбда-выражения в языке Scala

Функция — это основной первоэлемент функционального программирования. В Scala комбинируются объектно-ориентированные черты, известные всем, например, по языку Java, и функциональное программирование. В Scala базовым первоэлементом является как раз лямбда-выражение, называемое «функцией» или «функциональным литералом». В Scala функции являются «сущностями первого класса» (first-class citizens). Их можно присваивать финальным и нефинальным переменным (val или var , соответственно). В качестве аргумента функцию можно передавать другой функции, кроме того, их можно комбинировать для создания новых функций.

В Scala функциональный литерал записывается следующим образом:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Например, предыдущее лямбда-выражение на Java, вычисляющее разницу длины двух строк, в Scala будет записываться так:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

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

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Результат этого примера равен 20. Как видите, мы присвоили функциональный литерал переменной myFuncLiteral .

Синтаксическое и семантическое сходство лямбда-выражений из Java 8 и функций из Scala само по себе примечательно. Семантически они практически идентичны, а синтаксическая разница заключается только в представлении символа-стрелки (Java8: -> , Scala: => ), а еще проявляется в сокращенной нотации, которая здесь не рассматривается.

Функции высшего порядка как многоразовые элементы конструкций

Важнейшее достоинство функциональных литералов заключается в том, что их можно передавать точно так же, как любые другие литералы, вроде строки (String) или произвольного объекта (Object). Такая ситуация открывает широчайшие возможности и позволяет работать с исключительно компактными, многоразовыми программными конструкциями.

Наша первая функция высшего порядка

Когда мы передаем функциональный литерал методу, мы, в сущности, имеем дело с такой ситуацией: один метод принимает другой. Такие методы называются «функциями высшего порядка» (higher-order functions). Метод addActionListener из предыдущего примера кода Swing — именно такой. Мы также можем определять собственные функции высшего порядка, которые нам очень пригодятся.

Рассмотрим простой пример:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Здесь у нас есть метод measure, который измеряет время, необходимое для выполнения обратного вызова к функциональному литералу, называемому func . Сигнатура func такова, что он не принимает никаких параметров и возвращает результат обобщенного типа Т . Как видите, функции в Scala совсем не обязательно должны иметь параметры, хотя и могут их иметь — и зачастую будут их иметь.

Теперь можно передать любой функциональный литерал (или метод) методу измерения:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

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

Переиспользование при помощи функций высшего порядка

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

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Метод doWithContact считывает контактную информацию из файла — например, vCard . Далее он предлагает эту информацию синтаксическому анализатору (парсеру), который преобразует ее в домен объекта-контакта. После этого такой объект передается handle , обратному вызову функционального литерала. Этот вызов выполняет с доменом объекта-контакта ту операцию, которая предписывается в функции. Метод doWithContact , как и функциональный литерал, возвращает тип Unit , аналогичный использованию void в Java.

Теперь можно определить различные обратные вызовы, которые могут быть переданы методу doWithContact :

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Кроме того, передача обратного вызова может произойти внутристрочно:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

В чем польза функций высшего порядка

Видимо, аналогичный код на Java 8 будет выглядеть как-то так:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

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

Итак, используя функции высшего порядка, мы избавляем код от лишних повторений — таким образом, программист извлекает максимальную пользу из многократного использования кода практически на микроуровне.

Коллекции и функции высшего порядка

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

Фильтрация коллекций: до и после

Давайте рассмотрим типичный пример использования коллекций. Допустим, мы применяем определенное вычисление к каждому элементу коллекции. Например, у нас есть список фотографий (объектов Photo ), и мы хотим выделить все фотографии определенного размера:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Здесь у нас слишком много шаблонного кода. В частности, в нем выполняется создание результирующей коллекции и добавление новых элементов в список. В качестве альтернативы можно использовать класс Function , в который выносится все поведение функции:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Получится примерно такой код. Данный пример написан с использованием Guava.

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Шаблон немного сократился, но код по-прежнему остается довольно беспорядочным и пространным. Чтобы в полной мере ощутить потенциал и красоту лямбда-выражений, можно перевести этот код на Java 8 или Scala.

Scala:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Java 8:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Обе эти реализации красивы и очень лаконичны. Обратите внимание — в обоих вариантах применяется выведение типов: параметр p типа photo явно не определяется. Как мы уже говорили, при работе со Scala вы быстро убедитесь, что выведение типов — стандартная черта этого языка.

Цепочки функций в Scala

Итак, мы сократили код уже минимум на шесть строк и даже улучшили его читаемость. Но самое интересное начинается после сцепления нескольких функций высшего порядка. Чтобы это проиллюстрировать, давайте создадим класс photo и добавим ему дополнительные свойства на языке Scala:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Даже если вы не слишком хорошо знаете Scala, вы можете догадаться, что здесь произошло. Мы объявили класс Photo с тремя переменными экземпляра: name, sizeKb и rates . В переменной rates будут содержаться оценки, которые пользователи выставляют фотографии, — от 1 до 10. Теперь можно создать экземпляр класса Photo  — это делается так:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Имея список фотографий, достаточно просто определять различные вопросы, при этом мы будем сцеплять друг с другом по нескольку функций высшего порядка. Предположим, нам требуется извлечь имена всех файлов-изображений, размер которых превышает 10 Мб.

Первый вопрос: как преобразовать список названий фотографий в список имен файлов? Для этого мы воспользуемся одной из наиболее мощных функций высшего порядка, которая называется map :

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Метод map преобразует каждый элемент коллекции в элемент того типа, который определен в функции, переданной этому методу. В этом примере у нас есть функция, которая получает объект Photo , а возвращает строку (String). Эта строка представляет собой имя файла изображения.

Применяя функцию map , мы можем решить поставленную задачу, сцепив методы map и filter , где map идет вторым.

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Не стоит опасаться исключений NullPointerExceptions , поскольку каждый из методов (filter, map и т.д.) всегда возвращает коллекцию. А коллекция, даже если она пуста, не может быть null . Так, наша коллекция с фотографиями с самого начала была пустой, и в результате вычислений у нас, опять же, получится пустая коллекция.

Сцепление функций также называется «композицией функций». При применении композиции функций можно задействовать API Collections , чтобы найти в них блоки для решения нашей проблемы.

Рассмотрим более сложный пример:

Задача: «Вернуть имена всех фотографий, чей средний рейтинг выше 6, отсортировав их по общей сумме присвоенных рейтингов».

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Для решения этой задачи воспользуемся методом sortBy , который ожидает функцию, принимающую в качестве ввода элемент типа «коллекция» (в данном случае — Photo ). Далее этот метод возвращает объект типа Ordered (в данном случае — Int ). Поскольку список (List ) не имеет метода для нахождения среднего значения, определим функциональный литерал avg . Этот литерал вычислит среднее значение заданного списка целых чисел (Ints ) в анонимной функции, которая будет передана методу filter .

Цепочки функций в Java 8

Пока не совсем ясно, какие функции высшего порядка будут предлагаться в классах коллекций языка Java 8. Скорее всего, будут поддерживаться filter и map . Следовательно, первая из приведенных выше цепочек, вероятно, будет выглядеть так:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Опять же стоит отметить, что в синтаксическом плане разница со Scala практически отсутствует.

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

Параллельные коллекции

До сих пор мы еще не касались одного из самых важных преимуществ, связанных с использованием функций высшего порядка. Такие функции не только улучшают читаемость кода и делают его более кратким, но и добавляют очень важный уровень абстракции. Обратите внимание: во всех предыдущих примерах не было ни одного цикла. Кроме того, для фильтрации коллекций их не приходилось перебирать, ассоциировать (соотносить) или сортировать элементы. Итерация скрыта от пользователя коллекции, говоря иначе, — абстрагирована.

Дополнительный уровень абстракции необходим для максимально эффективного использования многоядерных платформ, поскольку базовая реализация цикла может самостоятельно «определять», как именно перебирать коллекцию. Следовательно, перебор может происходить не только последовательно, но и параллельно. Эффективное использование параллельной обработки при работе с многоядерными платформами — это уже не просто приятная, но обязательная мелочь.

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

Во-вторых, нам не хотелось бы в итоге получить ряд совсем несхожих реализаций. Даже притом, что в Java 7 есть Fork/Join, проблему декомпозиции и пересборки данных приходится решать на стороне клиента, а это не тот уровень абстракции, который нам нужен. И, в-третьих, зачем изобретать велосипед, если решение описанных проблем уже найдено в рамках функционального программирования?

Параллельные коллекции в Scala

Рассмотрим простой пример на Scala, где применяется параллельная обработка:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Сначала определяем метод heavyComputation , который — как понятно из названия — занимается интенсивными вычислениями (heavy computation). На четырехъядерном ноутбуке приведенное вычисление выполняется примерно за 4 секунды. После этого мы инстанцируем коллекцию типа range (диапазон от 0 до 10) и инициируем метод par . Метод par возвращает параллельную реализацию, которая предлагает такие же интерфейсы, как и последовательный аналог. Метод par есть в большинстве типов коллекций языка Scala — и это все, что нужно о нем знать.

Далее зададимся вопросом, какой выгоды в плане производительности можно добиться на четырехъядерном компьютере. Чтобы найти ответ, давайте повторно используем измерительный метод, встречавшийся нам выше:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

В данном случае довольно странным покажется тот факт, что параллельное исполнение всего в 2,5 раза быстрее последовательного, хотя мы и задействовали все четыре ядра. Причина в том, что при обеспечении параллелизма возникают дополнительные издержки: часть вычислительной мощности тратится на порождение новых потоков и интеграцию промежуточных результатов. Следовательно, не стоит использовать параллельные коллекции по умолчанию, их лучше применять только в ходе интенсивных вычислений.

Параллельные коллекции в Java 8

Интерфейс, который предположительно будет использоваться в Java 8 для работы с параллельными коллекциями, практически такой же, как и в Scala:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Парадигма точно такая же, как и в Scala. Единственное отличие заключается в том, что метод для создания параллельной коллекции называется parallel() , а не par .

А теперь — все сразу

Подведем итог: как может выглядеть работа с функциями высшего порядка/лямбда-выражениями в комбинации с параллельными коллекциями? Для этого мы рассмотрим довольно объемный пример кода на языке Scala. Здесь скомбинированы многие идеи, которые мы обсуждали выше.

Для работы с этим примером кода был выбран сайт, на котором предлагается коллекция пейзажных обоев. Мы напишем программу, которая будет извлекать url всех этих настольных изображений и параллельно загружать картинки. Кроме основных библиотек Scala, мы воспользуемся еще двумя — Dispatch для обеспечения http-коммуникации и FileUtils , библиотеку Apache, которая упрощает решение некоторых задач. Здесь придется столкнуться с некоторыми феноменами языка Scala, которые не освещены в данной статье, но их назначение и так должно быть понятно.

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Объяснение кода

Метод scrapeWallpapers обрабатывает поток управления, в котором URL изображений выбираются из html-документа, после чего загружается каждое из изображений.

При помощи fetchWallpaperImgURLsOfPage все URL обоев считываются из html, отображаемого на экране.

Объект Http — это класс из HTTP-библиотеки dispatch, которая предоставляет предметно-ориентированный язык (DSL) для работы с библиотекой Apache httpclient. Метод as_tagsouped преобразует html в xml, а xml уже является в Scala встроенным типом данных.

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Далее мы получаем из html в виде xml соответствующие href тех изображений, которые хотим загрузить:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Поскольку XML нативен в Scala, мы можем использовать xpath-подобное выражение \\ для выбора интересующих нас узлов. После получения всех href нам понадобится отфильтровать URL всех интересующих нас изображений и превратить href-ы в URL-объекты. Для этого мы сцепим несколько функций высшего порядка из API Collection языка Scala, как делали это выше с map и filter . В результате имеем список URL изображений.

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Далее требуется выполнить параллельную загрузку всех изображений. Для достижения параллелизма мы преобразуем список изображений в параллельную коллекцию. Затем метод foreach запускает несколько потоков для одновременного перебора элементов коллекции. Каждый из этих потоков будет, в конце концов, вызывать метод copyToDir .

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Метод copyToDir работает при помощи библиотеки FileUtils , относящейся к Apache Common. Статический метод copyURLToFile класса FileUtil импортируется также статически — следовательно, его можно вызывать непосредственно. Для ясности общей картины мы также выведем на экран имя потока, выполняющего задачу. При параллельном исполнении мы увидим, что одновременно интенсивно работает сразу несколько потоков.

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Этот метод также показывает, что Scala в полной мере обеспечивает взаимодействие с имеющимися библиотеками Java.

Итак, функциональные черты Scala и связанные с ними очевидные выгоды — в частности, применение функций высшего порядка к коллекциям и «бесплатный» параллелизм — позволяют параллельно выполнять синтаксический анализ, ввод-вывод и преобразование данных. Для этого достаточно всего лишь нескольких строк кода.

Виртуальные методы расширения/типажи

Виртуальные методы расширения в Java напоминают типажи (traits) из Scala. Что же такое «типажи»? Типаж в Scala сразу предоставляет интерфейс и может также включать реализацию — такая структура предлагает массу возможностей.

Подобно Java 8, Scala не поддерживает множественное наследование. Как в Java, так и в Scala подкласс может дополнять только один суперкласс. Но в случае с типажами наследование строится иначе: в классе могут «смешиваться» несколько типажей. Интересно отметить, что класс приобретает как тип и все методы, так и состояние такого типажа (типажей). Поэтому типажи также именуются «примесями», поскольку они добавляют в класс новые состояния и поведения.

Однако встает вопрос: если типажи обеспечивают какую-то форму множественного наследования, не подвержены ли они пресловутой «проблеме ромба»?

Ответ: нет, не подвержены. Scala определяет четкий набор правил старшинства (precedence rules), определяющих, какой код когда выполняется в иерархии множественного наследования. Эти правила не зависят от количества примесей. Работая с такими правилами, мы приобретаем все плюсы множественного наследования, но никаких связанных с ним проблем.

Если у нас есть типаж

В следующем примере показан код, понятный всем, кто работал в Java:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

При всей его важности с точки зрения дизайна, в повседневной практике журналирование происходит как бы незаметно. Каждый класс, снова и снова, объявляет регистратор (логгер). А нам все время приходится проверять, активирован ли уровень журналирования, — это делается, например, с помощью isDebugEnabled() . Налицо нарушение правила DRY (Don’t Repeat Yourself) — «Не повторяйся». К тому же в Java нет возможности убедиться, объявил ли программист проверку на уровне лога и использует ли он подходящий логгер, ассоциированный с классом. Java-разработчики настолько привыкли к подобной практике работы без журналирования, что она уже считается паттерном.

Типажи предлагают отличную альтернативу такому паттерну. Если поместить функционал журналирования в типаж, то впоследствии мы сможем смешать этот типаж с любым классом, в какой только захотим. Таким образом, класс получает доступ к сквозной функциональности журналирования — и при этом ничем не ограничиваются возможности наследования от другого класса.

Журналирование в качестве типажа — решение проблемы журналирования

В Scala мы можем реализовать типаж Loggable следующим образом:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Типаж в Scala определяется при помощи ключевого слова trait . В теле типажа может содержаться всё то же, что и в абстрактном классе, — например, поля или методы. Еще один интересный момент — использование self => . Применяется он, поскольку регистратор должен журналировать класс, с которым смешивается типаж Loggable , а не сам типаж. Синтаксис self => — такая конструкция называется в Scala «self-type » — позволяет типажу получить ссылку на класс, с которым этот типаж смешивается.

Обратите внимание на использование функции без параметров msg: => T , которая служит параметром ввода для метода отладки.

Основная причина использования проверки isDebugEnabled() такая: нам необходимо убедиться, что занесенная в журнал строка String будет рассчитываться лишь в случае, когда активирован отладочный уровень. То есть, если метод debug будет принимать в качестве параметра ввода только строку String , то журнальное сообщение так или иначе будет создаваться независимо от того, активирован ли отладочный уровень, — а это нежелательно.

Но, передавая вместо строки String функцию без параметров msg: => T , мы получаем именно то, что нужно. А именно: функция msg , возвращающая строку String для записи в регистрационный журнал, будет активирована лишь при условии, что проверка isDebugEnabled выполнена успешно. Если выполнить isDebugEnabled не удается, то функция msg так и не вызывается, и строка String не рассчитывается, поскольку в этом нет необходимости.

Если мы хотим использовать типаж Loggable в классе Photo , то для смешивания типажа с классом применяется ключевое слово extends :

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Ключевое слово extends наводит на мысль, что класс Photo наследует от Loggable  и, соответственно, не может дополнять никакой другой класс. Но это не так. Просто по правилам синтаксиса Scala необходимо, чтобы смешивание с классом или дополнение класса начиналось со слова extends . Если мы хотим смешать с классом несколько типажей, то с каждым типажом после первого используется ключевое слово with . Ниже будут приведены примеры работы с этим словом.

Чтобы продемонстрировать, что описанный метод работает, применим метод save() к классу Photo :

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Добавление к классам дополнительных поведений

В предыдущей части мы говорили о том, что с классом может смешиваться несколько типажей. Итак, кроме функциональности журналирования, мы можем добавить классу Photo и другое поведение. Допустим, нам нужна возможность упорядочить фотографии Photos в зависимости от размера файла. Приятно отметить, что в языке Scala предлагается ряд готовых типажей. Среди них — типаж Ordered[T] .

Несмотря на то, что типаж Ordered напоминает интерфейс Java Comparable , большое и очень важное отличие между ними заключается в том, что в случае со Scala мы располагаем готовой реализацией:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

В показанном выше примере с классом смешиваются два типажа. Кроме определенного выше Loggable , мы также смешиваем с классом типаж Ordered[Photo] . Типаж Ordered[T] требует реализации метода compare(type:T) , и это очень напоминает работу с интерфейсом Java Comparable .

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

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

В отличие от Java, символьные имена, такие, как > и <= , в Scala не являются специально зарезервированными ключевыми словами. возможностью сравнивать объекты при помощи <=, > и т.д. мы обязаны типажу Ordered , который реализует методы с этими символами.

Классы, реализующие типаж Ordered , можно сортировать в любой коллекции Scala. Имея коллекцию, заполненную объектами Ordered , можно применять к этой коллекции ключевое слово sorted , сортирующее объекты по порядку, определенному в методе compare .

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Достоинства типажей

Из приведенных примеров становится ясно, что при помощи типажа можно изолировать обобщенную функциональность в виде модуля. При необходимости такую изолированную функциональность можно подключить к любому классу. Чтобы добавить к классу Photo функцию журналирования, мы смешали с ним типаж Loggable , а для функции упорядочивания — типаж Ordered . Эти типажи можно вновь использовать и в других классах.

В этом заключается мощный механизм для создания модульного и «сухого» (DRY) кода. В ходе работы с ним мы обходимся лишь встроенными возможностями языка, не прибегая к дополнительным технологиям — например, любимому мной аспектно-ориентированному программированию.

Зачем использовать виртуальные методы расширения?

В спецификации Java 8 содержится черновое описание виртуальных методов расширения. Виртуальные методы расширения добавляют стандартные реализации для новых и/или имеющихся методов имеющихся интерфейсов. А зачем?

При работе со многими уже существующими интерфейсами было бы очень полезно реализовать поддержку лямбда-выражений в форме функций высшего порядка. Давайте для примера рассмотрим интерфейс java.util.Collection . Было бы очень неплохо, если бы он включал метод forEach (lambdaExpr). Но если бы такой метод предоставлялся без стандартной реализации, то все классы, использующие его, должны были бы такую реализацию обеспечивать сами. Совершенно очевидно, что в таком случае наступил бы настоящий хаос совместимости.

Именно поэтому команда разработчиков JDK решила прибегнуть к виртуальным методам расширения. С ними можно, например, добавить метод forEach к интерфейсу java.util.Collection вместе со стандартной реализацией. Следовательно, все использующие его классы будут автоматически наследовать и этот метод, и его реализацию. В таком случае API будет эволюционировать как бы сам по себе, а ведь именно для этого и придуманы виртуальные методы расширения. Но если реализующему классу недостаточно стандартной реализации, то ее просто можно переопределить.

Сравнение виртуальных методов расширения и типажей

Итак, основной причиной для использования виртуальных методов расширения является эволюция API. К тому же работа с ними порождает приятный положительный эффект: обеспечивается своеобразное множественное наследование, проявляющееся только на уровне поведений. А в Scala типажи допускают множественное наследование не только поведений, но и состояния, и даже позволяют ссылаться на реализующий класс (в примере с типажом Loggable мы применили для этого поле self ).

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

Типажи Loggable и Ordered в Java 8

Чтобы оценить, чего можно достичь при помощи виртуальных методов расширения, давайте попробуем реализовать типажи Ordered и Loggable в Java 8.

Типаж Ordered можно полностью реализовать при помощи виртуальных методов расширения, так как в нем не используется никаких состояний. Как уже упоминалось выше, аналогом Ordered в Java является java.lang.Comparable . Реализации будет иметь следующий вид:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

К уже имеющемуся интерфейсу Comparable мы добавили новые методы сравнения («больше», «больше или равно», «меньше», «меньше или равно»), идентичные тем, что встречаются в типаже Ordered (>, >=, <, <=) . Стандартная реализация, помеченная ключевым словом default, переадресует все вызовы к имеющемуся абстрактному методу сравнения. В результате существующие интерфейсы обогащаются новыми методами, а классы, реализующие Comparable , избавляются от необходимости реализовывать также и эти методы.

Если бы класс Photo реализовывал Comparable , мы могли бы производить операции сравнения с новыми добавляемыми методами.

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

Типаж Loggable реализуется при помощи виртуальных методов расширения не полностью, но почти полностью:

Scala Java 8 ява скала примеры сравнение листинг код программирование программер кодер противопоставление лучше

В этом примере мы добавили к интерфейсу Loggable методы журналирования, в частности, debug, info и т.д. По умолчанию эти методы делегируют свои вызовы статическому экземпляру Logger . Чего нам не хватает, так это ссылки на реализующий все это класс. Поскольку механизм для этого отсутствует, мы применим для регистрации интерфейс Loggable , который будет регистрировать все инструкции под Loggable вместо реализующего класса. В силу такого ограничения виртуальные методы расширения при таком сценарии менее удобны.

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

Заключение

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

Все описанные здесь возможности уже доступны в языке Scala, как мы это только что описали. Разработчики, желающие с ними познакомиться, могут опробовать ранние сборки Java 8 на большинстве платформ. Можно также присмотреться к Scala, чтобы заранее подготовиться к переменам.

~

В качестве послесловия. Отблески этой холиварной битвы через призму типичных комментариев взятых с форума:

За Java:

Это просто разные весовые категории. Scala — это sandbox, Java — industry standart. Перед тем как внедрить что-либо в industry standart, идеи о концепции обкатывают в sandbox’е. А после выхода Java8, ваша Scala фактически станет deprecated. Мне немного жаль ярых поклонников Scala — надо просто понимать, что у всего есть своё предназначение. Как говорят в народе: «Всяк сверчок знай свой шесток».

За Scala:

Спасибо, посмеялся. Scala — это не просто «улучшенная Java», Scala — это отдельный полноценный язык со своей философией, своим очень живым и активным коммьюнити, гайдлайном и т.д. Java никогда не станет настолько же строго типизированной, как Scala, иначе это сломает обратную совместимость, в этом узкое место её «индустриальности» (вы же это за плюс выдаёте).

Java никогда не начнёт использовать необязательный синтаксис, иначе это поломает сами основы этого слишком устоявшегося языка. И, повторюсь: Java никогда не станет community-driven языком, таким как Scala, Ruby, Python или Lisp, иначе это уже будет просто не Oracle Java, а что-то другое. Так что тут просто нечему становиться deprecated. Гораздо более вероятен другой сценарий, при котором Java станет всего лишь одним из многих равноправных языков для JVM, к этому всё и идёт, если вы вспомните всякие Kotlin и так далее.

dev.by, 2012

twitter.com facebook.com vkontakte.ru odnoklassniki.ru mail.ru ya.ru pikabu.ru blogger.com liveinternet.ru livejournal.ru google.com bobrdobr.ru yandex.ru del.icio.us

Подписка на обновления блога → через RSS, на e-mail, через Twitter
Теги: , , , , , , ,
Эта запись опубликована: Суббота, 13 октября 2012 в рубрике Программирование.

5 комментариев

Следите за комментариями по RSS
  1. Во многих случаях перепутаны местами примеры кода на Scala и Java 8.

  2. Спасибо за замечание, вроде бы всё зачистил и справил (дубль два).

  3. Прикрепляйте к статьям даты - уважайте читателей!

  4. Paul, если я правильно понял, вы хотели увидеть дату выхода статьи - она приводится в самом низу каждой статьи перед комментариями, в данном случае это:

    Эта запись опубликована: Суббота, 13 октября 2012 в рубрике Программирование.

  5. Проблема Java в том, что в ней нет ссылок на функции от того и не было лямда-выражений. Лямда выражения давно существуют в C# и вообще много что только появляется в Java уже давно используется в C# (динамические типы, лябда-выражения, замыкания) так что в этом плане Java просто догоняет и возможно даже и не придумывает, а тупо копирует.

Оставьте комментарий!

Не регистрировать/аноним

Используйте нормальные имена. Ваш комментарий будет опубликован после проверки.

Зарегистрировать/комментатор

Для регистрации укажите свой действующий email и пароль. Связка email-пароль позволяет вам комментировать и редактировать данные в вашем персональном аккаунте, такие как адрес сайта, ник и т.п. (Письмо с активацией придет в ящик, указанный при регистрации)

(обязательно)


⇑ Наверх
⇓ Вниз