When considering class initialization, we all know that when performing subclass initialization, if the parent class is not initialized, the subclass must be initialized first. However, things are not as simple as a single sentence.
First, let’s take a look at the conditions for initialization triggering 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 call the class;
(3) When initializing a class, if the parent class has not been initialized, the initialization of the parent class will be triggered first;
(4) The class where the entrance main method is executed is located;
(5) The class where the method handle is located in the dynamic language support of JDK1.7, if the initialization is not triggered;
After compilation, a <clinit> method is generated, and the class initialization is carried out in this method. This method is only executed, and the JVM ensures this and performs synchronization control;
Among them, condition (3), from the point of view of method calling, is the subclass <clinit> that recursively calls the parent class <clinit> at the beginning, which is similar to the fact that we must first call the parent class constructor in the subclass constructor;
But it should be noted that "triggering" does not complete initialization, which means that it is possible that the initialization of the subclass will end in advance before the initialization of the parent class, which is where the "danger" lies.
1. An example of class initialization:
In this example, I use a peripheral class to contain 2 static member classes with inheritance relationships. Because the initialization of the peripheral class and the static member classes have no causal relationship, it is safe and convenient to show it like this;
Parent class A and child class B respectively contain main functions. From the above triggering condition (4), it can be seen that different class initialization paths are triggered by calling these two main functions respectively;
The problem with this example is that the parent class contains the static reference of the child class 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 static references of the child class private static B b = new B(); protected static int aInt = 9; static { System.out.println("Class A initialization end..."); } public static void main(String[] args) { } } private static class B extends A { static { System.out.println("Class B initialization start..."); } //The domain of the subclass depends on the domain of the parent class private static int bInt = 9 + A.aInt; public B() { //The static domain of the constructor depends on the class System.out.println("Constructor call for class B" + "value of bInt" + bInt); } static { System.out.println("Class B initialization ends... " + "value of aInt: " + bInt); } public static void main(String[] args) { } } } Scenario 1: The output result when the entry is a main function of class B:
/** * Class A initialization starts... * The constructor of class B calls the value of bInt 0 * Class A initialization ends... * Class B initialization starts... * Class B initialization ends... AInt value: 18 */
Analysis: It can be seen that the call of the main function triggers the initialization of class B and enters the <clinit> method of class B. Class A, as its parent class, starts initializing first and enters the <clinit> method of A, and there is a statement new B(); at this time, B will be instantiated, which is already in the <clinit> of class B. The main thread has obtained the lock and started to execute the <clinit> of class B. We said at the beginning that the JVM will ensure that the initialization method of a class is only executed once. After receiving the new instruction, the JVM will not enter the <clinit> method of class B again but will be instantiated directly. However, at this time, class B has not completed the class initialization, so you can see that the value of bInt is 0 (this 0 is the zero initialization performed after allocating the memory of the method area during the preparation stage of class loading);
Therefore, it can be concluded that the parent class contains the static domain of the child type and performs the assignment action, which may cause the subclass instantiation to be performed before the class initialization is completed;
Scenario 2: The output result when the entry is a main function of class A:
/** * Class A initialization starts... * Class B initialization starts... * Class B initialization ends... The value of aInt: 9 * The constructor of class B calls the value of bInt 9 * Class A initialization ends... */
Analysis: After analysis of scenario 1, we know that triggering the initialization of class A by the initialization of class B will cause the instantiation of class variable b in class A to be performed before the initialization of class B is completed. So if Class A is initialized first, can class B be triggered first when class variable instantiation, so that initialization is made before instantiation? The answer is yes, but there are still problems.
According to the output, we can see that the initialization of class B is performed before the initialization of class A is completed, which causes variables like class variable aInt to be initialized only after class B is initialized, so the value of aInt obtained by domain bInt in class B is "0", rather than "18" as we expected;
Conclusion: In summary, it can be concluded that it is very dangerous to include class variables of subclass types in the parent class and instantiate them when defining them. The specific situation may not be as straightforward as an example. Calling methods to assign values at the definition is also dangerous. Even if you want to include static domains of subclass types, you should also assign values through static methods, because the JVM can ensure that all initialization actions are completed before the static method is called (of course, this guarantee is that you should not include static B b = new B(); such initialization behavior);
2. An instantiated example:
First, you need to know the process of object creation:
(1) When encountering a new instruction, check whether the class has completed loading, verification, preparation, parsing, and initialization (the parsing process is to parse the symbol reference into a direct reference, such as the method name is a symbol reference, which can be performed when using this symbol reference after initialization is completed, precisely to support dynamic binding), these processes are carried out before completing;
(2) Allocate memory, use the free list or pointer collision method, and "zero" the newly allocated memory. Therefore, all instance variables are initialized to 0 by default (referenced as null) in this link;
(3) Execute the <init> method, including checking the call to the <init> method (constructor) of the parent class, the assignment actions defined by the instance variable, the instantiator executes in the instantiator, and finally calling the actions in the constructor.
This example may be more well known, that is, it violates "don't call overridable methods in the constructor, clone method and readObject method". The reason is that polymorphism in Java, that is, dynamic binding.
The constructor of parent class A contains a protected method, and class B is its 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 result:
/** * B's doSomething, bInt: 0 */
Analysis: First of all, you need to know that when there is no display, the Java compiler will generate the default constructor and call the constructor of the parent class at the beginning. Therefore, the constructor of class B will call the constructor of class A first at the beginning.
The protected method doSomething is called in class A. From the output result, we see that the method implementation of the subclass is actually called, and the instantiation of the subclass has not started yet, so bInt is not 9 as "expected" but 0;
This is because of dynamic binding, doSomething is a protected method, so it is called through the invokevirtual directive, which finds the corresponding method implementation based on the type of object instance (here is the instance object of B, and the corresponding method is the method implementation of class B), so this result is.
Conclusion: As mentioned earlier, "Don't call overridable methods in the constructor, clone method and readObject method".
The above are the two "minefields" in Java class initialization and instantiation introduced to you. I hope it will be helpful to everyone's learning.