package com.ds.test; public class Upper { String upperString; public Upper() { Initializer.initialize(this); } }
package com.ds.test; public class Lower extends Upper { String lowerString = null; public Lower() { super(); System.out.println("Upper: " + upperString); System.out.println("Lower: " + lowerString); } public static void main(final String[] args) { new Lower(); } }
package com.ds.test; public class Initializer { static void initialize(final Upper anUpper) { if (anUpper instanceof Lower) { Lower lower = (Lower) anUpper; lower.lowerString = "lowerInited"; } anUpper.upperString = "upperInited"; } }What output is to be expected from running the
Lower
class?
In this very reduced example it is much easier to get a view of the whole situation - in reality where this occurred there was a lot more code to distract one's attention...
Anyway, this is what the output looks like:
Upper: upperInited Lower: null;While the little example uses Strings, the real code of
Initializer
had a delegate object registered with the equivalent of the
Lower
class - at least that was the intention. For some reason however did this not work when running the application. Instead, the default path was taken - the one for the delegate object being not set (
null
).
Now, change the code of
Lower
slightly:
package com.ds.test; public class Lower extends Upper { String lowerString; public Lower() { super(); System.out.println("Upper: " + upperString); System.out.println("Lower: " + lowerString); } public static void main(final String[] args) { new Lower(); } }The output is now:
Upper: upperInited Lower: lowerInitedNotice the difference in the code?
Yes, the
lowerString
field is no longer explicitly set to
null
. Why would this make a difference? Isn't the default value for reference type fields (such as
String
here)
null
anyway? Of course, it is. However it turns out that this tiny little change - which apparently would not change the code's behavior in any way - makes this thing fly or not fly.
So what is going on? It becomes clear when looking at the initialization order:
main()
calls theLower
constructor.- An instance of
Lower
is prepared. That means, all fields are created and populated with default values, i. e.null
for reference types,false
forboolean
s and so on. At this time, any inline assignments to the fields have not taken place! - The super-constructor is called. This is mandated by the language spec. So, before anything else happens,
Upper
's constructor is called. - The
Upper
constructor runs and hands a reference to the freshly created instance to theInitializer.initialize()
method. - The
Initializer
attaches newString
s to both fields. It does so by using a somewhat dirtyinstanceof
check - not a particularly good design pattern, but possible, nevertheless. Once that has happened, both theupperString
lowerString
references are no longernull
. - The
Initializer.initialize()
call finishes, as does theUpper
constructor. - Now it becomes interesting: Construction of the
Lower
instance continues. Assuming there is no explicit=null
assignment in thelowerString
field declaration, theLower
constructor resumes execution and prints out the two Strings that are attached to the fields.
However, if there is an explicit assignment tonull
, execution has a slightly different flow: Just after the super constructor is done, any variable initializers are executed (see section 12.5 of the Java Language Spec), before the rest of the constructor is run. In this case theString
reference that was previously assigned tolowerString
is now overwritten withnull
again! Only then does the rest of the constructor continue execution, now printinglowerString: null
.
Initializer
like this. It should not be aware of
Upper
's subclasses at all! Instead, if for some reason initialization of certain fields cannot be done in the
Lower
class itself, it will just require its own variant of some sort of initialization helper. In that case, it would really make no difference if you used
String lowerString;
or
String lowerString = null;
- just as it should be.