Винятки в Java 8 Lambda Expressions

1. Огляд

У Java 8 Lambda Expressions почали сприяти функціональному програмуванню, забезпечуючи стислий спосіб вираження поведінки. Однак функціональні інтерфейси, надані JDK, не дуже добре справляються з винятками - і код стає багатослівним і громіздким, коли справа доходить до їх обробки.

У цій статті ми розглянемо кілька способів усунення винятків при написанні лямбда-виразів.

2. Обробка неперевірених винятків

Спочатку розберемося в проблемі на прикладі.

У нас є Список, і ми хочемо розділити константу, скажімо 50, з кожним елементом цього списку і надрукувати результати:

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

Цей вираз працює, але є одна проблема. Якщо будь-який із елементів у списку дорівнює 0 , то ми отримуємо ArithmeticException: / за нулем . Давайте виправте це, використовуючи традиційний блок 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));

Як ми бачимо, ця ітерація нашого методу обгортки приймає два аргументи - лямбда-вираз та тип винятку, який потрібно вловити. Ця лямбда-обгортка здатна обробляти всі типи даних, а не лише цілі числа, і вловлювати будь-який конкретний тип винятків, а не виняток суперкласу .

Також зауважте, що ми змінили назву методу з lambdaWrapper на consumerWrapper . Це тому, що цей метод обробляє лише лямбда-вирази для функціонального інтерфейсу типу Consumer . Ми можемо писати подібні методи обгортки для інших функціональних інтерфейсів, таких як 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 за основним методом:

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 - це реалізація споживчого функціонального інтерфейсу .

Давайте поглянемо на визначення споживача :

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

Це все ще своєрідне обхідне рішення, але кінцевий результат виглядає досить чистим і, безумовно, простіший у підтримці .

І ThrowingConsumer, і throwingConsumerWrapper є загальними і їх можна легко використовувати повторно в різних місцях нашого додатку.

3.2. Handling a Checked Exception in Lambda Expression

In this final section, we'll modify the wrapper to handle checked exceptions.

Since our ThrowingConsumer interface uses generics, we can easily handle any specific exception.

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

Let's see how to use it in practice:

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

Note, that the above code handles only IOException, whereas any other kind of exception is rethrown as a RuntimeException .

4. Conclusion

In this article, we showed how to handle a specific exception in lambda expression without losing the conciseness with the help of wrapper methods. We also learned how to write throwing alternatives for the Functional Interfaces present in JDK to either throw or handle a checked exception.

Another way would be to explore the sneaky-throws hack.

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

Якщо ви шукаєте нестандартні робочі рішення, варто перевірити проект ThrowingFunction.