这是一道阿里巴巴的关于Java对象初始化的面试题,但是需要面试者对Java中对象初始化有一个透彻的认识,首先这道题对我有所启发,所以我将记录下来,大家相互学习。
代码如下:
public class InitializeDemo {
private static int k = 1;
private static InitializeDemo t1 = new InitializeDemo("t1");
private static InitializeDemo t2 = new InitializeDemo("t2");
private static int i = print("i");
private static int n = 99;
static {
print("静态块");
}
private int j = print("j");
{
print("构造块");
}
public InitializeDemo(String str) {
System.out.println((k++) + ":" + str + " i=" + i + " n=" + n);
++i;
++n;
}
public static int print(String str) {
System.out.println((k++) + ":" + str + " i=" + i + " n=" + n);
++n;
return ++i;
}
public static void main(String args[]) {
new InitializeDemo("init");
}
}
结果如下:
1:j i=0 n=0
2:构造块 i=1 n=1
3:t1 i=2 n=2
4:j i=3 n=3
5:构造块 i=4 n=4
6:t2 i=5 n=5
7:i i=6 n=6
8:静态块 i=7 n=99
9:j i=8 n=100
10:构造块 i=9 n=101
11:init i=10 n=102
那么为什么会出现上面的结果呢。首先我们要写了解对象的初始化过程是按照什么顺序来进行的。
这里的核心理念有:
1.静态属性和静态代码块都是在类加载的时候初始化和执行,两者的优先级别是一致的,且高于非静态成员,执行按照编码顺序。
2.非静态属性和匿名构造器在所有的构造方法之前执行,两者的优先级别一致,执行按照编码顺序。
3.以上执行完毕后执行构造方法中的代码。
总结一句话就是:(静态属性=静态代码块)> (非静态属性 = 构造块)> 构造方法。
然后我们来逐步分析代码执行过程。
1.运行main方法的时候,JVM会调用ClassLoader类加载器来加载InitializeDemo类,那么一起源于这次加载。
2.上面有五个静态属性,所以会按顺序逐一初始化这五个静态属性。
3.private static int k = 1; 此时将k初始化为1。
4.private static InitializeDemo t1 = new InitializeDemo("t1");创建InitializeDemo对象,这里我的理解是创建对象,初始化非静态属性和构造代码块。因此先执行private int j = print("j");,打印出j,然后执行构造块,最后执行构造方法。
5.private static InitializeDemo t2 = new InitializeDemo("t2");同步骤4。
6.private static int i = print("i");打印i。
7.private static int n = 99;直到这一步,n才被赋值为99,之前是从默认的0开始++的。
8.静态属性初始化完毕,代码走到静态块,打印出静态块,此时n=99。
9.静态属性和静态块执行完毕,然后执行main方法中的代码new InitializeDemo("init");
10.main方法中创建对象,先初始化非静态属性,private int j = print("j");打印j,然后执行构造块,最后执行构造方法。
这里没有提到基类,如果遇到,加上一点,基类静态优先于衍生类静态执行;
继承对于初始化的影响
这里主要是理解编译时类型和运行时类型的不同,从这个不同中可以看出 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’s method”
因为,f 变量的运行时类型为Apple,info()是Apple重载的父类的一个方法。调用方法时由变量的运行时类型来决定。
⒊ 关于 this 关键字
当在29行new一个Apple对象,在30行调用 getSuper()方法时,最终是执行到第4行的 return this
this 的解释是:返回调用本方法的对象。它返回的类型是Fruit类型(见getThis方法的返回值类型),但实际上是Apple对象导致的getThis方法的调用。故,这里的this的声明类型是Fruit,而运行时类型是Apple
⒋ 关于 super 关键字
super 与 this 是有区别的。this可以用来代表“当前对象”,可用 return 返回。而对于super而言,没有 return super;这样的语句。
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类继承的。