2 'minefields' in Java class initialization and instantiation

  • 2020-05-05 11:16:27
  • OfStack

When we think about class initialization, we all know that when we do subclass initialization, we initialize the child first if the parent class is not initialized. But there is more to it than that.
First look at the conditions for initializing triggers in Java:

(1) when using new to instantiate objects and access static data and methods, that is, when encountering instructions: new, getstatic/putstatic and invokestatic;
(2) when using reflection to invoke a class;
(3) when initializing a class, if the parent class is not initialized, the initialization of the parent class is first triggered;
(4) execute the class where the entry main method is located;
(5) the class where the method handle is located in JDK1.7 dynamic language support, if there is no initialization, initialization will be triggered;

After compiling, an < is generated clinit > Method, class initialization is done in this method, which is only performed, guaranteed by JVM, and controlled synchronously;
Where condition (3), from the point of view of the method call, is < of the subclass clinit > Will recursively call < of the parent class at the beginning clinit > , which is similar to the way we must first call the constructor of the parent class in the subclass constructor;
However, it is important to note that the "trigger" does not complete the initialization, which means that it is possible that the initialization of subclasses will end earlier than that of the parent classes, which is where the "danger" lies.

1. An example of class initialization:
For this example, I use a peripheral class that contains two static member classes that are inherited. Since the initialization of the peripheral class has no causal relationship with the static member class, it is safe and convenient to demonstrate this.
The parent A and the subclass B respectively contain main functions. According to the above trigger condition (4), the two main functions are respectively called to trigger different class initialization paths.
The problem with this example is that the parent class contains the static reference of the subclass and initializes it at the definition:


public class WrapperClass { 
  private static class A { 
    static { 
      System.out.println(" class A Initialization start ..."); 
    } 
    // The parent class contains the child class static reference  
    private static B b = new B(); 
    protected static int aInt = 9; 
 
    static { 
      System.out.println(" class A End of initialization ..."); 
    } 
 
    public static void main(String[] args) { 
 
    } 
  } 
 
  private static class B extends A { 
    static { 
      System.out.println(" class B Initialization start ..."); 
    } 
    // The domain of a subclass depends on the domain of the parent class  
    private static int bInt = 9 + A.aInt; 
 
    public B() { 
      // The constructor depends on the class static The domain  
      System.out.println(" class B Is called to the constructor  " + "bInt The value of the " + bInt); 
    } 
 
    static { 
      System.out.println(" class B End of initialization ... " + "aInt Value: " + bInt); 
    } 
 
    public static void main(String[] args) { 
 
    } 
  } 
} 

scenario 1: when the entry is the main function of class B, the output result is


/** 
   *  class A Initialization start ... 
   *  class B Is called to the constructor  bInt The value of the 0 
   *  class A End of initialization ... 
   *  class B Initialization start ... 
   *  class B End of initialization ... aInt Value: 18 
   */ 

analysis: you can see that the call to the main function triggers the initialization of class B, which goes to < of class B clinit > Method, class A initializes into < of A as its parent clinit > Method with a statement new B(); The instantiation of B occurs, which is < already in class B clinit > The main thread has acquired the lock to begin execution of < of B clinit > We said at the beginning that JVM will ensure that the initialization method of a class is only executed once, and JVM will not enter < of B after receiving the new instruction clinit > Method is instantiated directly, but class B has not yet finished class initialization, so you can see that the value of bInt is 0 (this 0 is the zero initialization after the method area memory is allocated in the preparatory phase of class loading);
Therefore, it can be concluded that including the static field of the subclass type in the parent class and performing the assignment action may cause the subclass to be instantiated before the class initialization is completed.

scenario 2: when the entry is the main function of class A, the output result is


/** 
   *  class A Initialization start ... 
   *  class B Initialization start ... 
   *  class B End of initialization ... aInt Value: 9 
   *  class B Is called to the constructor  bInt The value of the 9 
   *  class A End of initialization ... 
   */ 

analysis: after a scenario analysis, we know that the class is triggered by the initialization of such B A initialization, can lead to class variables in class A b instantiation of the class B before initialization is complete, that if the first initialization A can not at the time of class variable instantiation to trigger the first class B initialization, so as to make the initialization before the instantiation? The answer is yes, but there are still questions.
According to the output, it can be seen that the initialization of class B took place before the initialization of class A, which led to the initialization of the class variable aInt only after the initialization of class B. Therefore, the value of aInt obtained by the field bInt in class B was "0" instead of "18" as we expected.

Conclusion: In conclusion, we can draw the subclass type of class variables contained in the parent class, and in defining the instantiated is very dangerous behavior, the specific circumstances may not be as straightforward to the example, a method is called in the defined assignment as hidden danger, even to contain subclass type static domain, also should through static method carries on the assignment, Because JVM guarantees that all initialization is done before the static method is called (which of course you shouldn't include static B b = new B(); Such initialization behavior);

2. An example of instantiation:
The first thing you need to know is the object creation process:
(1) meet new instructions, check whether the class is finished loading, validation, preparation, parsing, initialization (parsing process is symbolic reference resolving into direct reference, such as the method name is a symbol, reference can be used in the initialization complete this symbol references, when it is in order to support dynamic binding), not completed prior to these process;
(2) allocate memory, adopt the method of free list or pointer collision, and "set the newly allocated memory to zero", so all the instance variables are initialized to 0 (null) by default in this link;
(3) execute < init > Method, including checking that < of the parent class is called init > Methods (constructors), assignments defined by instance variables, instantiators executed in sequence, and finally called actions in the constructor.

This example is probably better known for its violation of "do not call overridden methods in constructors, clone methods, and readObject methods." The reason lies in the polymorphism, or dynamic binding, of the Java.
The constructor for parent A contains an protected method, of which B is a subclass.


public class WrongInstantiation { 
  private static class A { 
    public A() { 
      doSomething(); 
    } 
 
    protected void doSomething() { 
      System.out.println("A's doSomething"); 
    } 
  } 
 
  private static class B extends A { 
    private int bInt = 9; 
 
    @Override 
    protected void doSomething() { 
      System.out.println("B's doSomething, bInt: " + bInt); 
    } 
  } 
 
  public static void main(String[] args) { 
    B b = new B(); 
  } 
} 

Output:


/** 
   * B's doSomething, bInt: 0 
   */ 

Analysis: first, you need to know that the Java compiler generates the default constructor when the supplied constructor is not shown and calls the parent constructor at the beginning, so the constructor of class B starts by calling the constructor of class A.
protected method doSomething is called in class A. From the output results, we can see that what is actually called is the method implementation of the subclass. At this time, the instantiation of the subclass has not started.
This is because, due to dynamic binding, doSomething is an protected method, so it is invoked through the invokevirtual directive, which finds the corresponding method implementation according to the type of object instance (here is the instance object of B, and the corresponding method is the method implementation of B class).

Conclusion: as stated earlier, "do not call overridden methods in constructors, clone methods, and readObject methods."

Above is the introduction of Java class initialization and instantiation in the two "minefields", I hope to help you learn.


Related articles: