Почему вам уже нужно перейти на 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 более интересной и эффективной.