Содержание


Java.next

Java 8 как язык Java.next

Оценка версии Java 8 в качестве адекватной замены Java

Comments

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

Этот контент является частью # из серии # статей: Java.next

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

Этот контент является частью серии:Java.next

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

Первоначальная идея этого цикла состояла в сравнительном анализе трех новых языков для платформы JVM, чтобы помочь вам понять, какой из них является вероятным преемником языка Java. Однако тем временем язык Java подвергся самому значительному изменению с момента добавления в него дженериков (generics). В настоящее время сам язык Java обладает многими востребованными характеристиками языков Groovy, Scala и Clojure. В этой статье я рассматриваю Java 8 в качестве языка Java.next и привожу примеры, которые показывают, насколько эффективно были дополнены существующие парадигмы программирования на языке Java.

Функции высшего порядка — ну, наконец-то!

Функции высшего порядка принимают другие функции в качестве своих параметров или возвращают другие функции в качестве своих результатов. У языка Java — возможно, последнего среди популярных языков — наконец-то появились функции высшего порядка в форме т. н. лямбда-выражений. Вместо простого "прикручивания" к языку функций высшего порядка проектировщики Java 8 приняли более разумное решение и позволили старым интерфейсам использовать новые функциональные возможности.

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

Листинг 1. Императивное преобразование имен
public String cleanNames(List<String> listOfNames) {
    StringBuilder result = new StringBuilder();
    for(int i = 0; i < listOfNames.size(); i++) {
        if (listOfNames.get(i).length() > 1) { 
            result.append(capitalizeString(listOfNames.get(i))).append(",");
        }
    }
    return result.substring(0, result.length() - 1).toString();
}

public String capitalizeString(String s) {
    return s.substring(0, 1).toUpperCase() + s.substring(1, s.length());
}

Итерирование было нормой в предыдущих версиях Java, однако в версии Java 8 эта задача выполняется более изящно благодаря потокам— абстракции, которая действует подобно гибриду коллекции и канала в UNIX®. В листинге 2 используются потоки.

Листинг 2. Преобразование имен на языке Java 8
public String cleanNames(List<String> names) {
    return names
            .stream()
            .filter(name -> name.length() > 1)
            .map(name -> capitalize(name))
            .collect(Collectors.joining(","));
}

private String capitalize(String e) {
    return e.substring(0, 1).toUpperCase() + e.substring(1, e.length());
}

Итеративная версия в листинге 1 должна объединить задачи фильтрации, преобразования и конкатенации в рамках одного цикла for— поскольку отдельное циклическое прохождение по коллекции для каждой из этих задач было бы неэффективно. С помощью потоков в Java 8 можно связывать функции в цепочку и объединять между собой — до тех пор, пока не осуществляется вызов функции, которая генерирует вывод (т. н. терминальной операции), такой как collect() или forEach().

Метод filter() в листинге 2— это тот же самый метод filter, который широко применяется в функциональных языках (см. статью Java.next: Преодоление синонимических трудностей). Метод filter() принимает функцию высшего порядка, которая возвращает логическое значение, используемое в качестве критерия фильтрации: значение true указывает на включение в отфильтрованную коллекцию, а значение false указывает на исключение из коллекции.

Метод filter() принимает тип Predicate<T> (это метод, возвращающий логическое значение). При желании можно создавать экземпляры предиката в явном виде (см. листинг 3).

Листинг 3. Создание предиката вручную
Predicate<String> p = (name) -> name.startsWith("Mr");
List<String> l = List.of("Mr Rogers", "Ms Robinson", "Mr Ed");
l.stream().filter(p).forEach(i -> System.out.println(i));

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

