Superclasses for inline classes

Brian Goetz brian.goetz at oracle.com
Wed Feb 12 17:23:13 UTC 2020


> The language story is still uncertain. Here are four alternative designs, all of which are, I think, reasonable approaches to consider. Some discussion about trade-offs is at the end.

Thanks for pulling this together into a nice menu, with chef’s recommendations.  Let me add a few notes, which are mostly “soft” concerns.  

Overall, having fewer “feature X can’t work with Y” restrictions is better; alternative 1 (no language support) creates a “inline classes can’t extend abstract classes” interaction.  WE know that there are at least a few abstract classes we want to have play nicely with inline classes: Number and Record immediately come to mind, but AbstractCollection is also a strong candidate.  (AbstractList has fields, and therefore is a useful example because these fields are private and serve the implementation, which illustrates the dangers Dan is concerned about: that AbstractList starts out being like AbstractCollection, but then finds itself constrained in its evolution because it has inline subclasses.)  

I agree that giving this feature too-prominent billing (such as `inline-friendly abstract class`) is a bad trade.  

I also have no problem with the language inferring whether the superclass is suitable for extension by inline classes; this isn’t much different than requiring an accessible constructor.  Some classes are suitable  for extension by some other classes under some circumstances; as long as the rules are clear enough, and the compiler error messages are helpful when you cross the line, I don’t need a special opt in to say “It’s OK for this class to be extended by inline classes.”  

The problematic cases are all when abstract class C is extended by inline class V, _and C and V are in different maintenance domains_.  If the two are in the same maintenance domain (package or module), then refactoring one may have consequences on the other, but the two can be co-refactored to avoid the problem.  (This is just like refactoring an enum to be a class; there are pitfalls, but if the two are in the same maintenance domain, they can be more easily navigated.)  So, the problematic case is:

 - A is a public abstract class in maintenance domain M, which is currently suitable for extension by inline classes
 - V is an inline class that extends A, in a separate maintenance domain N
 - Later, A wishes to refactor to have, say, private fields, but this would break V.

This is surely a possible nasty surprise; the question is how often this is going to happen, and how much it would cost to prevent it from happening.  Based on the numbers we got from Google’s code base (thanks!), it looks like ~3% of classes are public abstract classes, and many of those would meet the requirements to be extended by inline classes.  But, less-than-3% is still a small number, and cross-maintenance-domain extension is rarer.  (This is a mostly closed codebase; if this were a library, I suspect suitable exposed abstract classes would be even rarer, as all the style guidance suggests preferring exposing interfaces to abstract classes where possible.)  So I think the chain of events that are required to cause regret (publicly exposed abstract class, cross maintenance-domain extension, and subsequent incompatible evolution) likely constitute a corner-of-a-corner case.  This argues for something that minimizes the intrusion into the language.  

Why do we encourage people to expose interfaces and not abstract classes in APIs?  Because interfaces are “more abstract”, and therefore exposing them is more constraining.  So another lever at our disposal is: dial up the style-guide suggestions about not exposing abstract classes across maintenance domains.  

Finally, there’s some mitigation if the bad case comes to pass.  Suppose we have:

     public abstract class Foo { /* accidentally inline friendly */ }

and later you want to add fields to Foo and do:

    public abstract class Foo { 
        int field;
        Foo(int f) { … }
    }

    private class Bar extends Foo { … }

but can’t do so because some external inline class has extended it.  You still have an option:

    public abstract class Foo { /* stays inline friendly */ }

    private abstract class SonOfFoo extends Foo { 
        // all the non-inline-friendliness you want 
    }

    private class Bar extends SonOfFoo { … }

So, even though the author made a “mistake” by exposing the abstract class and therefore constraining themselves forever, the workaround isn’t so bad.  

Of the options, I’m still liking (4), backed up by an annotation like @FunctionalInterface that turns on extra type checking and documentation but serves only to capture design intent.  


