Java Stream API плохо работает с проверяемыми исключениями. В этой статье рассмотрим, что делать в таких ситуациях.
В Java есть проверяемые исключения (checked exceptions), которые нужно указывать в сигнатуре метода, и непроверяемые (unchecked exceptions) — не требующие этого. По сравнению с более ранними подходами концепция проверяемых исключений казалась революционной. Но с течением времени стало понятно, что эта идея не прижилась, и Java остался единственным распространенным языком программирования с проверяемыми исключениями. Например, в Kotlin все исключение непроверяемые.
Да и новые фичи Java не используют проверяемые исключения: в сигнатуре стандартных функциональных интерфейсов исключения не указаны. Использование лямбда-выражений в легаси коде часто приводит к громоздкому коду. Особенно это заметно в Stream API.
В этом посте я хотел бы рассмотреть варианты решения этих проблем.
Пример проблемного кода
Рассмотрим следующий пример кода:
Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(it -> new ForNamer().apply(it)) // 1
.forEach(System.out::println);
Этот код не компилируется — возникает ошибка о необходимости обработки проверяемого исключения
ClassNotFoundException
Для решения этой проблемы добавим try/catch.
Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(it -> {
try {
return Class.forName(it);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
})
.forEach(System.out::println);
Но добавление try/catch совсем не улучшает читаемость кода.
Инкапсулируем try/catch в класс
Для улучшения кода сделаем небольшой рефакторинг и создадим новый класс. IntelliJ IDEA даже предлагает использовать record:
var forNamer = new ForNamer(); // 1
Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(forNamer::apply) // 2
.forEach(System.out::println);
record ForNamer() implements Function<String, Class<?>> {
@Override
public Class<?> apply(String string) {
try {
return Class.forName(string);
} catch (ClassNotFoundException e) {
return null;
}
}
}
Создаем record.
Используем ее.
Попробуем использовать Lombok
Project Lombok — это процессор аннотаций, который генерирует дополнительный байт-код во время компиляции. Мы можем получить нужный результат, используя всего лишь одну аннотацию, без написания лишнего кода.
Project Lombok — библиотека Java, которая интегрируется с вашим IDE и инструментами сборки, улучшая вашу Java. Больше нет необходимости писать очередной геттер или метод equals. Используя одну аннотацию, вы можете получить полнофункциональный builder, автоматизировать логирование и сделать многое другое.
Например Lombok-аннотация @SneakyThrow
позволяет использовать проверяемые исключения без объявления их в сигнатуре метода. Но в настоящее время она не работает для Stream API. На GitHub есть соответствующий issue.
Commons Lang спешит на помощь
Apache Commons Lang — проект с давней историей. В свое время он был весьма популярен, предлагая утилиты, которые могли бы быть частью Java API, но не были. Гораздо удобнее использовать готовое решение, чем заново изобретать DateUtils
и StringUtils
в каждом проекте. При написании этого поста я обнаружил, что проект по прежнему жив, и там есть на что посмотреть. Например, Failable API.
Этот API состоит из двух частей:
Обертка вокруг
Stream
.Конвейерные методы, сигнатура которых допускает исключения.
Вот небольшой фрагмент:
Код, наконец, становится таким, каким мы ожидали увидеть его с самого начала:
Stream<String> stream = Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList");
Failable.stream(stream)
.map(Class::forName) // 1
.forEach(System.out::println);
Исправление ошибок компиляции недостаточно
Предыдущий код бросает в рантайме ClassNotFoundException
, обернутый в UndeclaredThrowableException
. Все компилируется без ошибок, но мы не можем определить нужное поведение:
Выбросить первое исключение.
Проигнорировать исключения.
Собрать классы и исключения, чтобы обработать их на последнем этапе конвейера.
Сделать что-то другое.
Для этого мы можем использовать силу Vavr. Vavr — это библиотека, которая привносит мощь функционального программирования в язык Java:
Vavr — это функциональная библиотека для Java. Она помогает уменьшить объем кода и повысить ошибкоустойчивость. Первый шаг к функциональному программированию — начать думать иммутабельными значениями. Vavr предоставляет иммутабельные коллекции и соответствующие функции с управляющими структурами для работы с ними.
Представьте, что нам нужен конвейер, который собирает как исключения, так и классы. Вот выдержка из API, описывающая компоненты, которые можно использовать для этого:
Пример кода:
Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(CheckedFunction1.liftTry(Class::forName)) // 1
.map(Try::toEither) // 2
.forEach(e -> {
if (e.isLeft()) { // 3
System.out.println("not found:" + e.getLeft().getMessage());
} else {
System.out.println("class:" + e.get().getName());
}
});
Оборачиваем вызов в Vavr
Try
.Преобразуем
Try
в Either, чтобы сохранить исключение. Если не нужно, то можно использовать Optional.Действуем в зависимости от того, содержит ли
Either
исключение (left) или ожидаемый результат (right).
Мы все еще остаемся в мире Java Stream. Все работает, как и ожидается, но forEach
выглядит не очень красиво.
Vavr предоставляет собственный класс Stream
, который имитирует Stream
из Java Stream API, но с дополнительной функциональностью. Давайте воспользуемся им, чтобы переписать пайплайн:
var result = Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(CheckedFunction1.liftTry(Class::forName))
.map(Try::toEither)
.partition(Either::isLeft) // 1
.map1(left -> left.map(Either::getLeft)) // 2
.map2(right -> right.map(Either::get)); // 3
result._1().forEach(it -> System.out.println("not found: " + it.getMessage())); // 4
result._2().forEach(it -> System.out.println("class: " + it.getName())); // 4
Разделяем
Stream
изEither
на кортеж из двухStream
.Преобразуем левый
Stream
изEither
вStream
изThrowable
.Преобразуем правый
Stream
изEither
вStream
изClass
.Далее делаем все, что нам нужно.
Выводы
Изначально в Java широко использовались проверяемые исключения. Однако эволюция языков программирования показала, что это была плохая идея.
Java Stream API плохо работает с проверяемыми исключениями. Код, использующий проверяемые исключения совместно со Stream API выглядит не очень хорошо. Для улучшения читаемости кода, ради чего мы и используем Stream API, можно использовать Apache Commons Lang.
Часто необходимо исключения обрабатывать, а не игнорировать их или останавливать пайплайн. В этом случае можно использовать библиотеку Vavr, которая предлагает более функциональный подход.
Исходный код примеров можно найти на GitHub.
Скоро состоится открытый урок «Архитектурные концепции построения систем обмена сообщений», на котором рассмотрим стили интеграции (File Transfer, RPI, Shared Database, Messaging), а также основные концепции обмена сообщениями. Регистрируйтесь по ссылке.
Megaprog
Я как-то написал небольшую библиотечку, облегчающую обработку checked exceptions. Достаточно просто обернуть лямбду в
StreamUtil.unchecked
https://github.com/Megaprog/stream-util/blob/master/src/main/java/org/jmmo/util/StreamUtil.java#L184sergey-gornostaev
Наверное, каждый джавист такую писал.
BugM
А каждый хороший джавит потом ее еще и закапывал и переходил на Either.