https://openjdk.org/jeps/447 JEP 447: Statements before super() Owner Archie Cobbs Type Feature Scope JDK Status Candidate Component specification / language Discussion amber dash dev at openjdk dot java dot net Reviewed by Brian Goetz, Vicente Arturo Romero Zaldivar Created 2023/01/20 17:33 Updated 2023/04/19 15:54 Issue 8300786 Summary In constructors in the Java programming language, allow statements that do not reference the instance being created to appear before this() or super(). Goals * In constructors, allow statements that do not access the instance being created to appear prior to invocations of this() or super (). * Correct an error in the specification which defines constructor invocations as a static context. * Preserve existing safety and initialization guarantees for constructors. * Do not change the behavior of any existing program. Non-Goals * Modifications to the Java Virtual Machine Specification (JVMS) -- This proposal may prompt reconsideration of the JVMS's current restrictions on constructors, but we do not intend here to revise the JVMS. * Maximizing Java Language Specification (JLS) and JVMS alignment -- These changes will bring the JLS and JVMS into closer alignment, but it is not a goal to harmonize them completely. The JLS and JVMS address different problem domains, and therefore it is reasonable for them to differ in what they allow. For example, the JVMS allows a constructor to write to the same final field multiple times, whereas the JLS does not. * Addressing larger language concerns -- There are many ways in which the interplay between superclass constructors and subclass initialization might be improved, but it is not a goal to explore those here. This work should be considered a pragmatic tweak rather than a statement on language design. Motivation Before an object can be used, its state must be initialized. In order to ensure orderly object initialization, the JLS includes a variety of rules specifically related to object construction. For example, dataflow analysis ensures that every final field is assigned a definite value. For classes in a non-trivial class hierarchy, object initialization does not occur in a single step. An object's state is the composition of groups of fields: the group of fields defined in the class itself plus the groups of fields defined in its superclasses. Each group of fields is initialized in a separate step by a corresponding constructor in those fields' defining class. An object is not fully initialized until every class in its hierarchy has initialized its own fields. To keep this process orderly, the JLS requires that superclass constructors execute prior to subclass constructors. Thus objects are always initialized from the top down. This ensures that, at each level, a constructor can assume that the fields in all of its superclasses have been initialized. This guarantee is important because constructors often need to rely on functionality in a superclass, and the superclass would not be able to guarantee correct behavior without the assumption that its own initialization is complete. For example, it is common for a constructor to invoke superclass methods to configure or prepare the object for a specific task. To enforce top-down initialization, the JLS requires that invocations of this() or super() in a constructor always appear as the first statement. This does indeed guarantee top-down initialization. It does so, however, in a heavy-handed way, by taking what is really a semantic requirement ("initialize the superclass before accessing the new instance") and enforcing it with a syntactic requirement ("super () or this() must be the first statement"). A rule that more carefully addresses the requirement to ensure top-down initialization would allow arbitrary statements prior to superclass construction as long as the instance's fields are not read until superclass construction completes. This would allow constructors to, for example, do housekeeping prior to superclass construction. Such a rule would closely follow the familiar existing rules for blank final fields. Those rules disallow reading prior to initialization, ensure that initialization happens exactly once, and allow full access afterward. The fact that the current rule is unnecessarily restrictive is, in itself, a reason to change it. There are also practical reasons to relax this restriction. For one, the current rule causes idioms commonly used within normal methods to be either difficult or impossible to use within constructors. Below are a few examples. Implementing fail-fast Sometimes we need to validate a constructor parameter that is passed up to the superclass constructor. Today we can only do this in-line, e.g., using static methods: public class PositiveBigInteger extends BigInteger { // This logic really belongs in the constructor private static long verifyPositive(long value) { if (value <= 0) throw new IllegalArgumentException("non-positive value"); return value; } public PositiveBigInteger(long value) { super(PositiveBigInteger.verifyPositive(value)); } } or else after the fact, potentially doing useless work: public class PositiveBigInteger extends BigInteger { public PositiveBigInteger(long value) { super(value); // potentially useless work here if (value <= 0) throw new IllegalArgumentException("non-positive value"); } } It would be more natural to validate parameters as the first order of business, just as in normal methods: public class PositiveBigInteger extends BigInteger { public PositiveBigInteger(long value) { if (value <= 0) throw new IllegalArgumentException("non-positive value"); super(value); } } Passing one value to a superclass constructor twice Sometimes we need to compute a value and pass it to the superclass constructor twice, as two different arguments. Today the only way to do that is to add an intermediate constructor: public class MyExecutor extends ScheduledThreadPoolExecutor { private static class MyFactoryHandler implements ThreadFactory, RejectedExecutionHandler { ... } // Extra intermediate constructor we must hop through private MyExecutor(int corePoolSize, MyFactoryHandler factory) { super(corePoolSize, factory, factory); } public MyExecutor(int corePoolSize) { this(corePoolSize, new MyFactoryHandler()); } } A more straightforward implementation might look like this: public class MyExecutor extends ScheduledThreadPoolExecutor { private static class MyFactoryHandler implements ThreadFactory, RejectedExecutionHandler { ... } public MyExecutor(int corePoolSize) { MyFactoryHandler factory = new MyFactoryHandler(); super(corePoolSize, factory, factory); } } Complex preparation of superclass constructor arguments Sometimes we must perform non-trivial computation in order to prepare superclass arguments. For example: public class MyBigInteger extends BigInteger { /** * Use the public key integer extracted from the given certificate. * * @param certificate public key certificate * @throws IllegalArgumentException if certificate type is unsupported */ public MyBigInteger(Certificate certificate) { final byte[] bigIntBytes; PublicKey pubkey = certificate.getPublicKey(); if (pubkey instanceof RSAKey rsaKey) bigIntBytes = rsaKey.getModulus().toByteArray(); else if (pubkey instanceof DSAPublicKey dsaKey) bigIntBytes = dsaKey.getY().toByteArray(); else if (pubkey instanceof DHPublicKey dhKey) bigIntBytes = dhKey.getY().toByteArray(); else throw new IllegalArgumentException("unsupported cert type"); super(bigIntBytes); } } All of the examples above that show code before super() adhere to the semantic requirement of "initialize the superclass before accessing the new instance" and therefore preserve top-down initialization. The JVMS already allows this Fortunately, the JVMS already grants suitable flexibility to constructors: * Multiple invocations of this() and super() may appear in a constructor as long as on any code path there is exactly one invocation. * Arbitrary code may appear before this() and super() as long as that code does not reference the instance under construction except to assign fields. * However, invocations of this() and super() may not appear within a try block, i.e., within a bytecode exception range. These more permissive rules still ensure top-down initialization: * Superclass initialization always happens exactly once, either directly via super() or indirectly via this(); and * Uninitialized instances are off-limits except for field assignments, which do not affect outcomes, until superclass initialization is complete. In fact, the current inconsistency between the JVMS and the JLS is an historical artifact. The original JVMS was more restrictive as well, but this led to issues with the initialization of compiler-generated fields for new language features such as inner classes and captured free variables. As a result the JVMS was relaxed to accommodate the compiler, but this new flexibility never made its way back up to the language level. The JLS contains a bug JLS SS8.1.3 defines static context and notes that The purpose of a static context is to demarcate code that must not refer explicitly or implicitly to the current instance of the class whose declaration lexically encloses the static context. The JLS naturally applies this concept to code inside a super() or this() invocation. Prior to the introduction of generics, inner classes, and captured free variables, this yielded the correct semantics for superclass constructor invocation. However, as SS8.1.3 notes a static context prohibits * this expressions, whether unqualified or qualified, * Unqualified references to instance variables of any lexically enclosing class or interface declaration, and * References to type parameters, local variables, formal parameters, and exception parameters declared by methods or constructors of any lexically enclosing class or interface declaration that is outside the immediately enclosing class or interface. These rules make this program illegal: import java.util.concurrent.atomic.AtomicReference; public class A extends AtomicReference { private int intval; public A(T obj) { super(obj); } public class B extends A { public B() { super((T)null); // illegal - 'T' } } public class C extends A { C() { super(A.this); // illegal - 'this' } } public class D extends A { D() { super(intval); // illegal - 'intval' } } public static Object method(int x) { class E extends A { E() { super((float)x); // illegal - 'x' } } return new E(); } } Yet this program has compiled successfully since at least Java 8, and these idioms are in common use! The operative mental model here is that code "must not refer explicitly or implicitly to the current instance of the class whose declaration lexically encloses" the code in question. However the concept of static context, as defined, goes beyond that to forbid, e.g., even references to generic type parameters. The underlying issue is that the JLS applies the concept of static context to two scenarios which are similar, but not equivalent: 1. When there is no this instance defined, e.g., as within a static method, and 2. When this is defined but must not be referenced, e.g., prior to superclass initialization. The current definition of static context is appropriate for the first scenario. After the addition of generics, inner classes, and captured free variables to the language, however, it is no longer appropriate for the second scenario. We therefore define a new concept, a pre-initialization context, which is like a static context but is less restrictive. It still disallows accessing the current instance in any way but does not disallow, for example, using the class's generic type parameters or accessing an outer instance. This more accurately matches not only the underlying requirement but also developer expectations, common usage, and the compiler's behavior going back as far as Java 8. (This change will, effectively, fix 8301649 by codifying the compiler's current behavior.) Description Summary of JLS modifications * Update the grammar to allow statements (other than return) to appear prior to super() or this(). * Define the statements up to and including a super() or this() call as a pre-initialization context. * Narrow the definition of static context to exclude pre-initialization contexts. * Update restrictions on static contexts to also restrict pre-initialization contexts where appropriate. Specific JLS modifications * SS6.5.6.1 Simple Expression Names Modify the first bullet point to read: + The expression name does not occur in a static context (SS8.1.3) or in a pre-initialization context of the associated instance (SS8.8.7.1). * SS6.5.7.1 Simple Method Names Modify the last sentence in the first paragraph as follows: The rules also prohibit (SS15.12.3) a reference to an instance method occurring in a static context (SS8.1.3), a pre-initialization context of the associated instance (SS8.8.7.1), or in a nested class or interface ... * SS8.1.3 Inner Classes and Enclosing Instances After "A construct (statement, local variable declaration statement, local class declaration, local interface declaration, or expression) occurs in a static context if the innermost:", remove the bullet point "explicit constructor invocation statement". After "which encloses the construct is one of the following:", remove the bullet point "an explicit constructor invocation statement (SS8.8.7.1)". Rewrite the second following note as: The purpose of a static context is to demarcate code for which there is no current instance defined of the class whose declaration lexically encloses the static context. Consequently, code that occurs in a static context is restricted in the following ways ... * SS8.8.7 Constructor Body Modify the beginning of this section to read: A constructor body may contain an explicit invocation of another constructor of the same class or of the direct superclass (SS8.8.7.1). ConstructorBody: { [BlockStatements] } ; { [BlockStatements] ExplicitConstructorInvocation [BlockStatements] } ; It is a compile-time error for a constructor to directly or indirectly invoke itself through a series of one or more explicit constructor invocations involving this. If a constructor body does not contain an explicit constructor invocation and the constructor being declared is not part of the primordial class Object, then the constructor body implicitly begins with a superclass constructor invocation "super();", an invocation of the constructor of its direct superclass that takes no arguments. Except for the possibility of explicit constructor invocations and the prohibitions on return statements (SS14.17), the body of a constructor is like the body of a method (SS8.4.7). If a constructor body contains an explicit constructor invocation, the BlockStatements preceding the explicit constructor invocation are called the prologue of the constructor body. The BlockStatements in a constructor with no explicit constructor invocation and the BlockStatements following the explicit constructor invocation in a constructor with an explicit constructor invocation are called the main body of the constructor. A return statement (SS14.17) may be used in the main body of a constructor if it does not include an expression. It is a compile-time error if a return statement appears in the prologue of a constructor body. * SS8.8.7.1 Explicit Constructor Invocations Modify this sentence that follows the bullet points: An explicit constructor invocation statement introduces a pre-initialization context of the current object. The pre-initialization context includes the prologue of the constructor and the explicit constructor invocation statement itself. Within a pre-initialization context, constructs that refer explicitly or implicitly to the current object are disallowed. These include this or super expressions referring to the current object, unqualified references to instance variables or instance methods of the current object, method references referring to instance methods of the current object, and instantiations of inner classes of the current object's class for which the current object is the enclosing instance (SS8.1.3). * SS12.5 Creation of New Class Instances Replace the numbered steps for constructor processing with the following: 1. Assign the arguments for the constructor to newly created parameter variables for this constructor invocation. 2. If this constructor contains an explicit constructor invocation (SS8.8.7.1), then execute the BlockStatements of the prologue of the constructor body. If execution of any statement completes abruptly, then execution of the constructor completes abruptly for the same reason; otherwise, continue with step 3. 3. If this constructor contains an explicit constructor invocation (SS8.8.7.1) of another constructor in the same class (using this), then evaluate the arguments and process that constructor invocation recursively using these same six steps. If that constructor invocation completes abruptly, then this procedure completes abruptly for the same reason; otherwise, continue with step 6. 4. This constructor does not contain an explicit constructor invocation of another constructor in the same class (using this). If this constructor is for a class other than Object, then this constructor contains an explicit or implicit invocation of a superclass constructor (using super). Evaluate the arguments and process that superclass constructor invocation recursively using these same six steps. If that constructor invocation completes abruptly, then this procedure completes abruptly for the same reason. Otherwise, continue with step 5. 5. Execute the instance initializers and instance variable initializers for this class, assigning the values of instance variable initializers to the corresponding instance variables, in the left-to-right order in which they appear textually in the source code for the class. If execution of any of these initializers results in an exception, then no further initializers are processed and this procedure completes abruptly with that same exception. Otherwise, continue with step 6. 6. Execute the main body of this constructor. If that execution completes abruptly, then this procedure completes abruptly for the same reason. Otherwise, this procedure completes normally. * SS15.8.3 this Change the second bullet point to read "in the main body of a constructor of a class (SS8.8.7)". Modify this sentence as follows: It is a compile-time error if a this expression occurs in a static context (SS8.1.3) or in a pre-initialization context of the associated instance (SS8.8.7.1). * SS15.11.2 Accessing Superclass Members using super Modify this sentence as follows: It is a compile-time error if a field access expression using the keyword super appears in a static context (SS8.1.3) or in a pre-initialization context of the associated instance (SS8.8.7.1). * SS15.12.3 Compile-Time Step 3: Is the Chosen Method Appropriate? Modify all three of these sentences as follows: It is a compile-time error if the method invocation occurs in a static context (SS8.1.3) or in a pre-initialization context of the associated instance (SS8.8.7.1). * SS15.13 Method Reference Expressions Modify this sentence as follows: If a method reference expression has the form super :: [TypeArguments] Identifier or TypeName . super :: [TypeArguments] Identifier, it is a compile-time error if the expression occurs in a static context (SS8.1.3) or in a pre-initialization context of the associated instance (SS8.8.7.1). * SS15.13.1 "Compile-Time Declaration of a Method Reference" Modify this sentence as follows: It is a compile-time error if the method reference expression has the form super :: [TypeArguments] Identifier or TypeName . super :: [TypeArguments] Identifier, and the method reference expression occurs in a static context (SS8.1.3) or in a pre-initialization context of the associated instance (SS8.8.7.1). Records Record constructors are subject to more restrictions that normal constructors. In particular: * Canonical record constructors may not contain any explicit super () or this() invocation, and * Non-canonical record constructors may invoke this(), but not super(). These restrictions remain in place, but otherwise record constructors also benefit from these changes. The net result is that non-canonical record constructors may now contain prologue statements before this (). Testing We will test the compiler changes with existing unit tests, unchanged except for those tests that verify changed behavior, plus new positive and negative test cases as appropriate. We will compile all JDK classes using the previous and new versions of the compiler and verify that the resulting bytecode is identical. No platform-specific testing should be required. Risks and Assumptions * An explicit goal of this work is to not change the behavior of existing programs. Therefore, other than any newly created bugs, the risk to existing code should be low. * It is possible that compiling and executing newly valid code will reveal latent bugs in existing code. OpenJDK logo Installing Contributing Sponsoring Developers' Guide Vulnerabilities JDK GA/EA Builds Mailing lists Wiki * IRC Bylaws * Census Legal Workshop JEP Process Source code Mercurial GitHub Tools Git jtreg harness Groups (overview) Adoption Build Client Libraries Compatibility & Specification Review Compiler Conformance Core Libraries Governing Board HotSpot IDE Tooling & Support Internationalization JMX Members Networking Porters Quality Security Serviceability Vulnerability Web Projects (overview, archive) Amber Audio Engine CRaC Caciocavallo Closures Code Tools Coin Common VM Interface Compiler Grammar Detroit Developers' Guide Device I/O Duke Font Scaler Galahad Graal Graphics Rasterizer IcedTea JDK 7 JDK 7 Updates JDK 8 JDK 8 Updates JDK 9 JDK (... 19, 20, 21) JDK Updates JavaDoc.Next Jigsaw Kona Kulla Lambda Lanai Leyden Lilliput Locale Enhancement Loom Memory Model Update Metropolis Mission Control Modules Multi-Language VM Nashorn New I/O OpenJFX Panama Penrose Port: AArch32 Port: AArch64 Port: BSD Port: Haiku Port: Mac OS X Port: MIPS Port: Mobile Port: PowerPC/AIX Port: RISC-V Port: s390x Portola SCTP Shenandoah Skara Sumatra Tiered Attribution Tsan Type Annotations Valhalla Verona VisualVM Wakefield Zero ZGC Oracle logo (c) 2023 Oracle Corporation and/or its affiliates Terms of Use * License: GPLv2 * Privacy * Trademarks