Superclasses for inline classes
daniel.smith at oracle.com
Wed Dec 18 23:57:40 UTC 2019
[Expanding on and summarizing discussion about abstract superclasses from today's meeting.]
There are some strong incentives for us to support inline classes that have superclasses other than Object. Briefly, these include:
- Identity -> inline migration candidates (notably java.lang.Integer) often extend abstract classes
- A common refactoring may be to extend an existing class with a (possibly private) inline implementation
- Abstract classes are more expressive than interfaces
- If we compile Foo.ref to an abstract class, we can better represent the full API of an inline class using an abstract class
To be clear, much of this has to do with migration, and I subscribe to a fairly expansive view of how much we should permit and encourage migration. I think most every project in the world has at least a few opportunities to use inline classes. Our design should limit the friction necessary (e.g., disruptive redesigns of type hierarchies) to integrate inline classes with the existing body of code.
We've considered, as an alternative, supporting transparent migration of existing classes to interfaces. But this raises many difficult issues surrounding source, binary, and behavioral compatibility. It would be nice not to have to tackle those issues, nor introduce a lot of caveats into the class -> interface migration story.
Inline class instantiation is is fundamentally different from identity class instantiation. While the language seeks to smooth over these differences, under the hood all inline objects come from 'defaultvalue' and 'withfield' invocations. There is no opportunity in these bytecodes for a superclass to execute initialization code.
(Could we redesign the construction model to properly delegate to a superclass? Sure, but that's a huge new feature that probably isn't justified by the use cases.)
As a result, constructors, instance initializers, and instance fields in a superclass are unusable to inline class instances. In fact, their existence would be a vulnerability, since authors typically make assumptions about initialization having occurred.
Fortunately, 'Object' doesn't require any initialization and so can safely be extended. Our goal is to expand the set of safe-to-extend classes.
An inline class may extend another class, as long as the superclass has the following properties:
- It has no instance fields
- It has no constructors
- It has no instance initializers
- It is abstract* or Object
- It extends another class with these properties
Subtype polymorphism works the same for superclasses as it does for superinterfaces.
(*Remi points out that we could drop the 'abstract' restriction, meaning there may be identity instances of the superclass. Given the restriction on fields, though, I'm struggling to envision a use case; the consensus is that 'new Object()' is probably something we want to *stop* supporting.)
Call a class that satisfies these constraints an "initialization-free class" (bikeshedding on this term is welcome!). Like an interface, its value set may include references to both inline class instances and identity class instances.
We *do *not* want the initialization-free property to be expressed as a class modifier—this feature is too obscure to deserve that much prominence, encouraging every class author to consider one more degree of freedom; and we don't want every class to have to manually opt in.
But we *do* need the initialization-free property to be part of the public information about the class. For example, the javadoc should say something like "this is an initialization-free class". Otherwise, it's impossible to tell the difference between, e.g., a class with no fields and a class with private fields.
In the past, a Java class declaration that lacks a constructor always got a default constructor. In this model, however, an initialization-free class has no constructor at all. A 'super()' call directed at such a class is a no-op.
Compilation & JVM support
There are two alternative compilation strategies:
1) An initialization-free class is compiled like always, including an '<init>' method of the form 'aload_0; invokespecial; return;'. Some metadata (flag or attribute) indicates that the class is initialization-free.
The initialization-free flag is partially validated at class load time: it's an error to claim to be initialization-free and be non-abstract (and non-Object), declare instance fields, or extend a non-initialization-free superclass.
We don't validate <init> method contents. If someone chooses to generate an "initialization-free" class file that contains <init> code, they accept the risk that the code won't run.
On loading, an inline class must extend an initialization-free class.
2) An initialization-free class is compiled without an '<init>' method.
For binary compatibility, existing references to 'Foo.<init>' must successfully resolve, with invocation being a no-op. (This is something new—resolution to fake declarations—and potentially concerning.)
At class load time, an inline class must extend a chain of superclasses that are abstract (or Object), lack '<init>' methods, and lack instance field declarations.
Existing classes that meet these requirements may act as inline class superclasses (probably surprisingly, since currently a class without an <init> method can't be initialized at all).
(My thoughts on (1) vs. (2): both are plausible, and I like the lack of metadata overhead in (2), but otherwise (1) seems much cleaner.)
In either case, the big new feature here is that an inline class may have a superclass other than Object. This may violate some existing assumptions in the implementation, although it sounds like we can hope nothing new really needs to be done to support it.
More information about the valhalla-spec-observers