int second = 1 + first;
can legally refer to first
because first
's declaration precedes second
's declaration. However, it is illegal for int forwardReference = first;
to refer to first
-- because that would be a forward reference. To prove to yourself that you cannot make forward references to subsequently declared object fields from a previously declared object field initializer, uncomment the commented-out int forwardReference = first;
inObjectInitializationDemo3
and recompile.
Object block initializers
Object field initializers are sufficient for the initialization of object fields. However, they prove inadequate for more complex object initialization. To parallel class block initializers, Java supports the object block initializer. An object block initializer consists of an open brace character ({
), initialization code, and a close brace character (}
). Furthermore, an object block initializer appears within a class but not within any of that class's methods, as you can see in Listing 13:
Listing 13. ObjectInitializationDemo4.java
// ObjectInitializationDemo4.java import java.io.*; class ObjectInitializationDemo4 { { System.out.println ("Initializing object " + hashCode ()); int localVariable = 1; } ObjectInitializationDemo4 (String msg) { System.out.println (msg); // System.out.println (localVariable); } public static void main (String [] args) { ObjectInitializationDemo4 oid41 = new ObjectInitializationDemo4 ("1"); ObjectInitializationDemo4 oid42 = new ObjectInitializationDemo4 ("2"); } }
ObjectInitializationDemo4
's main()
method creates a pair of ObjectInitializationDemo4
objects. For each object, the object block initializer executes before the constructor. That initializer prints a message, which includes the object's hashcode, and then declares/initializes a local integer variable. (To prove to yourself that you cannot access a local variable from outside the scope of an object block initializer, uncomment the commented-out System.out.println (localVariable);
method call and recompile.) Once the object block initializer completes, the constructor executes. The following output shows that object block initializers execute before constructors:
Initializing object 2765838 1 Initializing object 4177328 2
In many situations, you will not use object block initializers because you can use constructors to perform complex initialization tasks. However, certain situations require object block initializers. For example, anonymous inner classes (which I explore in a future article) often require object block initializers to perform complex initialization tasks. Anonymous inner classes require object block initializers because anonymous inner classes have no names, constructors take on the names of their classes, and you cannot declare constructors in classes that have no names.
Object initialization and class hierarchies
How do object field initializers and object block initializers work in the context of a class hierarchy? For some insight, we need to think in terms of <init>
methods and refer back to Listing 11. That listing shows us a principle of initialization: If a subclass does not declare a constructor, the compiler produces a corresponding<init>
method that explicitly calls its superclass's default no-argument <init>
method. Furthermore, for each subclass constructor that explicitly calls a superclass constructor, the compiler inserts a call to the superclass constructor's equivalent<init>
method at the start of the subclass's <init>
method.
Following the call to a superclass's <init>
method, a subclass's <init>
method executes byte code instructions for each subclass object field initializer and object block initializer. Those byte code instructions execute the initializers in the same order as they appear in source code. Furthermore, the byte code instructions duplicate in each subclass constructor that calls a superclass constructor. As Listing 14 demonstrates, that duplication is necessary because the developer could specify a call to any of the subclass's constructors, and object field initializers and object block initializers must execute, regardless of the subclass constructor that the developer chooses to call:
Listing 14. ObjectInitializationDemo5.java
// ObjectInitializationDemo5.java class Parent { int x = 1; { System.out.println ("x = " + ++x); } Parent () { System.out.println ("Executing superclass constructor"); } } class ObjectInitializationDemo5 extends Parent { int a = 2; { System.out.println ("a = " + ++ a); } ObjectInitializationDemo5 () { System.out.println ("Executing subclass constructor"); } ObjectInitializationDemo5 (String msg) { System.out.println (msg); } public static void main (String [] args) { ObjectInitializationDemo5 oid51 = new ObjectInitializationDemo5 (); ObjectInitializationDemo5 oid52 = new ObjectInitializationDemo5 ("Executing other subclass constructor"); } }
ObjectInitializationDemo5
declares classes Parent
and ObjectInitializationDemo5
. Each class declares an object field initializer, an object block initializer, and a constructor. To prove to yourself the previously mentioned <init>
method calling order and that the object field/block initializer's byte code instructions duplicate, examine the following output:
x = 2 Executing superclass constructor a = 3 Executing subclass constructor x = 2 Executing superclass constructor a = 3 Executing other subclass constructor
The first four output lines reflect the initialization of the object referenced by oid51
, and the last four output lines reflect the initialization of the object referenced byoid52
. In what order does that initialization occur? Let's find out. To begin, becauseObjectInitializationDemo5
features no class field initializers or class block initializers, no class initialization takes place. Instead, execution begins with the main()
method.main()
creates an ObjectInitializationDemo5
object whose reference assigns to oid51
. Notice the call to the default ObjectInitializationDemo5()
constructor.
Before the ObjectInitializationDemo5()
constructor -- that is, ObjectInitializationDemo5
's no-argument <init>
method -- executes System.out.println ("Executing subclass constructor");
, the <init>
method calls Parent
's no-argument <init>
method. In turn,Parent
's no-argument <init>
method calls Object
's default no-argument <init>
method. Once Object
's default no-argument <init>
method completes, Parent
's <init>
method continues by executing byte code instructions that correspond to the = 1
object field initializer. Then, byte code instructions corresponding to System.out.println ("x = " + ++x);
in Parent
's object block initializer execute. Once that completes, byte code instructions that correspond to the Parent
constructor's System.out.println ("Executing superclass constructor");
method call then execute. Then Parent
's no-argument <init>
method completes, and control returns to ObjectInitializationDemo5
's <init>
method.
ObjectInitializationDemo5
's <init>
method continues by executing byte code instructions that correspond to the = 2
object field initializer. Next, byte code instructions corresponding to System.out.println ("a = " + ++ a);
inObjectInitializationDemo5
's object block initializer then execute. Finally, byte code instructions that correspond to the ObjectInitializationDemo5()
constructor'sSystem.out.println ("Executing subclass constructor");
method call then execute, andObjectInitializationDemo5
's no-argument <init>
method completes. Initialization continues with ObjectInitializationDemo5 oid52 = new ObjectInitializationDemo5 ("Executing other subclass constructor");
. (I leave tracing that initialization as an exercise for you to complete.)
It might surprise you to realize that a superclass object field/block initializer can access a field in a subclass. However, allowing that behavior is not a good idea because superclass initialization occurs before subclass initialization -- and the subclass fields thus contain only default values. Therefore, superclass initializer access to subclass fields produces incorrect results. For a demonstration, check out Listing 15:
Listing 15. ObjectInitializationDemo6.java
// ObjectInitializationDemo6.java class Parent { { System.out.println ("a = " + ((ObjectInitializationDemo6) this).a); } } class ObjectInitializationDemo6 extends Parent { int a = 2; public static void main (String [] args) { new ObjectInitializationDemo6 (); } }
When you run ObjectInitializationDemo6
, you get a = 0
as output. Clearly, a
does not contain 2
because ObjectIntializationDemo6
's object initializers have not yet run.
Before we leave our examination of object initialization and class hierarchies, it might interest you to know that there exists a situation in which you can declare a class hierarchy -- in which each class has its own initializers -- and, when you construct an object, no initializer executes. That situation arises when a subclass constructor A calls another constructor B in the same subclass (via this
). From a JVM perspective, that implies that a corresponding <init>
method A calls <init>
method B (in the same subclass). Java assumes that method B will call either an <init>
method that corresponds to a superclass constructor or another subclass constructor's <init>
method. As a result, <init>
method A begins with byte code instructions that call<init>
method B. However, following that call, <init>
method A does not execute byte code instructions that correspond to object field initializers and object block initializers. Instead, it executes byte code instructions that correspond to developer-specified Java source code. Method A doesn't need to execute those instructions that correspond to initializers because Java expects another constructor/<init>
method (which calls a superclass constructor) to perform that task. Without proper care, this scenario can lead to something quite bizarre, as Listing 16 illustrates:
Listing 16. ObjectInitializationDemo7.java
// ObjectInitializationDemo7.java class Parent { int a = 3; { System.out.println ("a = " + a); } } class ObjectInitializationDemo7 extends Parent { int b = 1; { System.out.println ("b = " + b); } ObjectInitializationDemo7 () { this (1); } ObjectInitializationDemo7 (int x) { this (); } public static void main (String [] args) { System.out.println (new ObjectInitializationDemo7 ().a); } }
ObjectInitializationDemo7
never executes the object field/block initializers in either the Parent
class or in the ObjectInitializationDemo7
class. This is because each constructor compiles to an <init>
method whose first byte code instructions recursively call the other <init>
method. That situation continues until the JVM reports a stack overflow error. If you happen to observe a disassembly ofObjectInitializationDemo7()
and ObjectInitializationDemo7(int x)
, you will not see any byte code instructions that execute the object field initializer and object block initializer in ObjectInitializationDemo7
. All you will see are byte code instructions that call the other <init>
method. You can ensure the execution of your classes' object field/block initializers by avoiding such architectural flaws.
Review
This article showed you how classes and objects initialize. You learned about various initializers, including class field initializers, class block initializers, object field initializers, and object block initializers. You also learned about the seemingly strange JVM-level <clinit>
and <init>
methods, and saw that they are not as strange as you might think. Finally, you looked under each method's hood and observed the actual Java byte code instructions that execute various initializers. And that gave you a deeper understanding of how initialization works in the context of class hierarchies. Hopefully, you can use that knowledge to avoid problems in which superclass initialization code attempts to access subclass fields prior to their initialization, and recursive constructor calls overflow the stack and perform no initialization.
I encourage you to email me with any questions you might have involving either this or any previous article's material. (Please, keep such questions relevant to material discussed in this column's articles.) Your questions and my answers will appear in the relevant study guides.
In next month's article, you will learn about garbage collection.
Jeff Friesen has been involved with computers for the past 20 years. He holds a degree in computer science and has worked with many computer languages. Jeff has also taught introductory Java programming at the college level. In addition to writing for JavaWorld, he wrote his own Java book for beginners -- Java 2 By Example (QUE, 2000) -- and helped write a second Java book, Special Edition Using Java 2 Platform (QUE, 2001). Jeff goes by the nickname Java Jeff (or JavaJeff). To see what he's working on, check out his Website at http://www.javajeff.com.