Почему вам уже нужно перейти на Java 8

Опубликовано: 2022-03-11

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

В Java 8 появилось много новых функций. Я покажу вам несколько самых полезных и интересных:

  • Лямбда-выражения
  • Stream API для работы с Коллекциями
  • Асинхронная цепочка задач с CompletableFuture
  • Совершенно новый API времени

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

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

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

 class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }

Экземпляр этого класса может быть вызван в будущем в коде, где необходим этот компаратор, например:

 ... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...

Новый синтаксис лямбда позволяет нам сделать это проще. Вот простое лямбда-выражение, которое делает то же самое, что и метод compare из BinaryComparator :

 (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

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

JAVA 8 ЛЯМБДА ВЫРАЖЕНИЕ

Теперь мы можем улучшить наш предыдущий пример:

 ... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...

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

 Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

Теперь мы можем повторно использовать эту функциональность, например:

 ... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...

Обратите внимание, что в этих примерах лямбда-выражение передается методу sort() так же, как в предыдущем примере передается экземпляр BinaryComparator . Откуда JVM знает, как правильно интерпретировать лямбду?

Чтобы функции могли принимать лямбда-выражения в качестве аргументов, в Java 8 введена новая концепция: функциональный интерфейс . Функциональный интерфейс — это интерфейс, который имеет только один абстрактный метод. Фактически Java 8 рассматривает лямбда-выражения как специальную реализацию функционального интерфейса. Это означает, что для получения лямбда-выражения в качестве аргумента метода объявленный тип этого аргумента должен быть только функциональным интерфейсом.

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

 @FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }

Теперь мы можем вызвать метод sendDTO , передав разные лямбда-выражения для достижения различного поведения, например:

 sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));

Ссылки на методы

Аргументы лямбда позволяют нам изменять поведение функции или метода. Как видно из последнего примера, иногда лямбда служит только для вызова другого метода ( sendToAndroid или sendToIos ). Для этого особого случая в Java 8 введено удобное сокращение: ссылки на методы . Этот сокращенный синтаксис представляет лямбду, которая вызывает метод, и имеет форму objectName::methodName . Это позволяет нам сделать предыдущий пример еще более кратким и читабельным:

 sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);

В данном случае в this классе реализованы методы sendToAndroid и sendToIos . Мы также можем ссылаться на методы другого объекта или класса.

Потоковое API

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

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

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

 class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }

Давайте представим, что нам нужно напечатать всех авторов в коллекции books , которые написали книгу после 2005 года. Как бы мы это сделали в Java 7?

 for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }

И как бы мы это сделали в Java 8?

 books.stream() .filter(book -> book.year > 2005) // filter out books published in or before 2005 .map(Book::getAuthor) // get the list of authors for the remaining books .filter(Objects::nonNull) // remove null authors from the list .map(Author::getName) // get the list of names for the remaining authors .forEach(System.out::println); // print the value of each remaining element

Это только одно выражение! Вызов метода stream() для любой Collection возвращает объект Stream , инкапсулирующий все элементы этой коллекции. Этим можно управлять с помощью различных модификаторов из Stream API, таких как filter() и map() . Каждый модификатор возвращает новый объект Stream с результатами модификации, которыми можно в дальнейшем манипулировать. Метод .forEach() позволяет нам выполнять некоторые действия для каждого экземпляра результирующего потока.

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

Stream API помогает разработчикам взглянуть на коллекции Java под новым углом. Представьте теперь, что нам нужно получить Map доступных языков в каждой стране. Как это будет реализовано в Java 7?

 Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>(); for (Locale locale : Locale.getAvailableLocales()){ String country = locale.getDisplayCountry(); if (!countryToSetOfLanguages.containsKey(country)){ countryToSetOfLanguages.put(country, new HashSet<>()); } countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage()); }

В Java 8 все немного опрятнее:

 import java.util.stream.*; import static java.util.stream.Collectors.*; ... Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales()) .collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));

Метод collect() позволяет нам собирать результаты потока разными способами. Здесь мы видим, что он сначала группирует по странам, а затем отображает каждую группу по языку. ( groupingBy() и toSet() являются статическими методами класса Collectors .)

API ПОТОКА JAVA 8

Есть много других возможностей Stream API. Полную документацию можно найти здесь. Я рекомендую читать дальше, чтобы получить более глубокое представление обо всех мощных инструментах, которые может предложить этот пакет.

Асинхронная цепочка задач с CompletableFuture

В пакете Java 7 java.util.concurrent есть интерфейс Future<T> , который позволяет нам получить статус или результат какой-либо асинхронной задачи в будущем. Чтобы использовать эту функцию, мы должны:

  1. Создайте ExecutorService , который управляет выполнением асинхронных задач и может генерировать объекты Future для отслеживания их выполнения.
  2. Создайте асинхронно Runnable задачу.
  3. Запустите задачу в ExecutorService , которая предоставит Future , дающий доступ к статусу или результатам.

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

Однако в Java 8 концепция Future получила дальнейшее развитие благодаря интерфейсу CompletableFuture<T> , который позволяет создавать и выполнять цепочки асинхронных задач. Это мощный механизм для создания асинхронных приложений в Java 8, поскольку он позволяет нам автоматически обрабатывать результаты каждой задачи после завершения.

