Рубрики
Без рубрики

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

Краткое и практическое руководство по работе с лямбда-выражениями и исключениями

Автор оригинала: baeldung.

1. Обзор

В Java 8 лямбда-выражения начали облегчать функциональное программирование, предоставляя краткий способ выражения поведения. Однако функциональные интерфейсы , предоставляемые JDK, не очень хорошо справляются с исключениями, и код становится многословным и громоздким, когда дело доходит до их обработки.

В этой статье мы рассмотрим некоторые способы борьбы с исключениями при написании лямбда-выражений.

2. Обработка Непроверенных Исключений

Во-первых, давайте разберемся в проблеме на примере.

У нас есть List , и мы хотим разделить константу, скажем, 50 на каждый элемент этого списка, и распечатать результаты:

List integers = Arrays.asList(3, 9, 7, 6, 10, 20);
integers.forEach(i -> System.out.println(50 / i));

Это выражение работает, но есть одна проблема. Если какой-либо из элементов в списке 0 , тогда мы получаем ArithmeticException:/by zero . Давайте исправим это с помощью традиционного блока try-catch , чтобы мы регистрировали любое такое исключение и продолжали выполнение для следующих элементов:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
    try {
        System.out.println(50 / i);
    } catch (ArithmeticException e) {
        System.err.println(
          "Arithmetic Exception occured : " + e.getMessage());
    }
});

Использование try-catch решает проблему, но краткость лямбда-выражения теряется, и это уже не маленькая функция, как должно быть.

Чтобы справиться с этой проблемой, мы можем написать лямбда-оболочку для лямбда-функции . Давайте посмотрим на код, чтобы увидеть, как он работает:

static Consumer lambdaWrapper(Consumer consumer) {
    return i -> {
        try {
            consumer.accept(i);
        } catch (ArithmeticException e) {
            System.err.println(
              "Arithmetic Exception occured : " + e.getMessage());
        }
    };
}
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(lambdaWrapper(i -> System.out.println(50 / i)));

Сначала мы написали метод-оболочку, который будет отвечать за обработку исключения, а затем передали лямбда-выражение в качестве параметра этому методу.

Метод оболочки работает так, как ожидалось, но вы можете возразить, что он в основном удаляет блок try-catch из лямбда-выражения и перемещает его в другой метод, и это не уменьшает фактическое количество строк записываемого кода.

Это верно в данном случае, когда оболочка специфична для конкретного случая использования, но мы можем использовать универсальные средства для улучшения этого метода и использовать его для множества других сценариев:

static  Consumer
  consumerWrapper(Consumer consumer, Class clazz) {
 
    return i -> {
        try {
            consumer.accept(i);
        } catch (Exception ex) {
            try {
                E exCast = clazz.cast(ex);
                System.err.println(
                  "Exception occured : " + exCast.getMessage());
            } catch (ClassCastException ccEx) {
                throw ex;
            }
        }
    };
}
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(
  consumerWrapper(
    i -> System.out.println(50 / i), 
    ArithmeticException.class));

Как мы видим, эта итерация нашего метода-оболочки принимает два аргумента: лямбда-выражение и тип Исключения , который должен быть пойман. Эта лямбда-оболочка способна обрабатывать все типы данных, а не только Целые числа , и улавливать любой конкретный тип исключения, а не суперкласс Исключение .

Кроме того, обратите внимание, что мы изменили имя метода с lambda Wrapper на consumer Wrapper . Это связано с тем, что этот метод обрабатывает только лямбда-выражения для Функционального интерфейса типа Потребитель . Мы можем написать аналогичные методы обертки для других функциональных интерфейсов, таких как Function , BiFunction , BiConsumer и так далее.

3. Обработка Проверенных Исключений

Давайте изменим пример из предыдущего раздела и вместо печати на консоль запишем в файл.

static void writeToFile(Integer integer) throws IOException {
    // logic to write to file which throws IOException
}

Обратите внимание, что приведенный выше метод может вызвать исключение IOException.

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> writeToFile(i));

При компиляции мы получаем ошибку:

java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException

Поскольку IOException является проверяемым исключением, мы должны обрабатывать его явно . У нас есть два варианта.

Во-первых, мы можем просто выбросить исключение за пределы нашего метода и позаботиться о нем в другом месте.

В качестве альтернативы мы можем обработать его внутри метода, который использует лямбда-выражение.

Давайте рассмотрим оба варианта.

3.1. Выбрасывание проверенного исключения из лямбда-выражений

Давайте посмотрим, что произойдет, когда мы объявим IOException в методе main :

public static void main(String[] args) throws IOException {
    List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
    integers.forEach(i -> writeToFile(i));
}

Тем не менее, мы получаем ту же ошибку необработанного IOException во время компиляции .

java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException

Это связано с тем, что лямбда-выражения похожи на Анонимные внутренние классы .

В нашем случае метод writeToFile является реализацией Consumer функционального интерфейса .

Давайте взглянем на определение Consumer :

@FunctionalInterface
public interface Consumer {
    void accept(T t);
}

Как мы видим, метод accept не объявляет никаких проверенных исключений. Вот почему writeToFile не может вызывать исключение IOException.

Самым простым способом было бы использовать блок try-catch , обернуть проверенное исключение в непроверенное исключение и перестроить его:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
    try {
        writeToFile(i);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
});

Это позволяет скомпилировать и запустить код. Однако этот подход вводит ту же проблему, которую мы уже обсуждали в предыдущем разделе, – он многословен и громоздок.

Мы можем добиться большего.

Давайте создадим пользовательский функциональный интерфейс с одним accept методом, который вызывает исключение.

@FunctionalInterface
public interface ThrowingConsumer {
    void accept(T t) throws E;
}

А теперь давайте реализуем метод-оболочку, который способен переосмыслить исключение:

static  Consumer throwingConsumerWrapper(
  ThrowingConsumer throwingConsumer) {
 
    return i -> {
        try {
            throwingConsumer.accept(i);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    };
}

Наконец, мы можем упростить способ использования метода writeToFile :

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(throwingConsumerWrapper(i -> writeToFile(i)));

Это все еще своего рода обходной путь, но конечный результат выглядит довольно чистым и определенно легче поддерживать .

Как Бросающий потребитель , так и растущая потребительская обертка являются общими и могут быть легко повторно использованы в разных местах нашего приложения.

3.2. Обработка проверенного исключения в лямбда-выражении

В этом заключительном разделе мы изменим оболочку для обработки проверенных исключений.

Поскольку ваш интерфейс Throwing Consumer использует дженерики, мы можем легко обработать любое конкретное исключение.

static  Consumer handlingConsumerWrapper(
  ThrowingConsumer throwingConsumer, Class exceptionClass) {
 
    return i -> {
        try {
            throwingConsumer.accept(i);
        } catch (Exception ex) {
            try {
                E exCast = exceptionClass.cast(ex);
                System.err.println(
                  "Exception occured : " + exCast.getMessage());
            } catch (ClassCastException ccEx) {
                throw new RuntimeException(ex);
            }
        }
    };
}

Давайте посмотрим, как использовать его на практике:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(handlingConsumerWrapper(
  i -> writeToFile(i), IOException.class));

Обратите внимание, что приведенный выше код обрабатывает только IOException, в то время как любой другой вид исключения переосмысливается как RuntimeException .

4. Заключение

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

Другим способом было бы исследовать хитрый хак.

Полный исходный код функционального интерфейса и методов-оболочек можно загрузить из здесь и тестовых классов из здесь, на Github .

Если вы ищете готовые рабочие решения, Функция Метания проект стоит проверить.