Изключения в 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 решава проблема, но лаконичността на Lambda Expression се губи и това вече не е малка функция, както би трябвало да бъде.

За да се справим с този проблем, можем да напишем ламбда обвивка за ламбда функцията . Нека да разгледаме кода, за да видим как работи:

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 .

Също така забележете, че сме променили името на метода от 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)); }

Still, we get the same error of unhandled IOException during the compilation.

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

This is because lambda expressions are similar to Anonymous Inner Classes.

In our case, writeToFile method is the implementation of Consumer functional interface.

Let's take a look at the Consumer‘s definition:

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

As we can see accept method doesn't declare any checked exception. This is why writeToFile isn't allowed to throw the IOException.

The most straightforward way would be to use a try-catch block, wrap the checked exception into an unchecked exception and rethrow it:

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

This gets the code to compile and run. However, this approach introduces the same issue we already discussed in the previous section – it's verbose and cumbersome.

We can get better than that.

Let's create a custom functional interface with a single accept method that throws an exception.

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

And now, let's implement a wrapper method that's able to rethrow the exception:

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

Finally, we're able to simplify the way we use the writeToFile method:

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

This is still a kind of a workaround, but the end result looks pretty clean and is definitely easier to maintain.

Both, the ThrowingConsumer and the throwingConsumerWrapper are generic and can be easily reused in different places of our application.

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 си струва да проверите.