Towards cleaner nesting

Brian Goetz brian.goetz at oracle.com
Tue Jan 7 20:30:27 UTC 2020


Everything about nesting in Java is a mess.  The terminology is a mess 
(top level classes, nested classes, inner classes, local classes, 
anonymous classes); the set of restrictions on what can nest in what is 
ad-hoc (can have local classes but not local interfaces; inner classes 
cannot have static members, including static nested classes), and the 
set of rules about what must be, can be, or cannot be static is also 
ad-hoc (nested classes can be static or not, nested interfaces are 
implicitly static, but local and anonymous classes may not be static, 
even though it might make sense.)  On top of that, we can nest classes 
in methods (sometimes) and methods in classes but not methods in methods 
(local methods).

Not only does this make for a lot of accidental complexity in 
specification, implementation, and user's brains, but it means every 
feature interact with this complexity.  Nested records are implicitly 
static, but this meant that in 14 we can't have nested records in 
non-static classes, because, non-static classes can't have static 
members.  (Yes, this could be fixed; hold your "why don't you just" 
suggestions.)  And we borked up the implementation of local records the 
first time around, where they accidentally capture effectively final 
locals, which they shouldn't -- because we'd never really charted the 
"static local class" territory, and got it wrong the first time.  (Yes, 
this can be fixed too, and will be before 14 goes out.)

So, I'd like to propose a simpler, general story of nesting (which is 
consistent with the ad-hoc rubbish we have) which we can get to in 
stages.  The purpose of this mail is to discuss the model; in what 
increments we get there is a separate story.

Goals:
  - Anything (class, interface, record, enum, method) can be nested in 
anything;
  - Some things are always static (enums, records, interfaces) when 
nested; the rest can be made static when desired;
  - The rule about "no static members in nonstatic nested classes" has 
to go;
  - Rules about whether members / locals from enclosing contexts can be 
specified in a single place, using local reasoning.

The core of this is coming to an understanding of what "static" means.  
When construct X nests in Y (whether X and Y are classes, methods, 
interfaces, etc), for "X" to be "static" means that nesting is being 
used purely for purposes of namespacing, and not for purposes of having 
access to names (locals or nonstatic class members) from enclosing 
constructs.

Unfortunately all the terms we might use for whether or not a symbol in 
an outer construct can be used in a nested construct -- such as 
"accessible" -- are overloaded with other meanings. For purposes of this 
discussion, let's call this "capturable" (this is also overloaded, but 
less so.)  Each construct (class type or method) has two sets of names 
from outer constructs that are capturable -- a _statically capturable_ 
set SC(X), and a _non-statically capturable_ set NC(X).  We can define 
capturability using local reasoning:

Base cases:
  - Names of static members in X are in SC(X);
  - Names of instance members of X (if X is a class) or effectively 
final locals of X (if X is a method) are in NC(X);

Induction cases, where X is nested directly in Y:
  - SC(Y) is in SC(X)
  - If _X is not static_, then NC(Y) is in NC(X)

We then say that X can capture names in SC(X) and NC(X); all we need to 
compute capturability is the capture sets of X's immediately enclosing 
construct, and whether X is static or not in that construct (modulo 
shadowing etc.)

For the math-challenged, what this means is:
  - A nested construct can access static members of all the enclosing 
constructs;
  - A nested non-static construct can access instance members and 
effectively final locals of all enclosing constructs, up until we hit a 
static construct, and then capturing stops.  (So if Z is nested in Y is 
nested in static X, Z can access instance members / eff final locals of 
Y and X but not anything non-static from outside of X.)

Note that this is consistent with what currently happens when X is a 
method as well as a class type; static methods in a class "capture" the 
static members of the enclosing class, and instance methods also capture 
the instance members of the enclosing class -- and also consistent with 
capturing in lambdas and anonymous classes, if we assume that these are 
always non-static constructs.

We then say enums, records, and interfaces are _always_ static when 
nested, whether declared so or not, we eliminate the restriction about 
static members in non-static nested classes (now that we have a clear 
semantics for them), and allow local classes to be declared as static.  
(Eventually, we also relax the restrictions about methods in methods, 
static or not.)

(Additionally, the model supports the notion of "static lambda" and 
"static anonymous class" with obvious semantics (can't capture 
anything); we can decide later whether adding this flexibility is worth 
the additional surface syntax.)

This is a strict superset of the status quo, and yields a more flexible 
and regular language -- and hopefully a simpler spec (since so many of 
these cases are specified as ad-hoc corner cases.)


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20200107/e2f31ded/attachment.htm>


More information about the amber-spec-experts mailing list