本文主要记录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类继承的。
四,参考资料
疯狂Java 突破程序员基本功的16课 第二章
Effective Java中文版 第2版 中文 PDF版 第二版第17条
本文向大家介绍Java中初始化块详解及实例代码,包括了Java中初始化块详解及实例代码的使用技巧和注意事项,需要的朋友参考一下 Java中初始化块详解 在Java中,有两种初始化块:静态初始化块和非静态初始化块. 静态初始化块:使用static定义,当类装载到系统时执行一次.若在静态初始化块中想初始化变量,那仅能初始化类变量,即static修饰的数据成员. 非静态初始化块:在每个对象生成时都会被执
问题内容: 有很多方法可以使用MockIto初始化模拟对象。其中最好的方法是什么? 1。 2。 [编辑] 3。 如果有其他方法可以建议我… 问题答案: 对于模拟初始化,使用或是严格等效的解决方案。从MockitoJUnitRunner的javadoc中: 当你已经在测试用例上配置了特定的运行器时,可以使用第一个解决方案(带有)。 第二个解决方案(带有)更经典,也是我的最爱。代码更简单。使用转轮提供
问题内容: 我在用Java工作。 我通常会这样设置一些对象: 问题是:在此示例中是否等于,按原样我可以假定对未初始化的对象进行空检查将是准确的? 问题答案: 正确,未显式初始化的引用类型的静态成员和实例成员都由Java 设置为。相同的规则适用于数组成员。 根据Java语言规范的第4.12.5节: 变量的初始值 程序中的每个变量在使用值之前都必须具有一个值: 每个类变量,实例变量或数组组件在创建时均
问题内容: 我想问一下Java初始化的格式。 我目前所知道的是: 等等 现在,在main类中,我想初始化一个,我不知道该怎么做? 问题答案: 首先,文件是对象类型,与int和double不同,它们是原始类型。我不确定您对Java有多熟悉,但是要创建一个对象,请使用该对象的构造函数。 File具有一个构造函数,该构造函数接收该文件在计算机上的位置字符串。
前面一节的 Fruit 类有两个实变量,分别表述水果的类型和状态.直到为这个类写了一个定制的inspect方法,我们方才了解它不会对一个缺乏属性的水果做出合理的解释.幸运的是,Ruby提供了一种允许实变量总是被初始化的方法. initalize方法 当Ruby创建一个新对象时,它总是会寻找一个名为 initialize 的方法并执行它.因此,我们可以简单通过一个initialize方法向实变量中加