前言
让我想好好理理这个知识点,主要还是因为看了下面这道经典面试题(出处实在找不到),本来自信满满,写完一看,居然个答案不一样。我还试图想抓出答案的漏洞,所以就有这一篇文章,当然最后还是我被打脸了。
所以在原有面试题的基础上,再增加了一点点料,加大点难度,愉悦的开始。
面试题
首先有一个父类Father
public class Father { private int i = test(); private static int j = method(); static { System.out.println("(1)"); } Father() { System.out.println("(2)"); } { System.out.println("(3)"); } public int test() { System.out.println("(4)"); return 1; } public static int method() { System.out.println("(5)"); return 1; } }
然后又一个继承于Father类的子类Son,其中main方法在这个问题中我们称之为TEST-A方法
public class Son extends Father { private int i = test(); private static int j = method(); static { System.out.println("(6)"); } Son() { System.out.println("(7)"); } { System.out.println("(8)"); } public int test() { System.out.println("(9)"); return 1; } public static int method() { System.out.println("(10)"); return 1; } // TEST-A public static void main(String[] args) { System.out.println("(11)"); Son son = new Son(); System.out.println(); Son son1 = new Son(); } }
这是一个测试类Test,其中main方法在这个问题中我们称之为TEST-B方法
public class test { // TEST-B public static void main(String[] args) { System.out.println("(11)"); Son son = new Son(); System.out.println(); Son son1 = new Son(); } }
问题是,执行TEST-A,输出结果是怎样的?执行TEST-B,输出结果一样吗?
如果思考完想看答案可以再往下滑
(放张图片避免不小心看到答案)
答案
TEST-A:(5)(1)(10)(6)(11)(9)(3)(2)(9)(8)(7)
(9)(3)(2)(9)(8)(7)
TEST-B:(11)(5)(1)(10)(6)(9)(3)(2)(9)(8)(7)
(9)(3)(2)(9)(8)(7)
分析
首先这个问题涉及到的知识点有:
-
类初始化的时机
-
类的初始化过程
-
对象的初始化过程
类初始化的时机
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历七个阶段:加载、验证、准备、解析、初始化、使用和卸载。
那我们的类是何时被初始化的呢?
此时就要祭出《java虚拟机规范》中严格规定的、有且仅有这六种情况需要立即对类进行初始化操作:
-
遇到new、getstatic、putstatic或者invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
-
使用new关键字实例化对象的时候
-
读取或者设置一个类型的静态字段时(被final修饰,已在编译期将结果放入常量池的静态字段除外)
-
调用一个类型的静态方法的时候
-
-
使用java.lang.reflect包的方法对类型进行反射调用的时候,如果没有进行过初始化,则需要先进行初始化
-
当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
-
当JVM启动的时候,用户需要指定一个要执行的主类(包含main方法的类)虚拟机会先初始化这个类
-
当使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getstatic、REF_putstatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
-
当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,则该接口要在其之前被初始化
从这个例子出发,TEST-A与执行TEST-B两个的不同之处在于,类初始化时机的不同。
-
TEST-A
是将main方法定义在Son类中,所以看到上述的第4条,我们就会发现,Son类的加载会优先于Son类中main方法的执行,所以Son类的加载会在输出(11)前进行。
-
TEST-B
是将main方法定义在Test类中,所以不满足上述的第4条,且看到第1条,就可以发现,System.out.println("(11)"); 是先于 Son类的new,所以,Son类的加载会在输出(11)后再进行。
类的初始化过程
类的初始化阶段是类加载过程的最后一个步骤,而先前的四个阶段,除了在加载阶段可以通过自定义类加载器的方式局部参与外,其实都是完全由java虚拟机来主导控制,初始化阶段才是真正将主导权交给应用程序,这部分的过程比其他类加载过程更贴近于普通程序开发人员的实际工作。
初始化阶段也就是执行类构造器< clinit >()方法的过程,< clinit >()方法并不是我们直接在java代码中编写的方法,而是javac编译器的自动生成物;编译器会自动收集所有类变量的赋值动作和静态语句块(static{}块)中的语句合并在一起,收集的顺序由语句在源文件中出现的顺序决定,这也就决定了静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,只允许在前面的静态语句块进行赋值,但不能访问。
重点来了
< clinit >()方法与类的构造函数(即在虚拟机视角中的实例构造器< init >()方法)不同,它不需要显示地调用父类构造器,java虚拟机会保证在子类的< clinit >()方法执行前,父类的< clinit >()方法已经执行完毕。
这句话也在对应必须进行初始化操作的情况中的第3条:当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
从这个例子出发,我们看到 TEST-A 的输出结果中这部分固定的执行顺序:(5)(1)(10)(6),(5)(1)对应着的是Father类,(10)(6)对应着的是Son类,Father类优先于Son类执行,执行顺序符合上述说法。
public class Father { private int i = test(); private static int j = method(); static { System.out.println("(1)"); } .... public static int method() { System.out.println("(5)"); return 1; } }
(5)(1)对应着的是Father类中,我们能看到对静态变量i的赋值是依靠method方法赋值的,且该操作在源文件中出现的顺序是优于静态语句块的,所以在初始化过程中,看语句的顺序即可判断执行顺序
(10)(6)同理不再展开。
对象的初始化过程
当我们new一个对象时,首先会按照源文件中的顺序执行类中非静态对象和非静态代码块,之后再执行类的构造函数。
当我们new一个子类对象的时候,可想而知,由于继承关系的原因,我们可以在子类中获取父类的非静态对象和非静态方法,当然执行优先级也还是父类需要先于子类进行对象的初始化。
重点在于,Father类中的test()方法被Son类重写了
那Son类调用super()进行父类对象初始化时,父类的非静态对象调用的是父类本身的方法,还是子类重写的方法呢?
答案:子类重写父类方法,调用时会调用子类重写之后的方法
所以,那个例子在对象初始化过程中输出的 (9)(3)(2)(9)(8)(7)中, (9)(3)(2)对应着Father对象初始化依次调用非静态对象、非静态代码块、构造函数。(9)(8)(7)同理。