Reputation: 25855
If a lambda expression does not refer to any methods or fields of the surrounding instance, does the language guarantee that it doesn't hold a reference to this
?
In particular, I want to use lambda expressions to implement java.lang.ref.Cleaner
actions. For example:
import static some.Global.cleaner;
public class HoldsSomeResource {
private final Resource res;
private final Cleanable cleanup;
public HoldsSomeResource(Resource res) {
this.res = res;
cleanup = cleaner.register(this, () -> res.discard());
}
public void discard() {
cleanup.clean();
}
}
Clearly, it would be bad if the lambda expression implementing the cleanup action were to hold a reference to this
, since it would then never become unreachable. It seems to work when I test it right now, but I can't find the obvious reference in the JLS that it is guaranteed to be safe, so I'm slightly worried that I might run into problems in alternative and/or future Java implementations.
Upvotes: 0
Views: 319
Reputation: 298409
The specification does indeed not mention this behavior, but there is a statement in this document from Brian Goetz:
References to
this
— including implicit references through unqualified field references or method invocations — are, essentially, references to afinal
local variable. Lambda bodies that contain such references capture the appropriate instance ofthis
. In other cases, no reference tothis
is retained by the object.
While this isn’t the official specification, Brian Goetz is the most authoritative person we can have to make such a statement.
This behavior of lambda expressions is as intentional as it can be. The cited text continues with
This has a beneficial implication for memory management: while inner class instances always hold a strong reference to their enclosing instance, lambdas that do not capture members from the enclosing instance do not hold a reference to it. This characteristic of inner class instances can often be a source of memory leaks.
Note that this other behavior, inner class instances always holding an implicit reference to the outer this
instance, also does not appear anywhere in the specification. So when even this behavior, causing more harm than good if ever being intentional, is taken for granted despite not appearing in the specification, we can be sure that the intentionally implemented behavior to overcome this issue will never be changed.
But if you’re still not convinced, you may follow the pattern shown in this answer or that answer of delegating to a static
method to perform the Cleaner
registration. This has the benefit of also preventing accidental use of members while still being simpler than the documentation’s suggested use of a nested static class
.
Upvotes: 3
Reputation: 103263
I think you're safe. It's not an aspect of the JIT or a garbage collector implementation (stuff from "java.exe
") ; this is done directly by the compiler ("javac.exe"
). It's not going to 'backslide' and inject useless and potentially pricey variables. It also means you are not dependent on a JVM's behaviour: you're merely dependent on a compiler's behaviour. For starters, there aren't all that many (ecj and javac that's pretty much it - all others you might be thinking of are forks of those, or are wrappers around those), and I'm pretty sure both ecj and javac don't capture the this
now and presumably never will in the future.
A bigger issue is that javac certainly won't complain if you 'accidentally' do happen to capture anything that requires the this
ref; that will lead to the this ref getting silently captured and ruining your cleanup library rather thoroughly. It feels like you've designed a library here where it's rather all too easy to shoot yourself in the foot.
I'm not quite sure what you can do to fix this. Possibly you can lean into it and use ASM or bytebuddy or similar to tear the class open1 and doublecheck that the this
ref is not seeing capture. It's probably not worth the potentially sizable time it'd take to chase down all the refs to ensure that this
isn't captured in a roundabout fashion (where the lambda captures variable y, and y has a field of type Bar pointing at some instance and that instance has a field whose value is a ref back to the original this
, thus, preventing collection), but checking for direct capture is potentially interesting. Possibly even only in an assert
statement so any testcase that does it will result in an AssertionError
thrown, failing the test, letting you know this error was made.
[1] You can get the bytes of any class with String.class.getResourceAsStream("String.class")
- you can read that InputStream
and feed it into ASM / bytebuddy / etc. The costs of running a class through such a loop are considerable, of course.
Upvotes: 1