Home > Mobile >  Why Functional API in java does not handle checked exceptions?
Why Functional API in java does not handle checked exceptions?

Time:09-29

I saw many times that using the functional API in java is really verbose and error-prone when we have to deal with checked exceptions.

E.g: it's really convenient to write (and easier to read) code like

var obj = Objects.requireNonNullElseGet(something, Other::get);

Indeed, it also avoids to improper multiple invokation of getters, like when you do

var obj = something.get() != null ? something.get() : other.get();
//        ^^^^ first ^^^^          ^^^^ second ^^^^

BUT everything becomes a jungle when you have to deal with checked exceptions, and I saw sometimes this really ugly code style:

try {
  Objects.requireNonNullElseGet(obj, () -> {
    try {
      return invokeMethodWhichThrows();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  });
} catch (RuntimeException r){
  Throwable cause = r.getCause();
  if(cause == null)
    throw r;
  else
    throw cause;
}

which only intent is to handle checked exceptions like when you write code without lambdas. Now, I know that those cases can be better expressed with the ternary operator and a variable to hold the result of something.get(), but that's also the case for Objects.requireNonNullElse(a, b), which is there, in the java.util package of the JDK.

The same can be said for logging frameworks' methods which take Suppliers as parameters and evaluate them only if needed, BUT if you need to handle checked exceptions in those supplier you need to invoke them and explicitly check for the log level.

if(LOGGER.isDebugEnabled())
  LOGGER.debug("request from "   resolveIPOrThrow());

Some similar reasonament can be maid also for Futures, but let me go ahead.

My question is: why is Functional API in java not handling checked exceptions?

For example having something like a ThrowingSupplier interface, like the one below, can potentially fit the need of dealing with checked exceptions, guarantee type consistency and better code readability.

interface ThrowingSupplier<O, T extends Exception> {
  O get() throws T;
}

Then we need to duplicate methods that uses Suppliers to have an overload that uses ThrowingSuppliers and throws exceptions. But we as java developers have been used to this kind of duplication (like with Stream, IntStream, LongStream, or methods with overloads to handle int[], char[], long[], byte[], ...), so it's nothing too strange for us.

I would really appreciate if someone who has deep knowledge of the JDK argues about why checked exceptions have been excluded from the functional API, if there was a way to incorporate them.

CodePudding user response:

This question can be interpreted as 'why did those who made this decision decide it this way', which is asking: "Please summarize 5 years of serious debate - specifically what Brian Goetz and co thought about it", which is impossible, unless your name is Brian Goetz. He does not answer questions on SO as far as I know. You can go spelunking in de archives of the lambda-dev mailing list if you want.

One could make an informed guess, though.

In-scope vs Beyond-scope

There are 3 transparancies that lambdas do not have.

  1. Control flow.
  2. Checked exceptions.
  3. Mutable local variables.

Control flow transparency

Take this code, as an example:

private Map<String, PhoneNumber> phonebook = ...;

public PhoneNumber findPhoneNumberOf(String personName) {
  phonebook.entrySet().stream().forEach(entry -> {
    if (entry.getKey().equals(personName)) return entry.getValue();
  });
  return null;
}

This code is silly (why not just do a .get, or if we must stream through the thing, why not use .filter and .findFirst, but if you look past that, it doesn't even work: You cannot return the method from within that lambda. That return statement returns the lambda (and thus is a compiler error, the lambda you pass to forEach returns void). You can't continue or break a loop that is outside the lambda from inside it, either.

Contrast to a for loop that can do it just fine:

for (var entry : phonebook.entrySet()) {
  if (entry.getKey().equals(personName)) return entry.getValue();
}
return null;

does exactly what you think, and works fine.

Checked exception transparency

This is the one you are complaining about. This doesn't compile:

public void printFiles(Path... files) throws IOException {
  Arrays.stream(files).forEach(p -> System.out.println(Files.readString(p)));
}

The fact that the context allows you to throw IOExceptions doesn't help: The above does not compile, because 'can throw IOExceptions' as a status doesn't 'transfer' to the inside of the lambda.

There's a theme here: Rewrite it to a normal for loop and it compiles and works precisely the way you want to. So why, exactly, can't we make lambdas work the same way?

mutable local variables

This doesn't work:

int x = 0;
someList.stream().forEach(k -> x  );
System.out.println("Count: "   x);

You can neither modify local variables declared outside the lambda, nor even read them unless they are (effectively) final. Why not?

These are all GOOD things.. depending on scope layering

So far it seems really stupid that lambdas aren't transparent in these 3 regards. But it turns into a good thing in a slightly different context. Imagine instead of .stream().forEach something a little bit different:

class DoubleNullException extends Exception {} // checked!

public class Example {
  private TreeSet<String> words;

  public Example() throws DoubleNullException {
    int comparisonCount = 0;
    this.words = new TreeSet<String>((a, b) -> {
      comparisonCount  ;
      if (a == null && b == null) throw new DoubleNullException();
    });
    System.out.println("Comparisons performed: "   comparisonCount);
  }
}

Let's image the 3 transparencies did work. The above code makes use of two of them (tries to mutate comparisonCount, and tries to throw DoubleNullException from inside to outside).

The above code makes absolutely no sense. The compiler errors are very much desired. That comparator is not going to run until perhaps next week in a completely different thread. It runs whenever you add the second element to the set, which is a field, so who knows who is going to do that and which thread would do it. The constructor has long since ceased running - local vars are 'on the stack' and thus the local var has disappeared. Nevermind that the printing would always print 'comparisons made: 0' here, the statement 'comparisonCount :' would be trying to increment a memory position that no longer holds that variable at all.

Even if we 'fix' this (the compiler realizes that a local is used in a lambda and hoists it onto heap, this is what most other languages do), the code still makes no sense as a concept: That print statement wouldn't print. Also, that comparator can be called from multiple threads so... do we now allow volatile on our local vars? Quite the can of worms! In current java, a local variable cannot possibly suffer from thread concurrency synchronization issues because it is not possible to share the variable (you can share the object the variable points at, not the variable itself) with another thread.

The reason you ARE allowed to mess with (effectively) final locals is because you can just make a copy, and that's what the compiler does for you. Copies are fine - if nobody changes anything.

The exception similarly doesn't work: It's the code that calls thatSet.add(someElement) that would get the DoubleNullException. The fact that somebody wrote:

Example ex;
try {
  ex = new Example();
} catch (DoubleNullException e) {
  throw new WrappedEx(e);
}

ex.add(null);
ex.add(null); // BOOM

The line with the remark (BOOM) would throw the DoubleNullEx. It 'breaks' the checked exception rules: That line would compile (set.add doesn't throw DNEx), but isn't in a context where throwing DNEx is allowed. The catch block that is in the above snippet cannot ever run.

See how it all falls apart, and nothing makes sense?

The key clue is: What happens to the lambda? Is it 'transported'?

For some situations, you hand a lambda straight to a method, and that method has a 'use it and lose it' mentality: That method you handed the lambda to will run it 0, 1, or many times, but the key is: It runs it right then and there and once the method you handed the lambda to returns, that lambda is gone. The thing you handed the lambda to did not store it in a field or hand it to other code that stores it in a field, nor did that method transport the lambda to another thread.

In such cases (the method is use-it-then-lose-it), the transparencies would certainly be handy and wouldn't "break" anything.

But when the method you hand the lambda to does transport it to a field (such as the constructor of TreeSet which stores the passed comparator in a field, so that future .add calls can call it), the transparencies break down and make no sense.

Lambdas in java are for both and therefore the lack of transparency (in all 3 regards) actually makes sense. It's just annoying when you have a use-it-then-lose-it situation.

POTENTIAL FUTURE JAVA FIX: I've championed it before but so far, it fell on mostly deaf ears. Next time I see Brian I might bring it up again. Imagine an annotation or other marker you can stick on the parameter of a method that says: "I shall use it or lose it". The compiler will then ensure you do not transport it (the only thing the compiler will let you do with that param is call .invoke() on it. You can't call anything else, nor can you assign it or hand it to anything else unless you hand it to a method that also marked that parameter as @UseItOrLoseIt. Then the compiler can make the transparency happen with some tactical wrapping for control flow, and for checked exception flow, just by not complaining (checked exceptions are a figment of javac's imagination. The runtime does not have checked exceptions. Which is why scala, kotlin, and other runs-on-the-JVM languages can do it).

Actually THEY CAN!

As your question ends with - you can actually write O get() throws T. So why do the various functional interfaces, such as Supplier, not do this?

Mostly because it's a pain. I'm honestly not sure why e.g. list's forEach is not defined as:

public <T extends Throwable> forEach(ThrowingConsumer<? super E, ? super T> consumer) throws T {
  for (E elem : this) consumer.consume(elem);
}

Which would work fine and compile (with ThrowingConsumer having the obvious impl). Or even that Consumer as we have it is declared with the <O, T extends Exception> part.

It's a bit of a hassle. The way lambdas 'work' is that the compiler has to infer from context what functionalinterface you are implementing which notably includes having to bind all the generics out. Adding exception binding to this mix makes it even harder. IDEs tend to get a little confused if you're in the middle of writing code in a 'throwing lambda' and start red-underlining rather a lot, and auto-complete and the like is no help, because the IDE can't be useful in that context until it knows.

Lambdas as a system were also designed to backwards compatibly replace any existing usages of the concept, such as swing's ActionListener. Such listeners couldn't throw either, so having the interfaces in the java.util.function package be similar would be more familiar and slightly more java idiomatic, possibly.

The throws T solution would help but isn't a panacea. It solves, to an extent, the lack of checked exception transparency, but does nothing to solve either mutable local var transparency or control flow transparency. Perhaps the conclusion is simply: The benefits of doing it are more limited than you think, the costs are higher than you think. The cost/benefit analysis says: Bad idea, so it wasn't done.

  • Related