JDK-8300691 - final variables in for loop headers should accept updates

Maurizio Cimadamore maurizio.cimadamore at oracle.com
Wed Oct 23 10:14:19 UTC 2024


On 22/10/2024 19:35, Archie Cobbs wrote:

> This made me realize this issue really is all just about naming. For 
> example, outer instances are captured just like variables, but we 
> don't have a problem there because the captured version of the 
> variable has a different name inside the nested class vs. outside 
> (i.e., "Outer.this" vs. "outer", if you create the nested class via 
> "outer.new Inner()").

I think this observation hits the nails on the head.

When we capture “fields” we don’t really capture mutable boxes, we 
capture an (immutable) reference to the enclosing class.

I say immutable because the enclosing class reference is snapshotted, at 
construction - e.g. if you have:

|Outer outer = new Outer(); // 1 Outer.Inner inner = outer.new Inner(); 
outer = new Outer(); // 2 |

This is all legal code. One might wonder: does this code mean that 
Inner’s enclosing instance is updated in (2)? No, because, really, it’s 
as if the value of “outer” was snapshotted when we created Inner, and a 
reference to that snapshotted value was saved. Since we access that 
snapshotted reference using |Outer.this| (and not |outer|), we sort of 
preserve the illusion that snapshotting doesn’t occur here - the class 
is accessing its “enclosing instance” which is a separate thing from a 
“field defined in the enclosing class”.

So yes, a big part of this exercise is in trying to keep variable names 
meaning the same thing no matter where they appear (captured or not). If 
we start snapshotting everything, then you have two version of a local, 
the one snapshotted and captured e.g. inside a lambda, and the one 
(maybe mutable!) available outside.

My subjective opinion here is that, if this principle is important (and, 
as John mentioned, it was very painfully front and center when inner 
classes were first added to the language), giving this principle up to 
allow capture in an extra 20-30 imperative loops (in an entire codebase) 
doesn’t look a great deal. That is, reducing asymmeties between counted 
loops and for-each loop comes at a price: now capture variables no 
longer mean the same thing they do in their non-captured context.

Of course you can argue it both ways: by making the new capture 
treatment only available for induction variables in for loops (where the 
variable is not mutated in the body) we do reduce (not eliminate!) the 
chances of somebody observing different values for the same induction 
variable. Is that enough to put our minds at ease? Overall, it seems to 
me that this a very subjective topic: there is no well-defined principle 
as to why imperative for loops should behave any different than they do 
today, as there is (likely) no well-defined principle as to why for-each 
loop behaves the way it does today.

How you "fix" this largely depends on (a) how you find this asymmetry 
annoying (which likely depends on exposure - and different code bases 
might have different levels of that) and (b) how much your developer 
intuition is trained to view the loop induction variable as a single 
mutable *variable*, or a series of *values* where each value is derived 
from the former in a controlled fashion. I don't have a strong (enough) 
opinion on either :-)

Maurizio

​
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20241023/714141b2/attachment-0001.htm>


More information about the amber-dev mailing list