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

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

1. обзор

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

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

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

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

У нас естьList<Integer>, и мы хотим разделить константу, скажем 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 решает проблему, но лаконичностьLambda Expression теряется, и это уже не маленькая функция, как предполагалось.

Для решения этой проблемы мы можем написатьa lambda wrapper for the lambda function. Давайте посмотрим на код, чтобы увидеть, как он работает:

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));

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

Также обратите внимание, что мы изменили имя метода сlambdaWrapper наconsumerWrapper. Это потому, что этот метод обрабатывает лямбда-выражения только дляFunctional Interface типаConsumer. Мы можем написать аналогичные методы оболочки для других функциональных интерфейсов, таких какFunction,BiFunction,BiConsumer и так далее.

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

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

static void writeToFile(Integer integer) throws IOException {
    // logic to write to file which throws 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. Выдача проверенного исключения из лямбда-выражений

Давайте выбросим исключение из метода, в котором записано лямбда-выражение, в данном случае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. Это потому, что лямбда-выражения похожи на анонимные внутренние классы. В данном случае лямбда-выражение является реализацией методаaccept(T t) из интерфейсаConsumer<T>.

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

Consumer consumer = new Consumer() {

    @Override
    public void accept(Integer integer) throws Exception {
        writeToFile(integer);
    }
};

Приведенный выше код не компилируется, потому что реализация методаaccept не может вызвать никаких исключений.

Самый простой способ - использовать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);
    }
});

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

Поскольку мы просто хотим вызвать исключение, нам нужно написать наш собственный функциональный интерфейс потребителя, который может вызвать исключение, а затем метод-обертку, использующий его. Назовем этоThrowingConsumer:

@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);
        }
    };
}

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

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

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

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

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);
            }
        }
    };
}

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

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

Подобно случаю непроверенных исключений, бросание братьев и сестер для других функциональных интерфейсов, таких какThowingFunction,ThrowingBiFunction,ThrowingBiConsumer и т. Д. могут быть написаны вместе с их соответствующими методами оболочки.

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

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

Другой способ -explore the sneaky-throws hack.

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

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