The member variable: allocation order
If you can answer below questions correctly, you do not have to read this article.
In java, during the class instantiation process,
- which is called first? A) constructor B) instance variable initializers
- which is finished first? A) constructor B) instance variable initializers
The answers are 🅰️ for question 1, 🅱️ for question 2.
Why do we care
Many people “just know” how Java member variables are allocated. They know how it works, and the JVM works as they expected. However, when asked without a machine in front of them, surprisingly many of them get confused. In fact, the primary purpose of writing this article is to prevent future confusion and provide a reliable reference for myself.
Why is it so confusing? This is because a rarely witnessed transition of the control flow happens during member variable value allocation. When a function is invoked, java runs it from start to end without changing the control flow. (Aside from system transition like thread and process.) We do not usually see a sudden call of other methods unless we explicitly call other methods. However, during class instantiation, this happens. Java calls constructor, but in the middle of the constructor body, java calls an instance allocator. And if the allocation is done, java continues executing the constructor body.
The spec is illustrated at the oracle docs. Briefly speaking, the orders are as below:
- push parameters
- if the constructor begins with explicit
this
() execute it. - if the constructor does not begin with explicit
this()
, then callsuper()
. - then call instance initializers.
- execute the rest of the constructor body.
We will see each step in the codes in the below section.
Example
open class Parent {
constructor() {
println("parent constructor")
printMember()
}
open fun printMember() = Unit
}
class Child: Parent {
var member: Boolean = true
constructor() {
/* instance initializer called here */
println("primary constructor")
println("member = $member")
}
constructor(number: Number): this() {
println("constructor with a parameter")
}
override fun printMember() = println(member)
}
Before reading on, please carefully examine the above code and imagine what will happen when we call Child(1)
in Kotlin or new Child(1)
in java. There will be 5 lines printed: 3 lines from println
and 2 lines from 2 calls to the printMember
calls. According to the above Java SE 12.5, what will be printed in which order?
Let’s follow the specification:
First, we enter constructor(number: Number)
. We ignore step 1 throughout the above code since we will not be using the value anywhere. We see explicit constructor invocation, this()
. So Child#this()
will be executed.
Here, we do not see explicit this()
nor super()
, so we move onto step 3. Here, we call super
.
We can keep going (because class Parent
is a child of Object
), but we know that it will not print out anything. So, the parent’s constructor body will be executed. We see our first printed line here. We will see parent constructor
and member = false
printed out. Why is the member
a false
? This is because the member
is not initialized yet, and the default value for the uninitialized Boolean is a false.
So we come back to Child#constructor()
. Now step 4 is executed. Instance initializer is called here, and the member
is now true
. Then, we see two lines printed out. primary constructor
, and member = true
.
Then finally, we come back to the first constructor invoked. Child#constructor(Number)
. Following step 5, we see a line constructor with a parameter printed
out. As a result, you will see the following.
parent constructor
member = false
primary constructor
member = true
constructor with a parameter
This is the call stack.
Child#constructor(Number)
Child#constructor()
Parent#constructor()
Object#constructor()
Object#body()
Parent#body()
// println("parent constructor")
// printMember()
Child#body() // constructor without parameter's body
// println("primary constructor")
// printMember()
Child#body() // constructor with parameter's body
// println("constructor with a parameter")
Conclusion
Although many people get confused when asked without a computer, many developers know this instinctively. People just know that assigning a value in a constructor will naturally override the value assigned by the instance initializer. However, I believe knowing this by experience and by concrete rule & spec could show some difference on some occasions. I hope this can be helpful to you or the future myself 👍.