I'm working on project with Java 8 and found one situation which I can't understand.
I have code like this:
void deleteEntity(Node node) throws SomeException {
for (ChildNode child: node.getChildren()) {
deleteChild(child);
}
}
void deleteChild(Object child) throws SomeException {
//some code
}
This code is working fine, but I can rewrite it with a method reference:
void deleteEntity(Node node) throws SomeException {
node.getChildren().forEach(this::deleteChild);
}
And this code doesn't compile, giving the error Incompatible thrown types *SomeException* in method reference
.
Also IDEA gave me the error unhandled exception
.
So, my question is why? Why code compiles with for each loop and doesn't compile with lambda?
If you look at the Consumer<T>
interface, the accept
method (which is what your method reference would effectively be using) isn't declared to throw any checked exceptions - therefore you can't use a method reference which is declared to throw a checked exception. The enhanced for loop is okay, because there you're always in a context where SomeException
can be thrown.
You could potentially create a wrapper which converts the checked exception to an unchecked exception, and throw that. Alternatively, you could declare your own functional interface with an accept()
method which does throw a checked exception (probably parameterizing the interface with that exception), and then write your own forEach
method that takes that functional interface as an input.
You may try this:
void deleteEntity(Node node) throws SomeException { node.getChildren().forEach(UtilException.rethrowConsumer(this::deleteChild)); }
The UtilException
helper class below lets you use any checked exceptions in Java streams. Note the stream above also throws the original checked exception thrown by this::deleteChild
, and NOT some wrapping unchecked exception.
public final class UtilException { @FunctionalInterface public interface Consumer_WithExceptions<T, E extends Exception> { void accept(T t) throws E; } @FunctionalInterface public interface BiConsumer_WithExceptions<T, U, E extends Exception> { void accept(T t, U u) throws E; } @FunctionalInterface public interface Function_WithExceptions<T, R, E extends Exception> { R apply(T t) throws E; } @FunctionalInterface public interface Supplier_WithExceptions<T, E extends Exception> { T get() throws E; } @FunctionalInterface public interface Runnable_WithExceptions<E extends Exception> { void run() throws E; } /** .forEach(rethrowConsumer(name -> System.out.println(Class.forName(name)))); or .forEach(rethrowConsumer(ClassNameUtil::println)); */ public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) throws E { return t -> { try { consumer.accept(t); } catch (Exception exception) { throwAsUnchecked(exception); } }; } public static <T, U, E extends Exception> BiConsumer<T, U> rethrowBiConsumer(BiConsumer_WithExceptions<T, U, E> biConsumer) throws E { return (t, u) -> { try { biConsumer.accept(t, u); } catch (Exception exception) { throwAsUnchecked(exception); } }; } /** .map(rethrowFunction(name -> Class.forName(name))) or .map(rethrowFunction(Class::forName)) */ public static <T, R, E extends Exception> Function<T, R> rethrowFunction(Function_WithExceptions<T, R, E> function) throws E { return t -> { try { return function.apply(t); } catch (Exception exception) { throwAsUnchecked(exception); return null; } }; } /** rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))), */ public static <T, E extends Exception> Supplier<T> rethrowSupplier(Supplier_WithExceptions<T, E> function) throws E { return () -> { try { return function.get(); } catch (Exception exception) { throwAsUnchecked(exception); return null; } }; } /** uncheck(() -> Class.forName("xxx")); */ public static void uncheck(Runnable_WithExceptions t) { try { t.run(); } catch (Exception exception) { throwAsUnchecked(exception); } } /** uncheck(() -> Class.forName("xxx")); */ public static <R, E extends Exception> R uncheck(Supplier_WithExceptions<R, E> supplier) { try { return supplier.get(); } catch (Exception exception) { throwAsUnchecked(exception); return null; } } /** uncheck(Class::forName, "xxx"); */ public static <T, R, E extends Exception> R uncheck(Function_WithExceptions<T, R, E> function, T t) { try { return function.apply(t); } catch (Exception exception) { throwAsUnchecked(exception); return null; } } @SuppressWarnings ("unchecked") private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E { throw (E)exception; } }
Many other examples on how to use it (after statically importing UtilException
):
@Test public void test_Consumer_with_checked_exceptions() throws IllegalAccessException { Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String") .forEach(rethrowConsumer(className -> System.out.println(Class.forName(className)))); Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String") .forEach(rethrowConsumer(System.out::println)); } @Test public void test_Function_with_checked_exceptions() throws ClassNotFoundException { List<Class> classes1 = Stream.of("Object", "Integer", "String") .map(rethrowFunction(className -> Class.forName("java.lang." + className))) .collect(Collectors.toList()); List<Class> classes2 = Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String") .map(rethrowFunction(Class::forName)) .collect(Collectors.toList()); } @Test public void test_Supplier_with_checked_exceptions() throws ClassNotFoundException { Collector.of( rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))), StringJoiner::add, StringJoiner::merge, StringJoiner::toString); } @Test public void test_uncheck_exception_thrown_by_method() { Class clazz1 = uncheck(() -> Class.forName("java.lang.String")); Class clazz2 = uncheck(Class::forName, "java.lang.String"); } @Test (expected = ClassNotFoundException.class) public void test_if_correct_exception_is_still_thrown_by_method() { Class clazz3 = uncheck(Class::forName, "INVALID"); }
But don't use it before understanding the following advantages, disadvantages, and limitations:
• If the calling-code is to handle the checked exception you MUST add it to the throws clause of the method that contains the stream. The compiler will not force you to add it anymore, so it's easier to forget it.
• If the calling-code already handles the checked exception, the compiler WILL remind you to add the throws clause to the method declaration that contains the stream (if you don't it will say: Exception is never thrown in body of corresponding try statement).
• In any case, you won't be able to surround the stream itself to catch the checked exception INSIDE the method that contains the stream (if you try, the compiler will say: Exception is never thrown in body of corresponding try statement).
• If you are calling a method which literally can never throw the exception that it declares, then you should not include the throws clause. For example: new String(byteArr, "UTF-8") throws UnsupportedEncodingException, but UTF-8 is guaranteed by the Java spec to always be present. Here, the throws declaration is a nuisance and any solution to silence it with minimal boilerplate is welcome.
• If you hate checked exceptions and feel they should never be added to the Java language to begin with (a growing number of people think this way, and I am NOT one of them), then just don't add the checked exception to the throws clause of the method that contains the stream. The checked exception will, then, behave just like an UNchecked exception.
• If you are implementing a strict interface where you don't have the option for adding a throws declaration, and yet throwing an exception is entirely appropriate, then wrapping an exception just to gain the privilege of throwing it results in a stacktrace with spurious exceptions which contribute no information about what actually went wrong. A good example is Runnable.run(), which does not throw any checked exceptions. In this case, you may decide not to add the checked exception to the throws clause of the method that contains the stream.
• In any case, if you decide NOT to add (or forget to add) the checked exception to the throws clause of the method that contains the stream, be aware of these 2 consequences of throwing CHECKED exceptions:
1) The calling-code won't be able to catch it by name (if you try, the compiler will say: Exception is never thrown in body of corresponding try statement). It will bubble and probably be catched in the main program loop by some "catch Exception" or "catch Throwable", which may be what you want anyway.
2) It violates the principle of least surprise: it will no longer be enough to catch RuntimeException to be able to guarantee catching all possible exceptions. For this reason, I believe this should not be done in framework code, but only in business code that you completely control.
In conclusion: I believe the limitations here are not serious, and the UtilException
class may be used without fear. However, it's up to you!
Please note that parallel stream will continue executing the elements though there is exception thrown.
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;
public class ThrowingConsumerTest {
public static void main(String[] args) throws IOException {
List<Integer> myIntegerList = new ArrayList<>();
myIntegerList.add(1);
myIntegerList.add(2);
myIntegerList.add(3);
myIntegerList.add(null);
myIntegerList.add(4);
myIntegerList.add(5);
myIntegerList.add(6);
myIntegerList.add(7);
myIntegerList.add(8);
myIntegerList.add(9);
myIntegerList.add(10);
myIntegerList.add(11);
myIntegerList.add(12);
myIntegerList.add(13);
myIntegerList.add(14);
myIntegerList.add(15);
myIntegerList.add(16);
myIntegerList.add(17);
myIntegerList.add(18);
myIntegerList.add(19);
forEach(myIntegerList.stream(), ThrowingConsumerTest::exceptionThrowingConsumerCode);
}
/**
* Wrapper that converts Checked Exception to Runtime Exception
*/
static <T, E extends Exception> Consumer<T> unchecked(ThrowingConsumer<T, E> consumer) {
return (t) -> {
try {
consumer.accept(t);
} catch (Throwable e) {
//Lambda can return only RuntimeException.
RuntimeException ex = new RuntimeException();
ex.addSuppressed(e);
throw ex;
}
};
}
/**
* Wrapper that converts Runtime Exception to Checked Exception
* Custom forEach; to accept the exception throwing consumer.
*/
@SuppressWarnings("unchecked")
static <T, E extends Exception> void forEach(Stream<T> s, ThrowingConsumer<T, E> consumer) throws E {
try {
s.parallel().forEach(unchecked(t -> consumer.accept(t)));
} catch (RuntimeException e) {
//Checked Exception can be return from here
throw (E) e.getSuppressed()[0];
}
}
/*
* Consumer that throws Exception
*/
@FunctionalInterface
public interface ThrowingConsumer<T, E extends Exception> {
void accept(T t) throws E;
}
static void exceptionThrowingConsumerCode(Object i) throws IOException {
if (i == null) {
throw new IOException();
} else {
System.out.println(i);
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With