测试代码
有两个类 Father
、Son
,其中有实例变量、类变量、构造代码块、静态代码块、构造方法
class Father {
public Integer test = 100;
public 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;
}
}
public class Son extends Father {
public Integer test = 200;
public Father father = new Father();
public int i = test();
private static int j = method();
static {
System.out.print("(6)");
}
Son() {
System.out.print("(7)");
}
{
System.out.print("(8)");
j = 100;
}
@Override
public int test() {
System.out.print("(9)");
return 2;
}
public static int method() {
System.out.print("(10)");
return 1;
}
}
public class Test {
public static void main(String[] args) {
Son s1 = new Son();
System.out.println();
Son s2 = new Son();
System.out.println();
System.out.println(s1.test);
System.out.println(((Father)s1).test);
System.out.println(s1.i);
System.out.println(((Father)s1).i);
System.out.println(s1.test());
System.out.println(((Father)s1).test());
}
}
执行结果
(5)(1)(10)(6)(9)(3)(2)(4)(3)(2)(9)(8)(7)
(9)(3)(2)(4)(3)(2)(9)(8)(7)
200
100
2
2
1
(9)2
(9)2
1、Son s1 = new Son();
时需要加载类Son
,又因为Son
继承与Father
,所以先加载父类Father
,按顺序执行Father
中的静态代码,也就是以下的代码,由于方法method()
是静态方法,静态方法不参与多态,所以执行Father.method()
,执行结果:(5)(1)
class Father {
// ①
private static int j = method();
// ②
static {
System.out.print("(1)");
}
public static int method() {
System.out.print("(5)");
return 1;
}
}
2、加载完Father
之后继续加载子类Son
,按顺序执行Son
中的静态代码,执行结果:(10)(6)
public class Son extends Father {
// ①
private static int j = method();
// ②
static {
System.out.print("(6)");
}
public static int method() {
System.out.print("(10)");
return 1;
}
}
3、类加载完之后,开始创建对象,进行对象初始化,先执行父类初始化、再执行子类初始化,按顺序执行除构造方法以外的初始化代码,构造方法最后执行,执行结果:(9)(3)(2)(4)(3)(2)(9)(8)(7)
关于实例变量、构造代码块、构造方法的执行顺序可参考这一篇博客。
// 执行父类初始化
// 由于 ② test() 是普通方法,且被子类重写了,所以执行的是子类的 Son.test(),执行结果是 (9)(3)(2)
class Father {
// ①
public Integer test = 100;
// ②
public int i = test();
// ④
Father() {
System.out.print("(2)");
}
// ③
{
System.out.print("(3)");
}
// Father.test()
public int test() {
System.out.print("(4)");
return 1;
}
// Son.test()
@Override
public int test() {
System.out.print("(9)");
return 2;
}
}
// 执行子类初始化,执行结果是 (4)(3)(2)(9)(8)(7)
public class Son extends Father {
// ①
public Integer test = 200;
// ②,Father 类已经加载过来,此处只需执行 Father 类的初始化代码,也就是上面的 ①②③④
// 但与上面不同的是这里是直接 new Father(),而不是 new Father 的子类,所以 ② 的 test() 执行的是 Father.test(),结果是 (4)(3)(2)
public Father father = new Father();
// ③
public int i = test();
// ⑤
Son() {
System.out.print("(7)");
}
// ④
{
System.out.print("(8)");
j = 100;
}
@Override
public int test() {
System.out.print("(9)");
return 2;
}
}
4、Son s2 = new Son();
与上面的过程一样,只是省去了类加载的过程(步骤1、2),结果和步骤 3 一样:(9)(3)(2)(4)(3)(2)(9)(8)(7)
5、由于字段不参与多态,或者说字段的访问有其静态类型决定。所以 s1.test
、((Father)s1).test
分别打印了 200、100
以下摘自 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》311 页
正是因为 invokevirtual 指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
既然这种多态性的根源在于虚方法调用指令invokevirtual的执行逻辑,那自然我们得出的结论就只会对方法有效,对字段是无效的,因为字段不使用这条指令。事实上,在Java里面只有虚方法存在,字段永远不可能是虚的,换句话说,字段永远不参与多态,哪个类的方法访问某个名字的字段时,该名字指的就是这个类能看到的那个字段。当子类声明了与父类同名的字段时,虽然在子类的内存中两个字段都会存在,但是子类的字段会遮蔽父类的同名字段。为了加深理解,笔者又编撰了一份“劣质面试题式”的代码片段,请阅读代码清单8-10,思考运行后会输出什么结果。
6、 s1.i
、((Father)s1).i
都打印了 2,是因为 public int i = test();
,这里的 test()
调用的都是子类的版本 Son.test()
,s1.father.i
打印 1 是意料之中的
7、上面说过的字段不参与多态,但是实例方法是参与多态的,所以 test()
方法是根据其接受者的实际类型确定的,其实际类型是 Son
,所以调用了 Son.test()
,打印 (9)2
参考
- 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明