Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8300786

No longer require super() and this() to appear first in a constructor

    XMLWordPrintable

Details

    • JEP
    • Status: Draft
    • P4
    • Resolution: Unresolved
    • None
    • specification
    • None
    • acobbs
    • Feature
    • Open
    • JDK
    • amber dash dev

    Description

      Summary

      Allow statements that do not reference the instance being created to appear before this() or super() in a constructor.

      Goals

      Update the Java language to:

      • Allow statements that do not reference the instance being created to appear before the this() or super() calls in a constructor.
      • Preserve existing safety and initialization guarantees for constructors.

      Non-Goals

      Modifications to the JVMS. These changes may prompt reconsideration of the JVMS's current restrictions on constructors, however, in order to avoid unnecessary linkage between JLS and JVMS changes, any such modifications should be proposed in a follow-on JEP. This JEP assumes no change to the current JVMS.

      Maximizing JLS and JVMS Alignment. Although these changes will bring the JLS and JVMS into closer alignment, it is not a goal to harmonize them. The JLS and JVMS address different problem domains, and therefore it is reasonable for them to differ in what they allow. For one example, the JVMS allows a constructor to write to the same final field multiple times, whereas the JLS does not.

      Changes to Current Behavior. There is no intention to change the behavior of any program following current JLS. This change strictly expands the universe of valid programs, without affecting existing ones.

      Addressing Larger Language Concerns. Thinking about the interplay between superclass constructors and subclass initialization has evolved since the Java language was first created. This work should be considered a pragmatic tweak rather than a statement on language design.

      Motivation

      As in most object-oriented languages, Java defines an explicit "construction" step that occurs after memory allocation but before "regular" use of an object. This acknowledges the fact that, in general, some initialization and setup of object state is required before objects can be safely used. In order to ensure orderly object initialization, Java specifies a variety of rules specifically related to object construction. For example, dataflow analysis verifies that all final fields have definite values assigned during construction.

      However, for classes in a non-trivial class hierarchy, object initialization does not occur in a single step. An object's state consists of the composition of groups of fields: the group of fields defined in the class itself, plus the groups of fields defined in each ancestor superclass. Initialization of each group of fields is performed as a separate step by a corresponding constructor in those fields' defining class. An object is not fully initialized until every class in the hierarchy has had its opportunity to initialize its own fields.

      To keep this process orderly, Java requires that superclass constructors execute prior to subclass constructors. The result is that Java objects are always initialized "top down". This ensures that at each level, a constructor may assume that the fields in all of its superclasses have already been initialized. This guarantee is important, as constructors often need to rely on some functionality in the superclass, and the superclass wouldn't be able to guarantee correct behavior without the assumption that its own initialization were complete. For example, it's common for a constructor to invoke superclass methods to configure or prepare the object for some specific task.

      In order to enforce this top down initialization, the Java language requires that invocations of this() or super() always appear as the first statement in a constructor. This indeed guarantees top down initialization, but it does so in a heavy-handed way, by taking what is really a semantic requirement ("Intialize the superclass before accessing the new instance") and enforcing it with a syntactic requirement ("super() or this() must literally 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 this instance remains hands-off until superclass construction completes. This would allow constructors to do any desired "housekeeping" prior to superclass construction. Such a rule would closely follow the familiar existing rules for blank final fields, where access is disallowed prior to initialization, the initialization must happen exactly once, and full access is permitted afterward.

      The fact that the current enforcement mechanism is unnecessarily restrictive is, in itself, a reason for change. There are also practical reasons to relax this restriction. For one, the current rules cause certain idioms commonly used within normal methods to be either difficult or impossible to use within constructors. Below are a few examples.

      Implementing "Fail Fast"

      A subclass constructor sometimes wishes to enforce a requirement on a parameter that is also passed up to the superclass constructor. Today such requirements can only be applied "inline" e.g., using static methods, or after the fact.

      For example:

      public class PositiveBigInteger extends BigInteger {
      
          public PositiveBigInteger(long value) {
              super(PositiveBigInteger.verifyPositive(value));
          }
      
          // This logic really belongs in the constructor
          private static verifyPositive(long value) {
              if (value <= 0)
                  throw new IllegalArgumentException("non-positive value");
          }
      }

      or:

      public class PositiveBigInteger extends BigInteger {
      
          public PositiveBigInteger(long value) {
              super(value);   // potentially doing 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 Superclass Constructor the Same Parameter Twice

      Sometimes you need to create a single object and pass it to the superclass constructor twice, as two different parameters.

      Today the only way to do that requires adding an extra intermediate constructor:

      public class MyExecutor extends ScheduledThreadPoolExecutor {
      
          public MyExecutor(int corePoolSize) {
              this(corePoolSize, new MyFactoryHandler());
          }
      
          // Extra intermediate constructor we must hop through
          private MyExecutor(int corePoolSize, MyFactoryHandler factory) {
              super(corePoolSize, factory, factory);
          }
      
          private static class MyFactoryHandler
            implements ThreadFactory, RejectedExecutionHandler {
              ...
          }
      }

      A more straightforward implementation might look like this:

      public class MyExecutor extends ScheduledThreadPoolExecutor {
      
          public MyExecutor(int corePoolSize) {
              MyFactoryHandler factory = new MyFactoryHandler();
              super(corePoolSize, factory, factory);
          }
      
          private static class MyFactoryHandler
            implements ThreadFactory, RejectedExecutionHandler {
              ...
          }
      }

      Complex Preparation of Superclass Constructor Parameters

      Sometimes, complex handling or preparation of superclass parameters is needed.

      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 above examples showing code before super() still adhere to the principle of "intialize the superclass before accessing the new instance" and therefore preserve top down initialization.

      What the JVMS Actually Allows

      Fortunately, the JVMS already grants suitable flexibility to constructors:

      • Multiple invocations of this() and/or super() may appear in a constructor, as long as on any code path there is exactly one invocation
      • Arbitrary code may appear before this()/super(), as long as that code doesn't reference the instance under construction, with an exception carved out for field assignment
      • However, invocations of this()/super() may not appear within a try { } block (i.e., within a bytecode exception range)

      As described above, 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 performed

      In fact, the current inconsistency between the JLS and the JVMS is somewhat a historical artifact: the original JVMS was more restrictive as well, however, this led to issues with initialization of compiler-generated fields that supported 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.

      Description

      Language Changes

      The JLS will be modified as follows:

      (1) Change the grammar for ConstructorBody to:

      ConstructorBody:
          { [BlockStatements] } ;
          { [BlockStatements] ExplicitConstructorInvocation [BlockStatements] } ;

      (2) Specify that the ExplicitConstructorInvocation and the BlockStatements preceding it are in a static context (ยง8.1.3)

      (3) Specify that none of the BlockStatements preceding ExplicitConstructorInvocation may complete abrubtly for reason of returning.

      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
      • Non-canonical record constructors may invoke this(), but not super()

      These restrictions remain in place, but otherwise record constructors can benefit from these changes. The net result is that non-canonical record constructors can now contain statements before this().

      Testing

      Testing of compiler changes will be done using the existing unit tests, which are unchanged except for those tests that verify changed compiler behavior, plus new positive and negative test cases related to this new feature.

      All JDK existing classes will be compiled using the previous and new versions of the compiler, and the bytecode compared, to verify there is no change to existing bytecode.

      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 software should be low.

      It's possible that compiling and/or executing newly valid code could trigger bugs in existing code that were not previously accessible.

      Dependencies

      Java compiler changes - JDK-8194743

      Attachments

        Issue Links

          Activity

            People

              acobbs Archie Cobbs
              acobbs Archie Cobbs
              Votes:
              0 Vote for this issue
              Watchers:
              5 Start watching this issue

              Dates

                Created:
                Updated: