Blog-Archiv

Dienstag, 6. Juni 2023

Java Lambdas Must Not Catch Exceptions

This article shows a way how to get around Java compile errors resulting from lambdas that throw checked exceptions. You may also want to read a stackoverflow article about this.

Java lambdas were introduced in Java 8, but the lambdas in package java.util.function do not provide exception management. That means, if you implement a lambda calling a method that declares an exception, the Java compiler will report an error "Unhandled exception type ...", like here on line 10:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.function.Function;

public class LambdaThrowingException0
{
    public static void main(String[] args) {
        new LambdaThrowingException0().work();
    }

    private void work() {
        loop((bar) -> fooFail(bar)); // compile error "Unhandled exception type Exception"
    }

    private void loop(Function<String,String> toCall) {
        for (String bar : new String [] { "one", "two", "three" })
            toCall.apply(bar);
    }
    
    private String fooFail(String bar) throws Exception {
        System.out.println("fooFail("+bar+")");
        throw new Exception("Sometimes things go wrong!");
    }
}

The compile-error on line 10 happens because method fooFail() on line 18 declares a checked exception.

In a conventional lambda like Supplier, Consumer, Predicate, Function, you can call just methods that do not throw checked exceptions. That can become quite an obstacle in case the called method is not your own source code.

There are different ways to get around this. The following is about a solution that wraps any checked exception into a RuntimeException and then rethrows that. RuntimeExceptions don't need to be declared in method signatures and thus are allowed in lambdas too.

Workaround

To overcome this you can implement a functional interface that declares the exception:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/** Describes a function that throws some kind of exception. */
@FunctionalInterface
public interface FunctionThrowsException<P, R, E extends Exception> 
{
    R applyThrowingException(P parameter) throws E;
    
    default R apply(P parameter) {
        try {
            return applyThrowingException(parameter);
        }
        catch (Exception e) { // checked exception
            throw new RuntimeException(e); // unchecked exception
        }
    }
}

On line 5 the (one-and-only) abstract method of a functional interface is declared. This is what will be implemented by the lambda. As interfaces can contain also default-implementations since Java 8, an apply() method has been implemented to delegate to applyThrowingException() and catch its exception, wrapping it into a RuntimeException and rethrowing that.

Then you must declare this functional interface in the method receiving the lambda, which is loop() in the example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class LambdaThrowingException1
{
    public static void main(String[] args) {
        new LambdaThrowingException1().work();
    }

    private void work() {
        loop((bar) -> fooFail(bar));
    }

    private void loop(FunctionThrowsException<String,String,Exception> toCall) {
        for (String bar : new String [] { "one", "two", "three" })
            toCall.apply(bar);
    }
    
    private String fooFail(String bar) throws Exception {
        System.out.println("fooFail("+bar+")");
        throw new Exception("Sometimes things go wrong!");
    }
}

The functional interface is now in the parameter-list of loop() on line 11.

  • Mind that you must call apply() inside the loop on line 13, not applyThrowingException()!
    Unfortunately both have to be be public because they are part of an interface.

That's all, the compile error is gone.

Accepts Normal Functions Too

Following example implements one lambda that throws an exception (line 9), and another one that does not (line 8). You can use both with FunctionThrowsException:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class LambdaThrowingException2
{
    public static void main(String[] args) {
        new LambdaThrowingException2().work();
    }

    private void work() {
        loop((bar) -> fooSuccess(bar));
        loop((bar) -> fooFail(bar));
    }

    private void loop(FunctionThrowsException<String,String,Exception> toCall) {
        for (String bar : new String [] { "one", "two", "three" })
            toCall.apply(bar);
    }
    
    private String fooSuccess(String bar) {
        System.out.println("fooSuccess("+bar+")");
        return bar;
    }
    
    private String fooFail(String bar) throws Exception {
        System.out.println("fooFail("+bar+")");
        throw new Exception("Sometimes things go wrong!");
    }
}

Here is the output of this Java app:

fooSuccess(one)
fooSuccess(two)
fooSuccess(three)
fooFail(one)
Exception in thread "main" java.lang.RuntimeException: java.lang.Exception: Sometimes things go wrong!
	at FunctionThrowsException.apply(FunctionThrowsException.java:12)
	at LambdaThrowingException2.loop(LambdaThrowingException2.java:14)
	at LambdaThrowingException2.work(LambdaThrowingException2.java:9)
	at LambdaThrowingException2.main(LambdaThrowingException2.java:4)
Caused by: java.lang.Exception: Sometimes things go wrong!
	at LambdaThrowingException2.fooFail(LambdaThrowingException2.java:24)
	at LambdaThrowingException2.lambda$1(LambdaThrowingException2.java:9)
	at FunctionThrowsException.apply(FunctionThrowsException.java:9)
	... 3 more

The application terminated on the first thrown exception, there is just one "Sometimes things go wrong" message. Thus the only difference to a normal exception is that the error cause got wrapped into an unchecked RuntimeException.




Keine Kommentare: