回归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 havenottaken 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 thereisan explicit assignment to null, execution has a slightly different flow: Just when the super constructor is done, any variable initializers are executed (seesection 12.5 of the Java Language Spec),beforethe 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初始化块。
- publicclassDemo{
- //初始化staticfield,
- //其中="initializationstaticfield"又叫做staticfieldinitializer
- privatestaticStringstr="initializationstaticfield";
- //初始化块,又叫做staticinitializer,或staticinitializationblock
- static{
- System.out.println("Thisisstaticinitializer");
- }
- }
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类
- publicclassInitialization{
- static{
- System.out.println("InitializationMainclass");
- }
- publicstaticvoidmain(String[]args){
- System.out.println(Sub.y);
- System.out.println(Sub.x);
- System.out.println(Sub.z);
- }
- }
Sub类
- publicclassSubextendsSuper{
- publicstaticfinalinty=2005;
- publicstaticintz;
- static{
- System.out.println("InitializationSub");
- }
- }
Super类
- publicclassSuper{
- publicstaticintx=2006;
- static{
- System.out.println("InitializationSuper");
- }
- }
输入结果
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类
- publicclassInitializationOrder{
- publicstaticvoidmain(String[]args){
- SubClasssb=newSubClass();
- }
- }
SuperClass类
- publicclassSuperClass{
- static{
- System.out.println("SuperClassstatic");
- }
- SuperClass(Stringstr){
- System.out.println(str);
- }
- }
Interface类
- interfaceInterface{
- staticSuperClasssu=newSuperClass("InterfacenewSuperClass");
- }
SubClass类
- publicclassSubClassextendsSuperClassimplementsInterface{
- static{
- System.out.println("SubClassstatic");
- }
- privateSuperClasssu=newSuperClass("initializationvariable");
- SubClass(){
- super("super");
- //此处开始初始化SubClass类的成员变量初始化
- newSuperClass("newSuperClass");
-
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变量时,是没有分配该类以及父类的成员变量的内存空间的。
分享到:
相关推荐
《Java程序设计基础教程》按内容分成五篇:Java 语言入门篇、面向对象篇、数据流应用篇、Java GUI编程篇和高级应用篇,共有二十四讲组成。
介绍java对象的创建、初始化、和引用。并分析一下JAVA中对象创建和初始化过程中涉及的相关概念问题。
详细讲解java类中静态变量,普通标量,对象、基本类型的初始化顺序。
NULL 博文链接:https://yuu1987.iteye.com/blog/1113142
java编程思想-初始化与清理了解this之后,你就能更全面地理解“静态(static)方法”的含义。静态方法就是没有this的方法。在“静态方法”的内部不能调用“非静态方法”,反过来倒是可以的。而且你可以在没有创建...
Java中StringBuffer对象的初始化.pdf 学习资料 复习资料 教学资源
通过一个实际问题引入,将源代码转换成JVM字节码后,对JVM执行过程的关键点进行全面解析,并在文中穿插入了相关JVM规范和JVM的部分内部理论知识,以理论与实际结合的方式介绍对象初始化和类初始化之间的协作以及可能...
java对象初始化问题
在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:装载、链接和初始化,网上关于Java中对象初始化顺序的文章很多,这篇文章我们将详细介绍Java中对象初始化顺序。有需要的可以参考学习。
ComplexNumber(double r,double i):构造函数,创建复数对象的同时完成复数的实部、虚部的初始化,r为实部的初值,i为虚部的初值。 getRealPart():获得复数对象的实部。 getImaginPart():获得复数对象的虚部。 ...
Java 的初始化问题和其他高级语言类似。一个明显不一样的地方是体现在它的类加 载过程。传统的编程语言包括C++等,程序是作为启动过程的一部分立刻被加载,而Java 的 类加载只在需要使用程序代码时才会被加载(每个...
当一个类被加载、连接、初始化后,它的生命周期就开始了,当代表该类的Class对象不再被引用、即已经不可触及的时候,Class对象的生命周期结束。那么该类的方法区内的数据也会被卸载,从而结束该类的生命周期。一个类...
“礼让”原则学习Java对象初始化顺序
Java对象初始化的多维度分析 (2).pdf
Java对象的创建与初始化.docx
Jmockdta是一款实现模拟JAVA类型或对象的实例化并随机初始化对象的数据的工具框架。单元测试的利器。
java对象的初始化顺序[参考].pdf
Java中创建初始化对象.pdf 学习资料 复习资料 教学资源
本文对Java如何执行对象的初始化做一个详细深入地介绍。有需要的小伙伴们可以参考。
1:虚拟机加载OOPDemo类(省略详细加载过程(装载、链接、初始化)),提取类型信息(具体保存哪些类型信息查看博文:Java虚拟机体系结构),存储到方法区中,对应图中的绿色矩形区域。 2:通过保存在方法区的字节...