Some background on the 'why' of it all:
Closures in java are not transparent.
In java, closures (a -> foo(a)
or someExpr::someMethodRef
) are not transparent for the following 3 concepts:
- Checked exceptions: The list of checked exceptions a closure throws needs to be 'handled' (caught within the closure, or the method in the
@FunctionalInterface
that defines the type of the closure needs to be declared to throws
it) then and there, you cannot rely on a catch block, or a throws
clause, on the context where you made the closure. That is the problem you're running into here.
- non-effectively-final local vars: You cannot access (read or write) local variables from outside your lambda at all, unless they are either
final
, or they could be made final
without compiler errors, in which case javac
does you a favour and acts as if you put final
on it.
- control flow: You cannot write a
break
inside a closure that applies to a for
/while
/switch
/etc from outside of it. e.g. this does not work:
boolean banned = false;
for (String x : userNames) {
dbAccess.exec(db -> {
if (db.selectBool("SELECT banned FROM users WHERE un = ?", x)) {
banned = true; // won't compile - mutable local var
break; // won't compile - control flow
}
});
}
Why not?
Because that's actually what you want, if it is a 'travelling' closure. It's just annoying and weird for use-it-and-lose-it closures though. Thus, to make sense of this, consider the notion of a travelling closure, and then it makes sense.
Travelling vs. use-it-and-lose-it
A function is, by design, something you can 'ship around'. It can 'escape your context'. You are free to store a function in a field, or pass it to another method which stores it in a field. Then that field can be read by another thread 5 days from now (let's assume some very long running VM, e.g. a JVM serving web pages) and the code is then run.
In other words, given:
try {
// some code that _DEFINES_ a function, such as:
foo(a -> System.out.println(a));
} catch (Something e) {
}
There is no guarantee that this 'makes sense'. Perhaps foo
will take the closure, store it in a field, and run it 5 days from now - or, at any rate, runs it after the above code has long since completed. By completing that catch block and all context it needed to run is just gone. There is no way to run it! It sure feels like that catch block contains the error handler for when the code contained inside the try block throws it, but that is not how it works, given that the catch block no longer 'exists', in this scenario.
If your write code that makes no sense, it's a good idea if the compiler or runtime stops you from doing this. Better to get your error fast and with a good explanation, than to have to observe bizarre behaviour and go on a wild goose chase trying to figure out what you're missing, after all!
So javac
does just that and won't compile this code.
Even if that code makes perfect sense and would work exactly as you'd expected it to if that closure doesn't travel.
Thus, you can separate out closures into two camps:
- Use-it-or-lose-it closures: They are run (0 to many times) and then forgotten about within the lexical context where they are defined. Many usages of functions in java work like this, such as
list.stream().map(closureHere).filter(anotherClosureHere).collect()
, or list.sort(closureHere)
.
- travelling closures: They are stored and run later, and/or sent to other threads (even if those threads then run on the spot and the code can't continue until they're done, such as with fork/join: Those exceptions still aren't going to end up in your catch block. For example,
new TreeSet<>(comparatorHere)
, new Thread(runnableHere). (note how it doesn't depend on type; with
list.sort, we passed a
Comparatorwhich is use-it-and-lose-it, but with
new TreeSet, we _also_ passed a
Comparator` but that's a travelling closure.
It would have been fantastic if a method that accepts a function can declare whether it runs that function in 'use it and lose it' mode or in 'travelling' mode so that javac can add the appropriate sugar to give you transparency for checked exceptions, mutable locals, and control flow (and javac can conveniently also tell you if you then take that function ref and store it in a field or pass it to a method that isnt explicitly declared to treat it as use-it-and-lose-it). Maybe one day, because I'm pretty sure java can add this to the language in a backwards compatible fashion.
As a consequence, you should consider using closures for inline operations a minor evil. If you can write it just as well without, then do that (for loops and such are superior to streams if they're equally simple). For the same reason we write getters instead of direct field access, and for the same reason style guides tend to make braces mandatory even if javac
does not: You may not need any of the 3 transparencies today, but perhaps tomorrow you do, and it's annoying to have to rewrite at that point.