В листинге 2 метод map() работает ожидаемым образом — он применяет метод capitalize() к каждому элементу в коллекции. И, наконец, я вызываю метод collect(), который является терминальной операцией — он генерирует значения из потока. Метод collect() выполняет знакомую операцию reduce: объединение элементов с целью (как правило) уменьшения количества получаемых результатов, иногда — вплоть до единственного значения (пример: операция sum). В Java 8 имеется метод reduce(), однако в данном случае метод collect() предпочтительнее, поскольку он эффективно работает с изменяемыми контейнерами, такими как StringBuilder.

Реализация в существующих классах и коллекциях поддержки таких функциональных конструкций, как map и reduce, сталкивается в Java с проблемой эффективного обновления коллекций. Например, операция reduce теряет значительную часть своей полезности, если ее нельзя использовать с типичными Java-коллекциями, такими как ArrayList. Многие библиотеки коллекций в Scala и Clojure по умолчанию являются неизменяемыми, что позволяет среде исполнения генерировать эффективные операции. Java 8 не в состоянии заставить разработчиков изменить коллекции, а многие существующие классы коллекций в Java являются изменяемыми. По этой причине в Java 8 включены методы, которые выполняют операцию mutable reduction для коллекций (таких как ArrayList и StringBuilder), которая обновляет существующие элементы, а не заменяет каждый раз результат. Хотя метод reduce() будет работать в листинге 2, метод collect() работает более эффективно для коллекций, возвращаемых в этом экземпляре.

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

Листинг 4. Параллельная обработка в Java 8
public String cleanNamesP(List<String> names) {
    return names 
            .parallelStream() 
            .filter(n -> n.length() > 1) 
            .map(e -> capitalize(e)) 
            .collect(Collectors.joining(","));
}

Как и в Scala, я могу заставить потоковые операции в листинге 4 работать параллельно, добавив модификатор parallelStream(). Функциональное программирование делегирует детали реализации среде исполнения, что позволяет вам работать на более высоком уровне абстракции. Простота применения поточной обработки к коллекциям служит примером этого преимущества.

Различия между reducer-типами в Java 8 иллюстрирует трудность добавления глубоких парадигм, таких как функциональное программирование, к существующим языковым конструкциям. Проектировщики Java 8 сделали превосходную работу — добавили функциональные конструкции максимально "прозрачным" образом. Хорошим примером этой интеграции является добавление функциональных интерфейсов.

Функциональные интерфейсы

Обычная для Java идиома представляет собой интерфейс с единственным методом — т. н. SAM-интерфейс (single abstract method) — например, Runnable или Callable. Во многих случаях SAM-интерфейсы используются преимущественно в качестве транспортного механизма для переносимого кода. В Java 8 переносимый код лучше всего реализуется с помощью лямбда-выражений. Интеллектуальный механизм под названием функциональные интерфейсы (functional interfaces) позволяет лямбда-выражениям и SAM-интерфейсам взаимодействовать полезным образом. Функциональный интерфейс — это интерфейс, который содержит единственный абстрактный метод (и может включать несколько методов по умолчанию). Функциональные интерфейсы дополняют существующие SAM-интерфейсы, делая возможной замену традиционных анонимных внутренних классов лямбда-выражениями. Например, интерфейс Runnable может быть помечен аннотацией @FunctionalInterface. Эта необязательная аннотация дает компилятору указание подтвердить, что Runnable— это интерфейс (а не класс и не перечисление) и что аннотируемый тип удовлетворяет требованиям функционального интерфейса.

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

new Thread(() -> System.out.println("Inside thread")).start();

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

Методы по умолчанию

Java 8 также позволяет декларировать для интерфейсов т. н. методы по умолчанию (default methods). Метод по умолчанию — это неабстрактный нестатический public-метод (с телом), декларированный в типе интерфейса и помеченный ключевым словом default. Каждый метод по умолчанию автоматически добавляется к классам, реализующим интерфейс, — удобный способ для декорирования классов функциональностью по умолчанию. Например, интерфейс Comparator в настоящее время включает более десятка методов по умолчанию. Если я создаю какой-либо компаратор с помощью лямбда-выражения, то я могу с минимальными усилиями создать и реверсивный компаратор (см. листинг 5).

