Содержание


Изменения в языке Java 8

Лямбда-выражения и изменения в классах интерфейсов делают Java 8 новым языком

Comments

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

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

В этой статье вы увидите, как использовать лямбда-выражения в различных ситуациях, и узнаете о связанных с ними расширениях определений interface языка Java. В сопутствующей статье Java 8 concurrency basics из цикла JVM concurrency приведены дополнительные примеры работы с лямбда-выражениями, включая их совместное использование с т. н. потоками Java 8 (stream).

Понятие о лямбда-выражениях

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

В простом примере в листинге 1 лямбда-выражение используется для сортировки экземпляров Name. Первый блок кода в методе main() использует анонимный внутренний класс для реализации интерфейса Comparator<Name>, а второй блок кода использует лямбда-выражение (в разделе Ресурсы приведена ссылка на полный учебный код для этой статьи).

Листинг 1. Сравнение лямбда-выражения и анонимного внутреннего класса
public class Name {
    public final String firstName;
    public final String lastName;

    public Name(String first, String last) {
        firstName = first;
        lastName = last;
    }

    //  необходимо только для связанного в цепочку компаратора (chained comparator)
    public String getFirstName() {
        return firstName;
    }

    // необходимо только для chained comparator
    public String getLastName() {
        return lastName;
    }

    // необходимо только для direct comparator (а не для chained comparator)
    public int compareTo(Name other) {
        int diff = lastName.compareTo(other.lastName);
        if (diff == 0) {
            diff = firstName.compareTo(other.firstName);
        }
        return diff;
    }
    ...
}

public class NameSort {
    
    private static final Name[] NAMES = new Name[] {
        new Name("Sally", "Smith"),
        ...
    };
    
    private static void printNames(String caption, Name[] names) {
        ...
    }

    public static void main(String[] args) {

        // сортировка массива с помощью анонимного внутреннего класса
        Name[] copy = Arrays.copyOf(NAMES, NAMES.length);
        Arrays.sort(copy, new Comparator<Name>() {
            @Override
            public int compare(Name a, Name b) {
                return a.compareTo(b);
            }
        });
        printNames("Names sorted with anonymous inner class:", copy);

        // сортировка массива с помощью лямбда-выражения
        copy = Arrays.copyOf(NAMES, NAMES.length);
        Arrays.sort(copy, (a, b) -> a.compareTo(b));
        printNames("Names sorted with lambda expression:", copy);
        ...
    }
}

В листинге 1 лямбда-выражение используется в качестве замены для обычного анонимного внутреннего класса. Эта разновидность обычного внутреннего класса весьма широко распространена на практике, поэтому лямбда-выражения обеспечивают немедленный выигрыш программистам на Java 8 (В данном случае для выполнения работы по сравнению и внутренний класс, и лямбда-выражение использует метод, реализованный в классе Name. Если код метода compareTo() был встроен в лямбда-выражение, то выражение будет менее лаконичным).

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

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

  • Function: Принимает единственный параметр, возвращает результат на основе значения параметра.
  • Predicate: Принимает единственный параметр, возвращает булев результат на основе значения параметра.
  • BiFunction: Принимает два параметра, возвращает результат на основе значения параметра.
  • Supplier: Не принимает параметров, возвращает результат.
  • Consumer: Принимает единственный параметр, не возвращает результата (void)

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

Листинг 2. Сочетание предикатов
// использование сочетания предикатов для удаления соответствующих имен
List<Name> list = new ArrayList<>();
for (Name name : NAMES) {
    list.add(name);
}
Predicate<Name> pred1 = name -> "Sally".equals(name.firstName);
Predicate<Name> pred2 = name -> "Queue".equals(name.lastName);
list.removeIf(pred1.or(pred2));
printNames("Names filtered by predicate:", list.toArray(new Name[list.size()]));

В листинге 2 код определяет две конструкции вида Predicate<Name>, одна из которых соответствует имени Sally, а вторая соответствует фамилии Queue. вызов метода pred1.or(pred2) создает объединенный предикат, определенный посредством применения этих двух предикатов по порядку, с возвращением значения true, если любой из этих двух предикатов имеет значение true (с досрочным выходом, как в случае логического оператора || в Java). Метод List.removeIf() применяет этот объединенный предикат для удаления соответствующих имен из списка.

