Исключения в лямбда-выражениях Java
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.