Давайте посмотрим пример:

 import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);

Метод CompletableFuture.supplyAsync создает новую асинхронную задачу в Executor по умолчанию (обычно ForkJoinPool ). Когда задача завершится, ее результаты будут автоматически предоставлены в качестве аргументов функции this::getLinks , которая также запускается в новой асинхронной задаче. Наконец, результаты этого второго этапа автоматически распечатываются в System.out . thenApply() и thenAccept() — это лишь два из нескольких полезных методов, которые помогут вам создавать параллельные задачи без ручного использования Executors .

CompletableFuture упрощает управление последовательностью сложных асинхронных операций. Допустим, нам нужно создать многошаговую математическую операцию с тремя задачами. Задание 1 и задание 2 используют разные алгоритмы для поиска результата для первого шага, и мы знаем, что только один из них будет работать, а другой — нет. Однако какой из них сработает, зависит от входных данных, о которых мы не знаем заранее. Результат выполнения этих заданий необходимо суммировать с результатом задания 3 . Таким образом, нам нужно найти результат либо задачи 1 , либо задачи 2 , и результат задачи 3 . Чтобы добиться этого, мы можем написать что-то вроде этого:

 import static java.util.concurrent.CompletableFuture.*; ... Supplier<Integer> task1 = (...) -> { ... // some complex calculation return 1; // example result }; Supplier<Integer> task2 = (...) -> { ... // some complex calculation throw new RuntimeException(); // example exception }; Supplier<Integer> task3 = (...) -> { ... // some complex calculation return 3; // example result }; supplyAsync(task1) // run task1 .applyToEither( // use whichever result is ready first, result of task1 or supplyAsync(task2), // result of task2 (Integer i) -> i) // return result as-is .thenCombine( // combine result supplyAsync(task3), // with result of task3 Integer::sum) // using summation .thenAccept(System.out::println); // print final result after execution

Если мы рассмотрим, как Java 8 справляется с этим, мы увидим, что все три задачи будут выполняться одновременно, асинхронно. Несмотря на то, что задача 2 завершилась ошибкой с исключением, окончательный результат будет успешно вычислен и напечатан.

АСИНХРОННОЕ ПРОГРАММИРОВАНИЕ JAVA 8 С CompletableFuture

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

API даты и времени Java

Как указано в собственном признании Java:

До выпуска Java SE 8 механизм даты и времени Java предоставлялся классами java.util.Date , java.util.Calendar и java.util.TimeZone , а также их подклассами, такими как java.util.GregorianCalendar . У этих классов было несколько недостатков, в том числе

  • Класс Calendar не был типобезопасным.
  • Поскольку классы были изменяемыми, их нельзя было использовать в многопоточных приложениях.
  • Ошибки в коде приложения были обычным явлением из-за необычной нумерации месяцев и отсутствия безопасности типов».

Java 8, наконец, решает эти давние проблемы с новым пакетом java.time , который содержит классы для работы с датой и временем. Все они неизменяемы и имеют API, похожие на популярный фреймворк Joda-Time, который практически все Java-разработчики используют в своих приложениях вместо нативных Date , Calendar и TimeZone .

Вот некоторые из полезных классов в этом пакете:

  • Clock — часы, показывающие текущее время, включая текущий момент, дату и время с часовым поясом.
  • Duration и Period — количество времени. Для параметра « Duration » используются значения на основе времени, такие как «76,8 секунды», и « Period » на основе даты, например «4 года, 6 месяцев и 12 дней».
  • Instant — мгновенный момент времени в нескольких форматах.
  • LocalDate , LocalDateTime , LocalTime , Year , YearMonth — дата, время, год, месяц или их комбинация без часового пояса в календарной системе ISO-8601.
  • OffsetDateTime , OffsetTime — дата-время со смещением от UTC/Greenwich в календарной системе ISO-8601, например «2015-08-29T14:15:30+01:00».
  • ZonedDateTime — дата-время со связанным часовым поясом в календарной системе ISO-8601, например «1986-08-29T10:15:30+01:00 Europe/Paris».

API ВРЕМЕНИ JAVA 8

Иногда нам нужно найти какую-то относительную дату, например «первый вторник месяца». Для этих случаев java.time предоставляет специальный класс TemporalAdjuster . Класс TemporalAdjuster содержит стандартный набор корректоров, доступных в виде статических методов. Это позволяет нам:

  • Найдите первый или последний день месяца.
  • Найти первый или последний день следующего или предыдущего месяца.
  • Найдите первый или последний день года.
  • Найдите первый или последний день следующего или предыдущего года.
  • Найдите первый или последний день недели в месяце, например «первая среда июня».
  • Найдите следующий или предыдущий день недели, например «следующий четверг».

Вот краткий пример, как получить первый вторник месяца:

 LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
Все еще используете Java 7? Получить с программой! #Java8
Твитнуть

Резюме Java 8

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

Кроме того, Java 8 предлагает некоторые новые функции для работы с асинхронным программированием и столь необходимую переработку инструментов для работы с датой и временем.

Вместе эти изменения представляют собой большой шаг вперед для языка Java, делая разработку на Java более интересной и эффективной.