回归Java本质,今天在Dzone阅读了一篇关于
java对象实例初始化顺序的文章。说它有趣,是因为作者使用了一种并不太推荐的编码风格,只有用这种编码风格才能触发这个极为少见的 Java object initialization order 问题。如下:
Recently I came across an interesting problem whose solution eluded me at first glance. Consider these three classes:
--------------------------------------
01.
package
com.ds.test;
02.
03.
public
class
Upper {
04.
String upperString;
05.
06.
public
Upper() {
07.
Initializer.initialize(
this
);
08.
}
09.
}
--------------------------------------
01.
package
com.ds.test;
02.
03.
public
class
Lower
extends
Upper {
04.
05.
String lowerString =
null
; // 这里出问题!
06.
07.
public
Lower() {
08.
super
();
09.
System.out.println(
"Upper: "
+ upperString);
10.
System.out.println(
"Lower: "
+ lowerString);
11.
}
12.
13.
public
static
void
main(
final
String[] args) {
14.
new
Lower();
15.
}
16.
}
--------------------------------------
01.
package
com.ds.test;
02.
public
class
Initializer {
03.
static
void
initialize(
final
Upper anUpper) {
04.
if
(anUpper
instanceof
Lower) {
05.
Lower lower = (Lower) anUpper;
06.
lower.lowerString =
"lowerInited"
;
07.
}
08.
anUpper.upperString =
"upperInited"
;
09.
}
10.
}
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:
--------------------------------------
1.
Upper: upperInited
2.
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:
--------------------------------------
01.
package
com.ds.test;
02.
03.
public
class
Lower
extends
Upper {
04.
05.
String lowerString;
06.
07.
public
Lower() {
08.
super
();
09.
System.out.println(
"Upper: "
+ upperString);
10.
System.out.println(
"Lower: "
+ lowerString);
11.
}
12.
13.
public
static
void
main(
final
String[] args) {
14.
new
Lower();
15.
}
16.
}
The output is now:
--------------------------------------
1.
Upper: upperInited
2.
Lower: lowerInited
Notice 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 the Lower 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 for booleans 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 the Initializer.initialize() method.
- The Initializer attaches new Strings to both fields. It does so by using a somewhat dirty instanceof check - not a particularly good design pattern, but possible, nevertheless. Once that has happened, both the upperString lowerString references are no longer null.
- The Initializer.initialize() call finishes, as does the Upper constructor.
- Now it becomes interesting: Construction of the Lower instance continues. Assuming there is no explicit =null assignment in the lowerString field declaration, the Lower constructor resumes execution and prints out the two Strings that are attached to the fields.
However, if there is an explicit assignment to null, execution has a slightly different flow: Just when 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 the String reference that was previously assigned to lowerString is not overwritten with null again! Only then does the rest of the constructor continue execution, now printing lowerString: null.
Apart from being a nice example for why it is handy to be aware of some of the minutiae of object creation (or knowing where to look in the JLS, printed or online) this shows why it is a bad idea to write the 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.
还有一篇介绍的文章【转】
类装载步骤
在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工作如下:
装载:查找和导入类或接口的二进制数据;
链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;
校验:检查导入类或接口的二进制数据的正确性;
准备:给类的静态变量分配并初始化存储空间;
解析:将符号引用转成直接引用;
初始化:激活类的静态变量的初始化Java代码和静态Java代码块。
其中 初始化(initialization)包含两部分:
1.类的初始化(initialization class & interface)
2.对象的创建(creation of new class instances)。
因为类的初始化其实是类加载(loading of classes)的最后一步,所以很多书中把它归结为“对象的创建”的第一步。其实只是看问题的角度不同而已。为了更清楚的理解,这里还是分开来。
顺序:
因为类的加载肯定是第一步的,所以类的初始化在前。大体的初始化顺序是:
类初始化 -> 子类构造函数 -> 父类构造函数 -> 实例化成员变量 -> 继续执行子类构造函数的语句
下面结合例子,具体解释一下。
1. 类的初始化(Initialization classes and interfaces)
其实很简单,具体来说有:
(a)初始化类(initialization of class),是指初始化static field 和执行static初始化块。
- public class Demo{
- //初始化static field,
- //其中= "initialization static field"又叫做static field initializer
- private static String str = "initialization static field";
-
- //初始化块,又叫做static initializer,或 static initialization block
- static {
- System.out.println("This is static initializer");
- }
- }
btw,有些书上提到static initializer 和 static field initializer 的概念,与之对应的还有 instance initializer 和 instance variable initializer。例子中的注释已经解释了其含义。
(b)初始化接口(initialization of interface),是指初始化定义在该interface中的field。
*注意*
1.
initialization classes 时,该class的superclass 将首先被初始化,但其实现的interface则不会。
initialization classes 时,该class的superclass,以及superlcass的superclass 会首先被递归地初始化,一直到java.lang.Object为止。
但initialiazation interface的时候,却不需如此,只会初始化该interface本身。
2. 对于由引用类变量(class field)所引发的初始化,只会初始化真正定义该field的class。
3. 如果一个static field是编译时常量(compile-time constant),则对它的引用不会引起定义它的类的初始化。
为了帮助理解最后两点,请试试看下面的例子:
Initialization类
- public class Initialization {
-
- static {
- System.out.println("Initialization Main class");
- }
-
- public static void main(String[] args) {
- System.out.println(Sub.y);
- System.out.println(Sub.x);
- System.out.println(Sub.z);
- }
- }
Sub类
- public class Sub extends Super {
- public static final int y = 2005;
- public static int z;
-
- static {
- System.out.println("Initialization Sub");
- }
- }
Super类
- public class Super {
- public static int x = 2006;
-
- static {
- System.out.println("Initialization Super");
- }
- }
输入结果
Initialization Main class
2005
Initialization Super
2006
Initialization Sub
0
从这个结果可以看到,
1. static块在类中会先执行;(实际上是先加载static成员变量,然后是static代码块)
2. static 的final变量不会引起类的初始化;
3. 子类Sub引用父类Super里面的变量,就会引起父类的初始化,但不会引起子类的初始化;
引用子类Sub的static变量,会引起父类的初始化,除final外。
4. static的成员变量也有默认值。
2. 对象的创建(creation of new class instances)
看例子来说明:
InitializationOrder类
- public class InitializationOrder {
- public static void main(String[] args) {
- SubClass sb = new SubClass();
- }
- }
SuperClass类
- public class SuperClass{
- static {
- System.out.println("SuperClass static");
- }
-
- SuperClass(String str){
- System.out.println(str);
- }
- }
Interface类
- interface Interface{
- static SuperClass su = new SuperClass("Interface new SuperClass");
- }
SubClass类
- public class SubClass extends SuperClass implements Interface{
-
- static {
- System.out.println("SubClass static");
- }
-
- private SuperClass su = new SuperClass("initialization variable");
-
- SubClass() {
- super("super");
- //此处开始初始化SubClass类的成员变量初始化
- new SuperClass("new SuperClass");
- System.out.println("SubClass init finished!");
- }
- }
输出结果
SuperClass static
SubClass static
super
initialization variable
new SuperClass
SubClass init finished!
解释一下:
1) Java虚拟机要执行InitializationOrder类中的static 方法main(),这引起了类的初始化。开始初始化InitializationOrder类。具体的步骤略去不说。
2) InitializationOrder类初始化完毕后,开始执行main()方法。语句SubClass sb = new SubClass()将创建一个SubClass对象。加载类SubClass后对其进行类初始化,因为Subclass有一个父类SuperClass,所以先初始化SuperClass类。于是看到输出“SuperClass static”。
3) SuperClass类初始化完毕后,开始初始化SubClass类,输出“SubClass static”。
4) 至此,类的加载工作全部完成。开始进入创建SubClass的对象过程。先为SubClass类和其父类SuperClass类分配内存空间,
这时Super su 被赋值为null。
5) 执行构造函数SubClass(),执行super(), 调用父类的构造函数,输出“super”。
6) 初始化SubClass类的成员变量su,输出“initialization variable”。
7) 继续执行构造函数的剩余部分,执行new SuperClass("new SuperClass"),输出“new SuperClass”,
这时Super su 被赋值新建对象的引用。
8) 而SubClass虽然实现了接口Interface,但是初始化它的时候并不会引起接口的初始化,所以接口Interface中的static SuperClass su = new SuperClass("Interface new SuperClass")自始至终都没有被执行到。
所以对象的创建,具体步骤如下:
(1) 所有的成员变量—包括该类,及它的父类中的成员变量--被分配内存空间,并赋予默认值。(这里是第一次初始化成员变量)
(2) 为所调用的构造函数初始化其参数变量。(如果有参数)
(3) 如果在构造函数中用this 调用了同类中的其他构造函数,则按照步骤(2)~(6)去处理被调用到的构造函数。
(4) 如果在构造函数中用super调用了其父类的构造函数,则按照步骤(2)~(6)去处理被调用到的父类构造函数。
(5) 按照书写顺序,执行instance initializer 和 instance variable initializer来初始化成员变量。(这里是第二次初始化成员变量)
(6) 按照书写顺序,执行构造函数的其余部分。
*******************
总结:
从类的初始化和对象的创建步骤,可以知道,一个类是先初始化static的变量和static句块,然后在分配该类以及父类的成员变量的内存空间,赋予默认值,然后开始调用构造函数。而子类和父类之间,则先初始化和创建父类,然后在初始化和创建子类的。
因此当我们引用类的static变量时,是没有分配该类以及父类的成员变量的内存空间的。