В Java 8 определено много полезных комбинаций интерфейсов java.util.function однако эти комбинации не являются согласованными. Все вариации предикатов (DoublePredicate, IntPredicate, LongPredicate и Predicate<T>) определяют одни и те же методы для сочетания и модификации: and(), negate(), or(). Однако вариации примитивов для Function<T> не определяют методов сочетания и модификации. Если у вас есть опыт работы с языками функционального программирования, то вы можете посчитать эти различия и пропуски избыточными.

Изменения в классах interface

В Java 8 изменилась структура классов interface (таких как Comparator, использованный в листинге 1), отчасти для упрощения употребления лямбда-выражений. До версии Java 8 интерфейсы позволяли определять только константы и абстрактные методы, которые вы затем должны были реализовать. В Java 8 добавлена возможность определять в интерфейсах static-методы и default-методы. Static-методы в интерфейсе — это по существу то же самое, что static-методы в абстрактном классе. Default-методы больше походят на методы интерфейса в старом стиле, но имеют предоставленную реализацию, которая используется в том случае, если разработчик не переопределяет соответствующий default-метод.

Одна важная особенность default-методов состоит в том, что они могут быть добавлены к существующему классу interface без ущерба для совместимости с другим кодом, который использует этот интерфейс (при условии, что ваш существующий код не использует то же самое имя метода в иных целях). Это весьма мощная возможность; проектировщики Java8 использовали ее, чтобы "подстроить" поддержку лямбда-выражений ко многим уже существующим Java-библиотекам. В листинге 3 показан соответствующий пример — в форме третьего способа сортировки имен, добавленного к коду в листинге 1.

Листинг 3. Связывание в цепочку компараторов на основе выражений key-extractor
// сортировка массива с помощью лямбда-выражений key-extractor
copy = Arrays.copyOf(NAMES, NAMES.length);
Comparator<Name> comp = Comparator.comparing(name -> name.lastName);
comp = comp.thenComparing(name -> name.firstName);
Arrays.sort(copy, comp);
printNames("Names sorted with key extractor comparator:", copy);

Код в листинге 3 сначала демонстрирует применение нового static-метода Comparator.comparing() для создания компаратора на основе определяемого разработчиком лямбда-выражения key-extraction (в техническом смысле лямбда-выражение key-extraction является экземпляром интерфейса java.util.function.Function<T,R>, где тип результирующего компаратора совместим по присвоению с T, а тип извлеченного ключа R реализует интерфейс Comparable). Этот код также демонстрирует объединение компараторов с помощью нового default-метода Comparator.thenComparing(), который в листинге 3 возвращает новый компаратор, осуществляющий сортировку сначала по фамилии, а затем по имени.

У вас может сложиться впечатление о возможности встраивания следующей comparator-конструкции:

Comparator<Name> comp = Comparator.comparing(name -> name.lastName)
    .thenComparing(name -> name.firstName);

К сожалению, такая конструкция не работает с интерфейсом типа Java 8. Вам необходимо предоставить компилятору дополнительную информацию об ожидаемом типе результата static-метода — с помощью любой из следующих форм:

Comparator<Name> com1 = Comparator.comparing((Name name1) -> name1.lastName)
    .thenComparing(name2 -> name2.firstName);
Comparator<Name> com2 = Comparator.<Name,String>comparing(name1 -> name1.lastName)
    .thenComparing(name2 -> name2.firstName);

Первая форма добавляет тип лямбда-параметра к лямбда-выражению: (Name name1) -> name1.lastName. С этой помощью компилятор способен понять остальную часть того, что ему необходимо сделать. Вторая форма сообщает компилятору типы T и R для функционального интерфейса (который в данном случае реализован с помощью лямбда-выражения), переданные в метод comparing().

Возможность простого конструирования компараторов и связывания их в цепочку — это весьма полезная особенность Java 8, однако она реализуется ценой дополнительного повышения уровня сложности. В языке Java 7 интерфейс Comparator определяет два метода (метод compare() и вездесущий метод equals(), который гарантированно определен для каждого объекта). Версия Java 8 определяет 18 методов (два исходных метода, плюс 9 новых static-методов и 7 новых default-методов). Вы увидите, что эта модель масштабной инфляции интерфейсов для работы с лямбда-выражениями повторяется в значительной части стандартной Java-библиотеки.

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

