Search code examples
javagenericsexceptionjava-streamchecked-exceptions

How does trick with rethrow checked exceptions works from the point of view of Java language?


I have encountered that I must catch all checked exceptions inside stream expresions. I have read very popular topic:

How can I throw CHECKED exceptions from inside Java 8 streams?
And there is answer which suugest following approach:
we have 3 strings: "java.lang.Object" "java.lang.Integer" "java.lang.Strin"

We want to load classes by name.
Thus we need to use Class#forName method
Class#forName java doc)

As you can see following method declaration contains

throws ClassNotFoundException

Thus if we use usual loop we need to write followig code:

public void foo() throws ClassNotFoundException {
    String arr[] = new String[]{"java.lang.Object", "java.lang.Integer", "java.lang.Strin"};
    for(String className:arr){
        Class.forName(className);
    }
}

lets try to rewrite it using streams:

 public void foo() throws ClassNotFoundException {
        String arr[] = new String[]{"java.lang.Object", "java.lang.Integer", "java.lang.Strin"};
        Stream.of(arr).forEach(Class::forName);
    }

compiler says that:

Error:(46, 32) java: incompatible thrown types java.lang.ClassNotFoundException in method reference

Ok, lets try to use approach from topic mentioned above:

we create following methods:

public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) {
    return t -> {
        try {
            consumer.accept(t);
        } catch (Exception exception) {
            throwAsUnchecked(exception);
        }
    };
}

 @FunctionalInterface
 public interface Consumer_WithExceptions<T, E extends Exception> {
     void accept(T t) throws E;
 }

@SuppressWarnings("unchecked")
private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E {
    throw (E) exception;
}

And client code will looks like this:

public void foo() throws ClassNotFoundException {
        String arr[] = new String[]{"java.lang.Object", "java.lang.Integer", "java.lang.Strin"};
        Stream.of(arr).forEach(rethrowConsumer(Class::forName));
    }

I want to know how does it work. It is really unclear for me.

lets research rethrowConsumer:

public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) {
    return t -> {
        try {
            consumer.accept(t);
        } catch (Exception exception) {
            throwAsUnchecked(exception);
        }
    };
}

and throwAsUnchecked signature look slike

private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E {

Please clarify this mess.

P.S.

I localized that magic happens inside throwAsUnchecked because following snippet correct

   public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) {
        return t -> {
            try {
                throw new Exception();
            } catch (Exception exception) {
                throwAsUnchecked(exception);
            }
        };
    }

But following not

public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) {
    return t -> {
        try {
            consumer.accept(t);
        } catch (Exception exception) {
            throw new Exception();
            //throwAsUnchecked(exception);
        }
    };
}

Solution

  • The Java compiler understands that Class.forName() throws a checked exception. And that needs to somehow be handled. But when catching that checked exception and making it look like it gets wrapped into a RuntimeException for example - you resolve the compilation problem. By taking away the "thing" that the compiler complains about.

    Please note: the compiler does not understand that invoking throwAsUnchecked(exception) will always throw up. In other words: by making a method call here, there is actually no code inside that method body that must throw. Of course this doesn't change the runtime situation. In the end, there is still an exception thrown - and surprisingly not a RuntimeException, but an instance of ClassNotFoundException.

    The core point is that <E extends Throwable> translates into a RuntimeException (for the compiler). And that part is explained here!

    And because of that, the second version of foo() can be written like:

    public void foo() { ...
    

    there is no more need to declare throws ClassNotFoundException!

    In other words: this approach works by "tricking" the compiler into thinking that the exception that gets thrown is an unchecked one. And that trick itself is rooted in generics/type-erasure.