Исключения в лямбда-выражениях Java

Исключения в лямбда-выражениях Java, image #1

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);
  1. Этот код не компилируется — возникает ошибка о необходимости обработки проверяемого исключения 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;
        }
    }
}
  1. Создаем record.
  2. Используем ее.

Попробуем использовать Lombok

Project Lombok — это процессор аннотаций, который генерирует дополнительный байт-код во время компиляции. Мы можем получить нужный результат, используя всего лишь одну аннотацию, без написания лишнего кода.

Project Lombok — библиотека Java, которая интегрируется с вашим IDE и инструментами сборки, улучшая вашу Java. Больше нет необходимости писать очередной геттер или метод equals. Используя одну аннотацию, вы можете получить полнофункциональный builder, автоматизировать логирование и сделать многое другое.

Project Lombok

Например Lombok-аннотация @SneakyThrow позволяет использовать проверяемые исключения без объявления их в сигнатуре метода. Но в настоящее время она не работает для Stream API. На GitHub есть соответствующий issue.

Commons Lang спешит на помощь

Apache Commons Lang — проект с давней историей. В свое время он был весьма популярен, предлагая утилиты, которые могли бы быть частью Java API, но не были. Гораздо удобнее использовать готовое решение, чем заново изобретать DateUtils и StringUtils в каждом проекте. При написании этого поста я обнаружил, что проект по прежнему жив, и там есть на что посмотреть. Например, Failable API.

Этот API состоит из двух частей:

  1. Обертка вокруг Stream.
  2. Конвейерные методы, сигнатура которых допускает исключения.

Вот небольшой фрагмент:

Исключения в лямбда-выражениях Java, image #2

Код, наконец, становится таким, каким мы ожидали увидеть его с самого начала:

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 предоставляет иммутабельные коллекции и соответствующие функции с управляющими структурами для работы с ними.

Vavr

Представьте, что нам нужен конвейер, который собирает как исключения, так и классы. Вот выдержка из API, описывающая компоненты, которые можно использовать для этого:

Исключения в лямбда-выражениях Java, image #3

Пример кода:

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());
          }
      });
  1. Оборачиваем вызов в Vavr Try.
  2. Преобразуем Try в Either, чтобы сохранить исключение. Если не нужно, то можно использовать Optional.
  3. Действуем в зависимости от того, содержит ли 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
  1. Разделяем Stream из Either на кортеж из двух Stream.
  2. Преобразуем левый Stream из Either в Stream из Throwable.
  3. Преобразуем правый Stream из Either в Stream из Class.
  4. Далее делаем все, что нам нужно.

Выводы

Изначально в Java широко использовались проверяемые исключения. Однако эволюция языков программирования показала, что это была плохая идея.

Java Stream API плохо работает с проверяемыми исключениями. Код, использующий проверяемые исключения совместно со Stream API выглядит не очень хорошо. Для улучшения читаемости кода, ради чего мы и используем Stream API, можно использовать Apache Commons Lang.

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

Исходный код примеров можно найти на GitHub.

Источник

783 views·54 shares