⬆️ ⬇️

Generic exceptions in lambda functions

UPD: Added an example with lazy calculations on top of standard streams.



As is known from the functional interfaces in the Stream API, you cannot throw controlled exceptions. If for some reason this is necessary (for example, working with files, databases, or over the network), you have to wrap them in a RuntimeException. This works well if errors are ignored, but if they need to be processed, the code is cumbersome and hard to read. I wondered whether it was possible to declare interfaces and methods with generic exceptions and suddenly found out for myself what was possible.



Let us define such a functional interface, it differs from the standard Function <A, B> interface only in the presence of a third generic type for the thrown exception.

')

public interface FunctionWithExceptions<A, B, T extends Throwable>{ B apply(A a) throws T; } 


And we declare a simple method that converts the collection using this interface, this method also declared a generic type for the thrown exception (which coincides with the type of exception that the functional interface can throw out).



 public static <A, B, T extends Throwable> Collection<B> map(Collection<A> source, FunctionWithExceptions<A, B, T> function) throws T { Collection<B> result = new ArrayList<>(); for (A a : source) { result.add(function.apply(a)); } return result; } 


Let's see what exception handling will look like with them in different cases.



One exception



Let's try to convert the collection using the lambda function that throws out the exception being processed, at the expense of the generic type, it will be correctly transferred to the place where the map method is called. The type of exception will be saved.



 public Collection<byte[]> singleException(Collection<String> filenames) throws IOException { return map(filenames, f -> Files.readAllBytes(new File(f).toPath()); } 


Two exceptions in one lambda function



If we use a function that throws out a few processed exceptions, then they will be reduced to the most general type, which is not very good (but, in my opinion, not worse than throwing exceptions into a RuntimeException).



 public static byte[] waitAndRead(String filename, long time) throws InterruptedException, IOException { Thread.sleep(time); return Files.readAllBytes(new File(filename).toPath()); } public Collection<byte[]> joinedExceptions(Collection<String> filenames) throws Exception { return map(filenames, f -> waitAndRead(f, 1000L)); } 


Exceptions in different lambda functions



However, if we write a chain of lambda functions, each of which throws no more than one exception, then of course they will not be combined and can be correctly processed separately.



 private <T> T wait(T t, long time) throws InterruptedException { Thread.sleep(time); return t; } private byte[] read(String filename) throws IOException { return Files.readAllBytes(new File(filename).toPath()); } public Collection<byte[]> separatedExceptions(Collection<String> filenames) throws InterruptedException { try { return map(map(filenames, f -> wait(f, 1000L)), f -> read(f)); } catch (IOException e) { return Collections.emptyList(); } } 


As you can see in this example in IOException, we intercept and return an empty collection, and pass InterruptedException above.



Lambda function without exceptions



And finally, let's see how the function will behave which does not throw out the controlled exceptions, will it require to handle the exception of which is not present?



 public Collection<Boolean> noExceptions(Collection<String> filenames) { return Mapper.map(filenames, f -> new File(f).exists()); } 


Everything works great and there is no need to handle exceptions. It is interesting that, at the same time, the generic type of the exception was automatically revealed in RuntimeException, which in principle is logical, but a bit unexpected.



disadvantages



The main disadvantage of the approach described above is incompatibility with the Stream API due to the inability to use interfaces with generic exceptions instead of standard ones. You can potentially write the ThrowableStream API by analogy to StreamEx or extend StreamEx, but this will require writing a large amount of trivial code. The second drawback is that you cannot declare more than one generic exception.



By the way, using exceptions in classes with generic types is possible on earlier versions of Java (checked at 1.7), but there it is inconvenient and therefore rather pointless.



UPD:

Lazy computing



To support lazy computing, we create such a wrapper around the standard stream (for example, only two methods, the rest are implemented in the same way).



 public class ErStream<S, T extends Throwable> { private final Stream<S> mainStream; public ErStream(Stream<S> mainStream) { this.mainStream = mainStream; } public static <S, R, T extends Throwable, T1 extends T, T2 extends T> ErStream<R, T> map(ErStream<S, T1> erStream, FunctionWithExceptions<S, R, T2> function) { Function<S, R> f = uncheck(function); return new ErStream<>(erStream.mainStream.map(f)); } public static <S, T extends Throwable> Optional<S> findAny(ErStream<S, T> erStream) throws T { return erStream.mainStream.findAny(); } //  Djaler private static <S, R> Function<S, R> uncheck(FunctionWithExceptions<S, R, ?> function) { return t -> { try { return function.apply(t); } catch (Throwable exception) { throwAsUnchecked(exception); return null; } }; } //  Djaler @SuppressWarnings("unchecked") private static <E extends Throwable> void throwAsUnchecked(Throwable exception) throws E { throw (E) exception; } 


And we use it as follows (for compactness, all the examples in one block of code):



 private static byte[] read(String filename) throws EOFException { return null; } private static String search(String filename) throws FileNotFoundException { return filename; } public static void main(String[] args) { List<String> list = Arrays.asList("1.txt"); //autogenerated code with wrong generic types //ErStream<Object, Throwable> temp = map(new ErStream<>(list.stream()), s -> search(s)); //store single exception ErStream<String, FileNotFoundException> temp = map(new ErStream<>(list.stream()), s -> search(s)); try {//exception will be thrown here findAny(temp); } catch (FileNotFoundException e) {/*NOTHING*/} temp = map(new ErStream<>(list.stream()), s -> search(s)); try {//most general exception will be thrown here findAny(map(temp,s -> read(s))); } catch (IOException e) {/*NOTHING*/} //without exception, nothing thrown findAny(map(new ErStream<>(list.stream()), i -> i + 1)); } 


In this example, the error is thrown out of the terminal operation and ErStream can be saved to a variable (unfortunately, instead of the correct generic types, Eclipse 4.6.3 displays the most common types <Object, Throwable> and have to specify them yourself) or transferred to another function. It is also correct to derive a common type for combining several exceptions only in static methods. Also, while saving ErStream which does not generate an error in the generic-type local variable, an error will be a RuntimeException that when combined with other errors, it will output a general type as Exception (there is no such problem when writing functions into a chain).



Link to git with source code and junit test in which situations similar to those described in the article are checked.

Source: https://habr.com/ru/post/341524/



All Articles