Почему вам уже нужно перейти на 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; Структура во многом похожа на функцию. В скобках мы устанавливаем список аргументов. Синтаксис -> показывает, что это лямбда. А в правой части этого выражения мы задаем поведение нашей лямбды.
Теперь мы можем улучшить наш предыдущий пример:
... 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 .)

Есть много других возможностей Stream API. Полную документацию можно найти здесь. Я рекомендую читать дальше, чтобы получить более глубокое представление обо всех мощных инструментах, которые может предложить этот пакет.
Асинхронная цепочка задач с CompletableFuture
В пакете Java 7 java.util.concurrent есть интерфейс Future<T> , который позволяет нам получить статус или результат какой-либо асинхронной задачи в будущем. Чтобы использовать эту функцию, мы должны:
- Создайте
ExecutorService, который управляет выполнением асинхронных задач и может генерировать объектыFutureдля отслеживания их выполнения. - Создайте асинхронно
Runnableзадачу. - Запустите задачу в
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 завершилась ошибкой с исключением, окончательный результат будет успешно вычислен и напечатан.
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».
Иногда нам нужно найти какую-то относительную дату, например «первый вторник месяца». Для этих случаев java.time предоставляет специальный класс TemporalAdjuster . Класс TemporalAdjuster содержит стандартный набор корректоров, доступных в виде статических методов. Это позволяет нам:
- Найдите первый или последний день месяца.
- Найти первый или последний день следующего или предыдущего месяца.
- Найдите первый или последний день года.
- Найдите первый или последний день следующего или предыдущего года.
- Найдите первый или последний день недели в месяце, например «первая среда июня».
- Найдите следующий или предыдущий день недели, например «следующий четверг».
Вот краткий пример, как получить первый вторник месяца:
LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }Резюме Java 8
Как мы видим, Java 8 — это эпохальный релиз платформы Java. Есть много языковых изменений, особенно с введением лямбда-выражений, что представляет собой шаг к тому, чтобы привнести в Java больше функциональных возможностей программирования. Stream API — хороший пример того, как лямбда-выражения могут изменить то, как мы работаем со стандартными инструментами Java, к которым мы уже привыкли.
Кроме того, Java 8 предлагает некоторые новые функции для работы с асинхронным программированием и столь необходимую переработку инструментов для работы с датой и временем.
Вместе эти изменения представляют собой большой шаг вперед для языка Java, делая разработку на Java более интересной и эффективной.
