本文主要记录JAVA中对象的初始化过程,包括实例变量的初始化和类变量的初始化以及final关键字对初始化的影响。另外,还讨论了由于继承原因,探讨了引用变量的编译时类型和运行时类型
一,实例变量的初始化
这里首先介绍下创建对象的过程:
类型为Dog的一个对象首次创建时,或者Dog类的static字段或static方法首次访问时,Java解释器必须找到Dog.class(在事先设定好的路径里面搜索);
找到Dog.class后(它会创建一个Class对象),它的所有static初始化模块都会运行。因此,static初始化仅发生一次——在Class对象首次载入的时候;
创建一个newDog()时,Dog对象的构建进程首先会在内存堆(Heap)里为一个Dog对象分配足够多的存储空间;
这种存储空间会清为零,将Dog中的所有基本类型(Primitive)设为它们的默认值(0用于数字,以及boolean和char的等价设定);
进行成员字段定义时发生的所有初始化都会执行;
执行构造函数。
然后,开始对实例变量进行初始化。一共有三种方式对实例变量进行初始化:
①定义实例变量时指定初始值
②非静态初始化块中对实例变量进行初始化
③构造器中对实例变量进行初始化
当new对象初始化时,①②要先于③执行。而①②的顺序则按照它们在源代码中定义的顺序来执行。
当实例变量使用了final关键字修饰时,如果是在定义该final实例变量时直接指定初始值进行的初始化(第①种方式),则:该变量的初始值在编译时就被确定下来,那么该final变量就类似于“宏变量”,相当于JAVA中的直接常量。
public class Test { public static void main(String[] args) { final String str1 = "HelloWorld"; final String str2 = "Hello" + "World"; System.out.println(str1 == str2);//true final String str3 = "Hello" + String.valueOf("World"); System.out.println(str1 == str3);//false } }
第8行输出false,是因为:第7行中str3需要通过valueOf方法调用之后才能确定。而不是在编译时确定。
再来看一个示例:
public class Test { final String str1 = "HelloWorld"; final String str2 = "Hello" + "World"; final String str3; final String str4; { str3 = "HelloWorld"; } { System.out.println(str1 == str2);//true System.out.println(str1 == str3);//true // System.out.println(str1 == str4);//compile error } public Test() { str4 = "HelloWorld"; System.out.println(str1 == str4);//true } public static void main(String[] args) { new Test(); } }
把第13行的注释去掉,会报编译错误“Theblankfinalfieldstr4maynothavebeeninitialized”
因为变量str4是在构造器中进行初始化的。而前面提到:①定义实例变量时直接指定初始值(str1和str2的初始化)、②非静态初始化块中对实例变量进行初始化(str3的初始化)要先于③构造器中对实例变量进行初始化。
另外,对于final修饰的实例变量必须显示地对它进行初始化,而不是通过构造器(<clinit>)对之进行默认初始化。
public class Test { final String str1;//compile error---没有显示的使用①②③中的方式进行初始化 String str2; }
str2可以通过构造器对之进行默认的初始化,初始化为null。而对于final修饰的变量 str1,必须显示地使用 上面提到的三种方式进行初始化。如下面的这个Test.java(一共有22行的这个Test类)
public class Test { final String str1 = "Hello";//定义实例变量时指定初始值 final String str2;//非静态初始化块中对实例变量进行初始化 final String str3;//构造器中对实例变量进行初始化 { str2 = "Hello"; } public Test() { str3 = "Hello"; } public void show(){ System.out.println(str1 + str1 == "HelloHello");//true System.out.println(str2 + str2 == "HelloHello");//false System.out.println(str3 + str3 == "HelloHello");//false } public static void main(String[] args) { new Test().show(); } }
由于str1采用的是第①种方式进行的初始化,故在执行15行:str1+str1连接操作时,str1其实相当于“宏变量”
而str2和str3并不是“宏变量”,故16-17行输出false
在非静态初始化代码块中初始化变量和在构造器中初始化变量的一点小区别:因为构造器是可以重写的,比如你把某个实例变量放在无参的构造器中进行初始化,但是在new对象时却调用的是有参数的构造器,那就得注意该实例变量有没有正确得到初始化了。
而放在非静态初始化代码块中初始化变量时,不管是调用有参的构造器还是无参的构造器,非静态初始化代码块都会执行。
二,类变量的初始化
类变量一共有两个地方对之进行初始化:
❶定义类变量时指定初始值
❷静态初始化代码块中进行初始化
不管new多少个对象,类变量的初始化只执行一次。
三,继承对初始化的影响
主要是理解编译时类型和运行时类型的不同,从这个不同中可以看出this关键字和super关键字的一些本质区别。
class Fruit{ String color = "unknow"; public Fruit getThis(){ return this; } public void info(){ System.out.println("fruit's method"); } } public class Apple extends Fruit{ String color = "red";//与父类同名的实例变量 @Override public void info() { System.out.println("apple's method"); } public void accessFruitInfo(){ super.info(); } public Fruit getSuper(){ return super.getThis(); } //for test purpose public static void main(String[] args) { Apple a = new Apple(); Fruit f = a.getSuper(); //Fruit f2 = a.getThis(); //System.out.println(f == f2);//true System.out.println(a == f);//true System.out.println(a.color);//red System.out.println(f.color);//unknow a.info();//"apple's method" f.info();//"apple's method" a.accessFruitInfo();//"fruit's method" } }
值得注意的地方有以下几个:
⒈第35行引用变量a和f都指向内存中的同一个对象,36-37行调用它们的属性时,a.color是red,而f.color是unknow
因为,f变量的声明类型(编译时类型)为Fruit,当访问属性时是由声明该变量的类型来决定的。
⒉第39-40行,a.info()和f.info()都输出“apple'smethod”
因为,f变量的运行时类型为Apple,info()是Apple重载的父类的一个方法。调用方法时由变量的运行时类型来决定。
⒊关于this关键字
当在29行new一个Apple对象,在30行调用getSuper()方法时,最终是执行到第4行的returnthis
this的解释是:返回调用本方法的对象。它返回的类型是Fruit类型(见getThis方法的返回值类型),但实际上是Apple对象导致的getThis方法的调用。故,这里的this的声明类型是Fruit,而运行时类型是Apple
⒋关于super关键字
super与this是有区别的。this可以用来代表“当前对象”,可用return返回。而对于super而言,没有returnsuper;这样的语句。
super主要是为了:在子类中访问父类中的属性或者在子类中调用父类中的方法而引入的一个关键字。比如第24行。
⒌在父类的构造器中不要去调用被子类覆盖的方法(Override),或者说在构造父类对象时,不要依赖于子类覆盖了父类的那些方法。这样很可能会导致初始化的失败(没有正确地初始化对象)
因为:前面第1点和第2点谈到了,对象(变量)有声明时类型(编译时类型)和运行时类型。而方法的调用取决于运行时类型。
当new子类对象时,会首先去初始化父类的属性,而此时对象的运行时类型是子类,因此父类的属性的赋值若依赖于子类中重载的方法,会导致父类属性得不到正确的初始化值。示例如下:
class Fruit{ String color; public Fruit() { color = this.getColor();//父类color属性初始化依赖于重载的方法getColor // color = getColor(); } public String getColor(){ return "unkonw"; } @Override public String toString() { return color; } } public class Apple extends Fruit{ @Override public String getColor() { return "color: " + color; } // public Apple() { // color = "red"; // } public static void main(String[] args) { System.out.println(new Apple());//color: null } }
Fruit类的color属性 没有正确地被初始化为"unknow",而是为 null
主要是因为第5行 this.getColor()调用的是Apple类的getColor方法,而此时Apple类的color属性是直接从Fruit类继承的。
四,参考资料
Effective Java中文版 第2版 中文 PDF版 第二版第17条