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 .