Если существующий у вас метод уже делает все, что вам нужно, то вы можете воспользоваться механизмом method reference (ссылка на метод) для непосредственной передачи этого метода. Указанный подход иллюстрируется в листинге 4.

Листинг 4. Использование существующих методов в качестве лямбда-выражений
...
// сортировка массива с помощью существующих методов в качестве лямбда-выражений
copy = Arrays.copyOf(NAMES, NAMES.length);
comp = Comparator.comparing(Name::getLastName).thenComparing(Name::getFirstName);
Arrays.sort(copy, comp);
printNames("Names sorted with existing methods as lambdas:", copy);

Код в листинге 4 делает то же самое, что код в листинге 3, но с использованием существующих методов. Вы можете воспользоваться имеющимся в Java 8 синтаксисом ссылки на метод ClassName::methodName, чтобы задействовать любой метод таким же образом, как если бы он был лямбда-выражением. Результат будет в точности таким же, как в случае определения лямбда-выражения, которое вызывает этот метод. Вы можете использовать ссылки на метод для static-методов, для instance-методов любого определенного объекта или в качестве входного типа для лямбда-выражений (как в листинге 4, где методы getFirstName() и getLastName()— это instance-методы для типа сравниваемых Name) и для конструкторов.

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

Лямбда-выражения категории capturing и noncapturing

Все лямбда-выражений, которые вы до настоящего момента увидели в этой статье, относятся к категории noncapturing, другими словами, это простые выражения, использующие только значения, переданные в них как эквивалент параметров метода интерфейса. Лямбда-выражения категории capturing в Java 8 используют значения из окружающего контекста. Лямбда-выражения категории capturing подобны замыканиям, используемым в некоторых других языках JVM (включая Scala), но отличаются в том, что в Java 8 любые значения из окружающего контекста должны быть effectively final. Таким образом, значение или должно быть действительно final (какими в более ранних версиях Java должны были быть значения, на которые производится ссылка из анонимных внутренних классов), или никогда не должно подвергаться модифицированию внутри контекста. Этот критерий применяется к значениям, которые используются и лямбда-выражениями, и анонимными внутренними классами.

Чтобы преодолеть ограничения типа "effectively final", можно использовать определенные обходные маневры. К примеру, чтобы использовать только текущие значения определенных переменных в лямбда-выражении, вы можете добавить новый метод, который принимает значения в качестве параметров и возвращает лямбда-выражение (в форме соответствующей ссылки на интерфейс) с захваченными (captured) значениями. Если вы хотите, чтобы лямбда-выражение изменило значение из окружающего контекста, вы можете обернуть соответствующее значение в виде допускающего изменения держателя (holder).

Лямбда-выражения категории noncapturing потенциально могут обрабатываться более эффективно, чем лямбда-выражения категории capturing, поскольку компилятор может генерировать их как static-методы в охватывающем классе, а магия этапа исполнения способна встраивать вызовы непосредственно. Лямбда-выражения категории capturing могут быть менее эффективным, однако лямбда-выражение этой категории должно функционировать, как минимум, на уровне анонимного внутреннего класса в этом же контексте.

Лямбда-выражения — что происходит за сценой

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

Вместо использования для лямбда-выражений отдельного файла класса версия Java 8 опирается на байткод-инструкцию invokedynamic, добавленную в версии Java 7. Инструкция invokedynamic ориентирована на bootstrap-метод, который, в свою очередь, создает реализацию лямбда-выражения при первом вызове этого метода. После этого осуществляется непосредственный вызов возвращенной реализации. Это позволяет избежать накладных расходов в виде пространства, потребляемого отдельным файлом для каждого класса, и значительной части накладных расходов на этапе исполнения, обуславливаемых загрузкой класса. Точно таким же образом реализация лямбда-функции откладывается до момента bootstrap. Bootstrap-код, в настоящее время генерируемый Java 8, собирает на этапе исполнения новый класс для лямбда-выражения, однако в будущих реализациях вполне могут быть использованы и другие подходы.

Версия Java 8 включает в себя средства оптимизации, благодаря которым реализация лямбда-выражений посредством инструкции invokedynamic хорошо работает в практических условиях. Большинство других JVM-языков, в том числе Scala (2.10.x), использует для замыканий сгенерированные компилятором внутренние классы. Будущие версии этих языков, вероятно, перейдут к подходу на основе инструкции invokedynamic с целью использования средств оптимизации, предлагаемых версией Java 8 (и последующими версиями).