Листинг 5. Методы по умолчанию для компаратора
List<Integer> n = List.of(1, 4, 45, 12, 5, 6, 9, 101);
Comparator<Integer> c1 = (x, y) -> x - y;
Comparator<Integer> c2 = c1.reversed();
System.out.println("Smallest = " + n.stream().min(c1).get());
System.out.println("Largest = " + n.stream().min(c2).get());

В листинге 5 я создаю экземпляр Comparator, обернутый вокруг лямбда-выражения. После этого я могу создать реверсивный компаратор посредством вызова метода по умолчанию reversed(). Возможность присоединение методов по умолчанию к интерфейсам напоминает обычное использование миксинов (см. статью Миксины и трейты) и является отличным дополнением для языка Java.

Optional

Обратите внимание на завершающий вызов get() в терминальных вызовах, показанных в листинге 5. Вызовы встроенных методов, таких как min(), возвращают Optional, а не значение. Это поведение напоминает поведение элемента option в языках Java.next, продемонстрированное на примере языка Scala в статье Общие черты Groovy, Scala и Clojure, часть 3. Optional предотвращает одновременное присутствие в возврате метода null как ошибки и null как приемлемого значения. Например, терминальные операции в Java 8 способны использовать метод как приемлемого значения. Например, терминальные операции в Java 8 способны использовать метод ifPresent() для исполнения фрагмента кода только в том случае, если существует приемлемый результат. Например, следующий код печатает результат только при наличии значения.

n.stream()
    .min((x, y) -> x - y)
    .ifPresent(z -> System.out.println("smallest is " + z));

Кроме того, имеется метод orElse(), который я могу использовать, если я хочу выполнить дополнительное действие. Просмотр интерфейса Comparator в Java 8 наглядно демонстрирует, насколько мощные возможности добавляют методы по умолчанию.

Более подробно о потоках

