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. Конвейерные методы, сигнатура которых допускает исключения.

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

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

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, описывающая компоненты, которые можно использовать для этого:

Пример кода:

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.


Скоро состоится открытый урок «Архитектурные концепции построения систем обмена сообщений», на котором рассмотрим стили интеграции (File Transfer, RPI, Shared Database, Messaging), а также основные концепции обмена сообщениями. Регистрируйтесь по ссылке.

Комментарии (4)


  1. Megaprog
    05.11.2022 08:08

    Я как-то написал небольшую библиотечку, облегчающую обработку checked exceptions. Достаточно просто обернуть лямбду в StreamUtil.unchecked https://github.com/Megaprog/stream-util/blob/master/src/main/java/org/jmmo/util/StreamUtil.java#L184


    1. sergey-gornostaev
      05.11.2022 11:58
      +3

      Наверное, каждый джавист такую писал.


      1. BugM
        05.11.2022 15:04

        А каждый хороший джавит потом ее еще и закапывал и переходил на Either.


  1. Artyomcool
    07.11.2022 08:24
    -1

    Плохо зарекомендовали себя не исключения, а Stream API.