Ограничения лямбда-выражений

Как говорилось в начале статьи, лямбда-выражения всегда являются реализациями какого-либо определенного функционального интерфейса. Лямбда-выражения можно передавать только как ссылки на интерфейс; как и в случае других реализаций интерфейса, лямбда-выражение можно использовать только в качестве определенного интерфейса, для чего оно и было создано. Код в листинге 5 демонстрирует ограничения пары идентичных (за исключением имен) функциональных интерфейсов. Компилятор Java 8 принимает метод String::length как лямбда-реализацию обоих этих интерфейсов. Однако после того как лямбда-выражение определено в качестве экземпляра первого интерфейса, оно уже не может быть использовано в качестве экземпляра второго интерфейса.

Листинг 5. Ограничения лямбда-выражений
private interface A {
    public int valueA(String s);
}
private interface B {
    public int valueB(String s);
}
public static void main(String[] args) {
    A a = String::length;
    B b = String::length;

    // Ошибка компилятора!
    // b = a;

    // ClassCastException на этапе исполнения!
    // b = (B)a;

    // работает с использованием ссылки на метод
    b = a::valueA;
    System.out.println(b.valueB("abc"));
}

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

В языках функционального программирования для определения переменных используются не интерфейсы, а типы функций. Обычным явлением для таких языков является работа с т. н. функциями высшего порядка— это функции, которые передают функции в качестве параметров или возвращают функции в качестве значений. Результатом является гораздо более гибкий стиль программирования, чем в случае лямбда-выражений, включая возможность использования функций в качестве стандартных блоков для составления других функций. Версия Java 8 не определяет типы функций, поэтому вы не сможете составлять лямбда-выражений таким образом. Вы сможете составлять интерфейсы (см. листинг 3), но только с кодом, написанным для работы с определенными задействованными интерфейсами. В одном только новом пакете java.util.function43 интерфейса настроены специально для использования с лямбда-выражениями. С учетом сотен уже существующих интерфейсов можно сказать, что перечень способов, посредством которых можно составлять интерфейсы, всегда будет строго ограниченным.

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

Заключение

Лямбда-выражения представляют собой весьма значительное расширение языка Java. Как и другое столь же значительное расширение — ссылки на методы — они быстро станут необходимым инструментом для всех Java-разработчиков — по мере переноса их приложений на платформу Java 8. Лямбда-выражения особенно полезны в сочетании с т. н. потоками Java 8 (stream). В статье JVM concurrency: Java 8 concurrency basics показано совместное использование лямбда-выражений и потоков с целью упрощения параллельного программирования и повышения производительности приложения.


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


Похожие темы

  • Оригинал статьи: Java 8 language changes.
  • Демонстрационный программный код для данной статьи: Загрузите полный демонстрационный код для данной статьи из репозитария ее автора на сайте GitHub.
  • Lambda Expressions (Лямбда-выражения). В этом разделе на сайте Java Tutorials объясняются подробности работы с лямбда-выражениями в различных контекстах.
  • Lambda: A Peek Under the Hood: Презентация на конференции JavaOne 2013, в которой архитектор языка Java и регулярный автор сайта IBM developerWorks Java Брайан Гец (Brian Goetz) объясняет логику разработки и реализации лямбда-выражений в Java 8.
  • Programming with Lambda Expressions in Java: Выступление на конференции JavaOne 2013: живая демонстрация написания программного кода с примерами лямбда-выражений (ведущий: Venkat Subramaniam).
  • JVM concurrency: Java 8 concurrency basics (Денис Сосноски (Dennis Sosnoski), developerWorks, апрель 2014 г.). В статье демонстрируется совместное использование лямбда-выражений и потоков с целью упрощения параллельного программирования и повышения производительности приложения.
  • Записная книжка дизайнера языка: в этом цикле статей developerWorks Брайан Гец исследует некоторые проблемы дизайна языка, вызвавшие трудности при эволюции языка Java версий Java SE 7, Java SE 8 и далее.
  • IBM SDK, Java Technology Edition Version 8: Примите участие в программе бета-тестирования IBM SDK for Java 8.0.
  • IBM Java developer kits: Найдите пакет IBM Java SDK и среду исполнения для своей платформы.

Комментарии

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

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