Интерфейс Stream и связанные с ним средства Java 8 — это хорошо продуманный набор расширений, которые вдохнут новую жизнь в язык Java

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

  • Потоки не хранят значений, а действуют скорее как конвейер от входного источника до пункта назначения — терминальной операции.
  • Потоки спроектированы как функциональные элементы, а не как stateful-элементы. Например, операция filter() возвращает поток отфильтрованных значений без изменения базовой коллекции.
  • Потоковые операции пытаются быть настолько "ленивыми", насколько это возможно (см. статьи Java.next: Мемоизация и функциональный синергизм " и и Функциональное мышление: Отложенное выполнение. Часть 1). Ленивая коллекция выполняет работу только в том случае, если необходимо извлекать значения.
  • Потоки могут быть неограниченными (или бесконечными). Например, вы можете создать поток, который возвращает все числа, и задействовать такие методы, как limit() и findFirst(), для сбора подмножеств.
  • Подобно элементам типа Iterator, потоки потребляются после использования и требуют регенерации перед очередным повторным использованием.

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

Потоки уже включают множество полезных терминальных операций. В качестве иллюстрации обратимся к примеру классификатора чисел из моего цикла Функциональное мышление (который также используется в двух предыдущих статьях цикла Java.next). В листинге 6 этот классификатор реализован на языке Java 8.

Листинг 6. Классификатор чисел на языке Java 8
public class NumberClassifier {

    public static IntStream factorsOf(int number) {
        return range(1, number + 1)
                .filter(potential -> number % potential == 0);
    }

    public static boolean isPerfect(int number) {
        return factorsOf(number).sum() == number * 2;
    }

    public static boolean isAbundant(int number) {
        return factorsOf(number).sum() > number * 2;
    }

    public static boolean isDeficient(int number) {
        return factorsOf(number).sum() < number * 2;
    }

}

Если вы уже знакомы с моими версиями классификатора чисел на других языках (см. статью Функциональное мышление: Часть 1. Разработка программ в функциональном стиле), то вы заметите, что в листинге 6 отсутствует декларация метода sum(). Во всех остальных реализациях этого кода на альтернативных языках я должен был сам написать метод sum(). Java 8 содержит sum() в виде терминальной операции, что освобождает меня от написания этого метода. Функциональное программирование скрывает "движущиеся части", сокращая возможности для возникновения ошибок разработчика. Если мне не нужно заниматься реализацией sum(), то я не смогу сделать ошибку в этой реализации. Интерфейс Stream и связанные с ним средства Java 8 — это хорошо продуманный набор расширений, которые вдохнут новую жизнь в язык Java.

В других вариантах классификатора чисел я показал оптимизированную версию метода factors(), которая осуществляет прохождение по потенциальным делителям только вплоть до квадратного корня и генерирует симметричные делители. В листинге 7 показана оптимизированная версия метода factors() на языке Java 8.

Листинг 7. Оптимизированный классификатор на языке Java 8
    public static List fastFactorsOf(int number) {
        List<Integer> factors = range(1, (int) (sqrt(number) + 1))
                .filter(potential -> number % potential == 0)
                .boxed()
                .collect(Collectors.toList());
        List factorsAboveSqrt = factors
                .stream()
                .map(e -> number / e).collect(toList());
        factors.addAll(factorsAboveSqrt);
        return factors.stream().distinct().collect(toList());
    }

Метод factorsOf() в листинге 7 не может объединить два потока в один результат, даже если эти потоки были связаны. Тем не менее после прохода по потоку он становится исчерпанным (подобно Iterator) и перед повторным использованием требует регенерации. В листинге 7 я создаю две коллекции с использованием потоков и связываю результаты посредством добавления вызова distinct() для обработки граничного случая — появления дубликатов вследствие целочисленных квадратных корней. Потоки в Java 8 обладают внушительной мощью (особенно с учетом возможности комбинирования потоков).

Заключение

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

В следующей статье я завершаю этот цикл размышлениями о выборе языка.


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


Похожие темы

  • Оригинал статьи: Java.next: Java 8 as Java.next.
  • JDK 8 project: загрузите Java 8.
  • Lambda Expressions (Лямбда-выражения). Учебное пособие по некоторым новым возможностям Java 8.
  • Java 8 language changes (Изменения в языке Java 8), Денис Сосноски (Dennis Sosnoski), developerWorks, 2014 г. Познакомьтесь с критическим взглядом на лямбда-выражения и на изменения интерфейсов в языке Java.
  • Functional Programming in Java (Функциональное программирование на Java), Венкат Субраманиам (Venkat Subramaniam), издательство Pragmatic Bookshelf, 2014 г.: прекрасное учебное руководство.
  • Functional Thinking (Функциональное мышление), Нил Форд (Neal Ford), издательство O'Reilly Media, 2014 г.): книга автора данного цикла, рассматривающая множество тем по Java 8.
  • Цикл статей Functional thinking (Функциональное мышление): колонка Нила Форда на developerWorks, посвященная функциональному программированию.
  • Записная книжка дизайнера языка: в этом цикле статей developerWorks архитектор языка Java Брайан Гетц исследует некоторые проблемы дизайна языка, вызвавшие трудности при эволюции языка Java версий Java SE 7, Java SE 8 и далее.
  • Введение в мультиарендность для виртуальных машин Java, Грэм Джонсон (Graeme Johnson) и Майкл Доусон (Michael Dawson), developerWorks, сентябрь 2013 г. Новая опция для облачных систем в бета-релизе IBM Java 8.
  • Scala: современный функциональный язык, работающий на платформе JVM.
  • Clojure: современный функциональный вариант Lisp, работающий на платформе JVM.
  • IBM SDK, Java Technology Edition Version 8: примите участие в программе бета-тестирования IBM SDK for Java 8.0.

Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=984431
ArticleTitle=Java.next: Java 8 как язык Java.next
publish-date=09292014