> 
> -----
> 
> Alternative 1: No language support
> 
> The language requires all inline classes to extend Object (or perhaps prohibits the 'extends' clause). Abstract <init> methods are available only as a compiler tool.
> 
> Some special exceptions are necessary:
> 
> - Somehow, class files for Object and Number must be generated with abstract <init> methods
> 
> - Integer, Double, etc., are inline classes but are allowed to extend Number
> 
> -----
> 
> Alternative 2: Abstract constructors in the language
> 
> abstract class C {
>    public abstract C();
> }
> 
> Like a method, a constructor can be declared 'abstract' and omit a body. (Bikeshed: maybe the 'abstract' keyword is spelled differently, or left off entirely.)
> 
> These declarations can align closely with the JVM's rules for <init> methods:
> - The superclass must also have an abstract constructor
> - No instance initializers or instance field initializers are allowed
> - It's okay to overload the constructor, but 'abstract' only works on a no-arg constructor
> - (Perhaps) the class doesn't have to be abstract
> - It's allowed (as a no-op) to invoke the constructor (new Object() or super())
> 
> We'll need one further restriction that isn't checked by the JVM and isn't as principled: no instance fields are allowed, even if they're default-initialized. Otherwise, inline classes will have to search for private fields to decide if extension is legal or not, breaking encapsulation.
> 
> The abstract-ness of the constructor is part of the API—appears in javadoc, changing it is incompatible. Having an empty or default constructor isn't the same as having an abstract constructor.
> 
> Inline classes can only extend classes with abstract constructors (and maybe no 'synchronized' instance methods).
> 
> -----
> 
> Alternative 3: Inline-friendly property in the language
> 
> inlineable abstract class C {
> }
> 
> The 'inlineable' property (bikeshed: how to spell this?) enforces some constraints and authorizes children to be inline classes.
> 
> Specific constraints:
> - Can't have instance fields
> - Can't declare a constructor or instance initializer
> - Must extend an inlineable class
> - Must be an abstract class or Object (maybe?)
> - Must not have synchronized methods (probably?)
> 
> An annotation spelling is an option, too, although that crosses a line—there's a precedent for annotations that prompt compiler errors, but not for annotations that influence bytecode output.
> 
> An inlineable class's bytecode has an abstract <init> method.
> 
> The 'inlineable' property is part of the API—appears in javadoc, changing it is incompatible.
> 
> Inline classes can only extend inlineable classes.
> 
> -----
> 
> Alternative 4: Infer no constructor
> 
> Typically, for each class that lacks a constructor declaration, a default constructor is provided that does some simple initialization. But in the following circumstances, we claim there is no constructor at all:
> 
> - Class doesn't declare a constructor or instance initializer
> - Class doesn't declare fields
> - Superclass has no constructor
> - Class is abstract or Object (maybe?)
> 
> Again, blank private fields may not *need* initialization, but inline subclasses need to know that they exist, and the best way to communicate that is through the constructor.
> 
> 'new Object()' (and perhaps 'new Foo()' if we drop the abstract class requirement) doesn't reference any constructor at all. In that case, a fresh instance is allocated without any code execution.
> 
> For compatibility, this also applies to 'super()' (although we can discourage its use in these cases going forward).
> 
> A class without a constructor has, in bytecode, an abstract <init> method.
> 
> The lack of a constructor is part of the API—appears in javadoc, changing it is incompatible. An annotation like @FunctionalInterface could help by checking that nothing has changed on recompilation.
> 
> Inline classes can only extend classes that lack constructors (and maybe 'synchronized' instance methods).
> 
> -----
> 
> Discussion
> 
> Noise level: We don't want to make a big deal about this feature. People shouldn't think too much about it. (4) wins in this regard—no new syntax, just some hand-waving in the language model about what it actually means when you leave out your constructor. (2) introduces a subtle variation on constructor declarations, which can generally be overlooked. (3) is a neon invitation to treat these classes like a fundamentally new kind of entity.
> 
> Compatibility: Adding or removing 'abstract' from an '<init>' method is a binary incompatible change, so it's good if that doesn't happen accidentally. But it's hard to increase awareness of that commitment while minimizing noise level, so there are trade-offs. (2) and (3) both force authors to change something. (4) doesn't have that guardrail, and it's quite easy to imagine an innocent refactoring that makes a class inline-hostile.
> 
> Brian suggested an annotation as an optional guard against this, but I doubt most inline class superclasses will even be aware that they should consider the annotation. And if we succeed in making everyone worry about it, well... adding that overhead as a programming best practice seems counter to (4)'s strengths.
> 
> What's especially troubling about (4) is we're taking longstanding intuitions about Java programming—an absent constructor is the same as an empty constructor—and saying, just kidding, those are two different things. And not just different implementations, but different APIs with different compatibility promises. In fact, by leaving off the constructor, you've promised to *never* introduce a private instance field to this class.
> 
> Availability: Ideally, extending a suitable abstract class should be frictionless for inline class authors. In (2) and (3), the authors are blocked until the abstract class author opts in. In (4), the opt in is by default, although there's still a requirement that the abstract class be compiled with an appropriate source version.
> 
> In practice, if we require an opt in, this will be an obscure, little-used feature; or maybe it will become widespread, but we'll have introduced some new standard boilerplate into the language. If we don't require an opt in, inline classes will be free to extend most abstract classes automatically.
> 
> For context, Brian passed to me some Google corpus numbers on abstract classes. Abstract classes are fairly rare in the universe of type declarations (4%). Among abstract classes, a large majority seem to be candidate supertypes for inline classes—85% have no fields. And most are public (75%), meaning they're less likely to be aware of all their subclasses.
> 
> Summary: I'm torn. (4) is really the feature I want, especially on the availability front, but I don't know if we can get away with pulling the rug out from under authors.
> 
> And stepping back, whether we opt in (as in (4)) or opt out (as in (2), (3)) by default, it's unfortunate that the decision we're asking authors to make is not something they're equipped for. Specifically: "Do I think it's more likely that someday an inline class will want to extend this class, or that someone will want to add a private field to this class?" How can they answer that? Best they can do is play the odds, which I don't imagine change much from class to class, and I'm guessing would say it's much more likely to be extended by an inline class.
> 



More information about the valhalla-spec-observers mailing list