Перейти к основному содержимому

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

· 6 мин. чтения

1. Обзор

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

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

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

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

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

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

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

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

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

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

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

static <T, E extends Exception> Consumer<T>
consumerWrapper(Consumer<T> consumer, Class<E> 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<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(
consumerWrapper(
i -> System.out.println(50 / i),
ArithmeticException.class));

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

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

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

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

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

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

List<Integer> 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 в основном методе:

public static void main(String[] args) throws IOException {
List<Integer> 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<Integer> .

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

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

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

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

List<Integer> 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<T, E extends Exception> {
void accept(T t) throws E;
}

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

static <T> Consumer<T> throwingConsumerWrapper(
ThrowingConsumer<T, Exception> throwingConsumer) {

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

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

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

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

И ThrowingConsumer , и throwingConsumerWrapper являются универсальными и могут быть легко повторно использованы в разных местах нашего приложения.

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

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

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

static <T, E extends Exception> Consumer<T> handlingConsumerWrapper(
ThrowingConsumer<T, E> throwingConsumer, Class<E> 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<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(handlingConsumerWrapper(
i -> writeToFile(i), IOException.class));

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

4. Вывод

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

Другим способом было бы изучить хак скрытных бросков.

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

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