简体   繁体   中英

Why aren't fields initialized to non-default values when a method is run from super()?

I must have spent over an hour trying to figure out the reason for some unexpected behavior. I ended up realizing that a field wasn't being set as I'd expect. Before shrugging and moving on, I'd like to understand why this works like this.

In running the example below, I'd expect the output to be true, but it's false. Other tests show that I always get whatever that type default value is.

public class ClassOne {

    public ClassOne(){
        fireMethod();
    }

    protected void fireMethod(){
    }

}

public class ClassTwo extends ClassOne {

    boolean bool = true;

    public ClassTwo() {
        super();
    }

    @Override
    protected void fireMethod(){
        System.out.println("bool="+bool);
    }

    public static void main(String[] args) {
        new ClassTwo();
    }
}

output:

bool=false
boolean bool = true;

public ClassTwo() {
    super();
}

is identical to

boolean bool;

public ClassTwo() {
    super();
    bool = true;
}

The compiler automatically moves the fields initializations within the constructor (just after the super constructor call, implicitly or explicitly).

Since a boolean field default value is false , when super() is called (and thus ClassOne() and fireMethod() ), bool hasn't been set to true yet.


Fun fact: the following constructor

public ClassTwo() {
    super();
    fireMethod();
}

will be understood as

public ClassTwo() {
    super();
    bool = true;
    fireMethod();
}

by the JVM, and the output will thus be

bool=false
bool=true

The superclass constructor is called before the subclass constructor. And in Java, before a constructor is run, all instance members have their default value (false, 0, null). So when super() is called, bool is still false (default value for booleans).

More generally, calling an overridable method from a constructor (in ClassOne) is a bad idea for the reason you just discovered: you might end up working on an object that has not been fully initialised yet.

The instance initializers are executed after super() is called implicitly or explicitly.

From the Java Language Specification, section 12.5: "Creation of new class instances :

"3. This constructor does not begin with 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 will begin with an explicit or implicit invocation of a superclass constructor (using super). Evaluate the arguments and process that superclass constructor invocation recursively using these same five steps. If that constructor invocation completes abruptly, then this procedure completes abruptly for the same reason. Otherwise, continue with step 4.

"4. 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 5."

super() is actually superfluous (pun not intended) in this case because it is implicitly called in every constructor. So what this means is that the constructor of ClassOne is called first. So before a constructor is run, the instance members have their default values (so bool is false ). It is only after the constructors are run, that the fields are initialized.

So your constructor effectively becomes:

public ClassTwo() {
    super(); //call constructor of super class

    bool = true; //initialize members;
}

But ClassOne calls the overridable method that prints out the value of bool , which is false at that point.

In general, it is bad practice to call overridable methods from a constructor (as you are doing in ClassOne ) because you're now working with an object that is not completely initialized.

From Effective Java (2nd Edition):

There are a few more restrictions that a class must obey to allow inheritance. Constructors must not invoke overridable methods, directly or indirectly. If you violate this rule, program failure will result. The superclass constructor runs before the subclass constructor, so the overriding method in the subclass will be invoked before the subclass constructor has run. If the overriding method depends on any initialization performed by the subclass constructor, the method will not behave as expected.

The final answer will be: do not use an overridable method in a constructor.

In every constructor:

  • call the super constructor (implicit or first in the constructor)
  • do the field initialisations of the current class ( type field = value; )
  • do the rest of the constructor

This makes life interesting

public class A {

    public A() {
        init();
    }

    protected void init() {
    }
}

public class B extends A {
    int a = 13;
    int b;

    @Override
    protected void init() {
        System.out.println("B.init a=" + a + ", b=" + b);
        a = 7;
        b = 15;
    }

    public static void main(String[] args) {
        new B().f();
    }

    public void f() {
        System.out.println("B.f a=" + a + ", b=" + b);
    }
}

This results in

B.init a=0, b=0
B.f a=13, b=15
  1. As during the call of B.init B fields are the zeroed ones.
  2. It will set a to 7 (for nothing), and b to 15.
  3. Then in B's constructor a is initialized.

My IDE already flags calling an overridable method in a constructor as bad style.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM