142、读程序分析执行结果
public class Son extends Father {
private int i = test();
private static int j = method();
static {
System.out.print(" [6] ");
}
Son(){
System.out.print(" [7] ");
}
{
System.out.print(" [8] ");
}
public int test(){
System.out.print(" [9] ");
return 1;
}
public static int method(){
System.out.print(" [10] ");
return 1;
}
public static void main(String[] args) {
Son s1 = new Son();
System.out.println();
Son s2 = new Son();
}
}
class Father{
private int i = test();
private static int j = method();
static {
System.out.print(" [1] ");
}
Father(){
System.out.print(" [2] ");
}
{
System.out.print(" [3] ");
}
public int test(){
System.out.print(" [4] ");
return 1;
}
public static int method(){
System.out.print(" [5] ");
return 1;
}
}
执行结果:
[5] [1] [10] [6] [9] [3] [2] [9] [8] [7]
[9] [3] [2] [9] [8] [7]
解析:本题主要考察了类的初始化过程、实例的初始化过程、方法的重写。
类初始化过程:
当一个类要创建实例的时候,需要先加载并初始化该类,并且main方法所在的类需要先加载和初始化,在这里并不冲突,我们的主方法是在Son类里面的,创建的也是Son类的实例。所以,在这里即使我们把 Son s1 = new Son();System.out.println();Son s2 = new Son();这三句删除了,只留下一个主方法,仍然会导致Son类的初始化,并且会在控制台打印如下信息: [5] [1] [10] [6] 至于为什么会这样打印,下面再说。
除了上面的情形会导致类初始化外,还有就是一个子类要初始化,需要先将其父类初始化。
一个类初始化实际上就是执行<clinit>()方法,每一个类都有这个方法,这个方法不是我们编写的,是编译器自动帮我们生成的,在字节码文件中可以看到。<clinit>()方法是由静态类变量显式赋值代码和静态代码块组成的,这两个就是谁在上面谁先执行,是按顺序执行的,并且只执行一次。
知道了上面这些,下面我们就可以分析当把Son s1 = new Son();System.out.println();Son s2 = new Son();这三句注释之后,为什么会打印 [5] [1] [10] [6]了:由于Son类中存在主方法,因此会导致Son类的初始化,又由于一个子类要初始化,需要先将其父类初始化,父类初始化就需要执行<clinit>()方法,<clinit>()方法又是由静态类变量显式赋值代码和静态代码块组成的,这两个就是谁在上面谁先执行,是按顺序执行的,并且只执行一次。在父类中存在private static int j = method();这一句就是静态类变量的显式赋值,除此之外父类还存在静态代码块,并且private static int j = method();在上面,先执行,所以程序会调用父类的method方法给j赋值,调用父类的method方法的时候打印出了[5] ,执行完静态类变量j的显式赋值代码之后,再去执行父类的静态代码块,爽快的就把[1] 给打印出来了。至此,完成了父类的初始化,同理开始执行子类的初始化,先执行子类静态类变量j显式赋值代码,再去执行静态代码块,所以先打印了 [10]后打印了 [6]。
以上只是分析了把Son s1 = new Son();System.out.println();Son s2 = new Son();这三句代码注释之后程序的执行过程。把这三句加上之后,程序的执行过程又变成了什么样呢?别着急,一步一步来。
实例初始化过程:
实例初始化就是执行<init>()方法,同样的,<init>()方法和类初始化时执行的<clinit>()方法都是编译器自动帮我们生成的,只有在字节码文件中才可以看到。但是,不同的是,类的<clinit>()方法只能有一个,而实例的<init>()方法可能会有多个重载,有几个构造器就会有几个<init>()方法。<init>()方法是由非静态实例变量显式赋值代码和非静态代码块、对应的构造器代码组成,执行顺序是非静态实例变量显式赋值代码和非静态代码块从上到下顺序执行,对应的构造器代码最后执行!并且,每new一次对象创建一次实例,调用一次对应构造器,就会执行一次对应的<init>()方法。最最最最重要的是:在子类的构造器中,无论写或者不写都一定会调用父类的构造器,子类的构造器中默认会有super();这一行代码!所以:子类的实例初始化会有下面的四部分构成:
- super() (一定是最前)
- 非静态实例变量显式赋值代码(和3按顺序)
- 非静态代码块(和2按顺序)
- 子类的无参构造(一定是最后)
下面我们接着解释程序执行结果,上面已经把类的实例化讲解完毕,前四个输出 [5] [1] [10] [6]我们已经明白为什么会这样输出了。
[9] [3] [2] [9] [8] [7]
[9] [3] [2] [9] [8] [7] 这又是为什么呢?
按照上面的解释:子类实例初始化,第一步先执行super(),就是先把父类的实例初始化,按照实例初始化的过程,先执行非静态实例变量显式赋值代码(由于在这个程序中父类没有父类了)即 i = test();调用父类的test()方法,按理说应该是第一个先打印[4]啊,为什么会打印[9]呢?这里面还涉及到一个点,那就是方法的重写Override!为什么在刚才分析类初始化的时候,method()方法没有讲重写呢,到实例初始化就要提test()方法的重写呢?这里就要讨论到哪些方法会被重写,哪些方法不会被重写了。
Java中哪些方法不被重写:
- final修饰的方法
- static修饰的方法
- private等子类中不可见的方法
在Java中,非静态方法前面其实都有一个默认的对象this,那么这个this在构造器(或者说<init>()方法里面)它表示的是正在创建的对象,因为在这里,我们创建的子类Son对象,所以这里test()执行的是子类重写的代码(这里是面向多态的一个体现),所以i = test();执行的是子类重写的test()方法,所以第一个输出的是[9]而不是我们认为的[4]。总结:子类如果重写了父类的方法,通过子类对象调用的一定是子类重写过的代码。
父类执行完非静态实例变量显式赋值代码之后(就是打印完[9]之后),就开始执行父类的非静态代码块,所以下一个打印的是[3],最后再执行父类的无参构造,打印输出[2]。
然后开始执行子类的非静态实例变量显式赋值代码,打印输出[9],紧接着执行子类的非静态代码块,打印输出[8],最后执行子类的无参构造,打印输出[7]。
以上就完全解释了为什么第一行打印的是:
[5] [1] [10] [6] [9] [3] [2] [9] [8] [7]
[5] [1] [10] [6] 是父类和子类的类初始化打印出来的,[9] [3] [2] [9] [8] [7] 是实例初始化打印出的。第二行为什么又打印了一遍
[9] [3] [2] [9] [8] [7] 呢?
那是由于每new一次对象创建一次实例,调用一次对应构造器,就会执行一次对应的<init>()方法。
本例子中new了两个子类Son实例对象,所以把[9] [3] [2] [9] [8] [7]打印了两遍。
你看明白了吗?欢迎下方留言讨论呦!