网上看到一个非常有意思的java面试题,感觉里面有不少可以讲的东西,拿来与大家分享一下:
题目很简洁就是给出下面这段程序的输出内容:
public class DispatchTest
{
public static void main(String[] args)
{
Base b = new Sub();
System.out.println(b.x);
}
}
class Base{
int x = 10;
public Base()
{
this.printMessage();
x=20;
}
public void printMessage()
{
System.out.println("Base.x = " + x);
}
}
class Sub extends Base
{
int x = 30;
public Sub()
{
this.printMessage();
x = 40;
}
public void printMessage()
{
System.out.println("Sub.x = " + x);
}
}
如果能够给出正确的答案说明对jvm的类初始化动作很熟悉,不过即使这样我也推荐您能够看下去。
这里我先不给出答案,直接分析,那么怎么分析呢,光盯着这几行代码肯定是得不到答案的,不过可以利用javap命令来或者jvm的虚拟指令。使用方法很简单先javac编译java源文件,然后javap -verbose classname > 1.txt将该文件的虚拟指令输出到1.txt文件中。
该文件类似下面的内容:
Compiled from "DispatchTest.java"
public class DispatchTest extends java.lang.Object
SourceFile: "DispatchTest.java"
minor version: 0
major version: 50
Constant pool:
const #1 = Method #8.#17; // java/lang/Object."<init>":()V
const #2 = class #18; // Sub
const #3 = Method #2.#17; // Sub."<init>":()V
const #4 = Field #19.#20; // java/lang/System.out:Ljava/io/PrintStream;
const #5 = Field #21.#22; // Base.x:I
const #6 = Method #23.#24; // java/io/PrintStream.println:(I)V
const #7 = class #25; // DispatchTest
const #8 = class #26; // java/lang/Object
const #9 = Asciz <init>;
const #10 = Asciz ()V;
const #11 = Asciz Code;
const #12 = Asciz LineNumberTable;
const #13 = Asciz main;
const #14 = Asciz ([Ljava/lang/String;)V;
const #15 = Asciz SourceFile;
const #16 = Asciz DispatchTest.java;
const #17 = NameAndType #9:#10;// "<init>":()V
const #18 = Asciz Sub;
const #19 = class #27; // java/lang/System
const #20 = NameAndType #28:#29;// out:Ljava/io/PrintStream;
const #21 = class #30; // Base
const #22 = NameAndType #31:#32;// x:I
const #23 = class #33; // java/io/PrintStream
const #24 = NameAndType #34:#35;// println:(I)V
const #25 = Asciz DispatchTest;
const #26 = Asciz java/lang/Object;
const #27 = Asciz java/lang/System;
const #28 = Asciz out;
const #29 = Asciz Ljava/io/PrintStream;;
const #30 = Asciz Base;
const #31 = Asciz x;
const #32 = Asciz I;
const #33 = Asciz java/io/PrintStream;
const #34 = Asciz println;
const #35 = Asciz (I)V;
{
public DispatchTest();
Code:
Stack=1, Locals=1, Args_size=1
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=2, Args_size=1
0: new #2; //class Sub
3: dup
4: invokespecial #3; //Method Sub."<init>":()V
7: astore_1
8: getstatic #4; //Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: getfield #5; //Field Base.x:I
15: invokevirtual #6; //Method java/io/PrintStream.println:(I)V
18: return
LineNumberTable:
line 6: 0
line 7: 8
line 8: 18
}
上面的虚拟指令是DispatchTest文件对应的指令,内容非常多,不过我们主要关注的是constant pool和后面的方法指令,每个class文件都会生成自己的常量池内容,存放着符号引用和其他一些内容,这些东西都是编译时产生的,常量池结合下面的指令构成运行时基本的元素。
public DispatchTest();很明显这是构造方法的入口,jvm规范规定5种情况会主动触发类的初始化动作。其中一种情况就包括遇到new操作符,类在初始化之前必须是已经完成了加载的过程,jvm将二进制的class文件加载到内存中,并完成验证,解析,初始化的动作。
DispatchTest的字节码并没有什么特殊的,来看下Sub的字节码
Compiled from "DispatchTest.java"
class Sub extends Base
SourceFile: "DispatchTest.java"
minor version: 0
major version: 50
Constant pool:
const #1 = Method #13.#23; // Base."<init>":()V
const #2 = Field #12.#24; // Sub.x:I
const #3 = Method #12.#25; // Sub.printMessage:()V
const #4 = Field #26.#27; // java/lang/System.out:Ljava/io/PrintStream;
const #5 = class #28; // java/lang/StringBuilder
const #6 = Method #5.#23; // java/lang/StringBuilder."<init>":()V
const #7 = String #29; // Sub.x =
const #8 = Method #5.#30; // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
const #9 = Method #5.#31; // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
const #10 = Method #5.#32; // java/lang/StringBuilder.toString:()Ljava/lang/String;
const #11 = Method #33.#34; // java/io/PrintStream.println:(Ljava/lang/String;)V
const #12 = class #35; // Sub
const #13 = class #36; // Base
const #14 = Asciz x;
const #15 = Asciz I;
const #16 = Asciz <init>;
const #17 = Asciz ()V;
const #18 = Asciz Code;
const #19 = Asciz LineNumberTable;
const #20 = Asciz printMessage;
const #21 = Asciz SourceFile;
const #22 = Asciz DispatchTest.java;
const #23 = NameAndType #16:#17;// "<init>":()V
const #24 = NameAndType #14:#15;// x:I
const #25 = NameAndType #20:#17;// printMessage:()V
const #26 = class #37; // java/lang/System
const #27 = NameAndType #38:#39;// out:Ljava/io/PrintStream;
const #28 = Asciz java/lang/StringBuilder;
const #29 = Asciz Sub.x = ;
const #30 = NameAndType #40:#41;// append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
const #31 = NameAndType #40:#42;// append:(I)Ljava/lang/StringBuilder;
const #32 = NameAndType #43:#44;// toString:()Ljava/lang/String;
const #33 = class #45; // java/io/PrintStream
const #34 = NameAndType #46:#47;// println:(Ljava/lang/String;)V
const #35 = Asciz Sub;
const #36 = Asciz Base;
const #37 = Asciz java/lang/System;
const #38 = Asciz out;
const #39 = Asciz Ljava/io/PrintStream;;
const #40 = Asciz append;
const #41 = Asciz (Ljava/lang/String;)Ljava/lang/StringBuilder;;
const #42 = Asciz (I)Ljava/lang/StringBuilder;;
const #43 = Asciz toString;
const #44 = Asciz ()Ljava/lang/String;;
const #45 = Asciz java/io/PrintStream;
const #46 = Asciz println;
const #47 = Asciz (Ljava/lang/String;)V;
{
int x;
public Sub();
Code:
Stack=2, Locals=1, Args_size=1
0: aload_0
将this压栈,该this指的是sub对象的引用
1: invokespecial #1; //Method Base."<init>":()V
通过this调用父类Base的构造函数
4: aload_0
this压栈
5: bipush 30
30压栈
7: putfield #2; //Field x:I
执行x=30
10: aload_0
this压栈
11: invokevirtual #3; //Method printMessage:()V
调用sub的printMessage
14: aload_0
this压栈
15: bipush 40
40压栈
17: putfield #2; //Field x:I
x=40
20: return
LineNumberTable:
line 27: 0
line 25: 4
line 28: 10
line 29: 14
line 30: 20
public void printMessage();
Code:
Stack=3, Locals=1, Args_size=1
0: getstatic #4; //Field java/lang/System.out:Ljava/io/PrintStream;
3: new #5; //class java/lang/StringBuilder
6: dup
7: invokespecial #6; //Method java/lang/StringBuilder."<init>":()V
10: ldc #7; //String Sub.x =
12: invokevirtual #8; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_0
16: getfield #2; //Field x:I
19: invokevirtual #9; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
22: invokevirtual #10; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: invokevirtual #11; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
28: return
LineNumberTable:
line 33: 0
line 34: 28
}
在子类Sub的成员变量未初始化时,调用了父类的构造函数<init>,那么看下父类的构造函数的字节码:
public Base();
Code:
Stack=2, Locals=1, Args_size=1
0: aload_0
this压栈,this指的是Base引用
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2; //Field x:I
x=10
10: aload_0
11: invokevirtual #3; //Method printMessage:()V
注意这里调用的时候会发生动态绑定,实际调用的是sub对象的printMessage方法
14: getstatic #4; //Field java/lang/System.out:Ljava/io/PrintStream;
17: aload_0
18: getfield #2; //Field x:I
21: invokevirtual #5; //Method java/io/PrintStream.println:(I)V
24: aload_0
25: bipush 20
x=20
27: putfield #2; //Field x:I
30: return
LineNumberTable:
line 13: 0
line 11: 4
line 14: 10
line 15: 14
line 16: 24
line 17: 30
由于动态绑定的特性,父类构造函数的this.printMessage会被绑定到子类的printMessage方法中,因为这里的this指针类型虽然是Base但运行时指向的是sub类型的对象,由于在此时子类的x变量还未完成赋值操作,因此会打印出0。然后父类中的成员变量x完成了赋值操作。继续调用子类的构造函数,此时x被赋值为30,继而打印出30。最后主函数中打印b.x,因为对于成员变量不存在动态绑定,所以b,x指的就是父类中的x值,因此输出20。
所以最终的输出如下:
Sub.x = 0
Sub.x = 30
20
总结
结合前面的一片关于类加载的文章,对类的加载和初始化做一个全面的总结。
触发类初始化的几种场景
1.遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
上面的四条指令都是字节码指令,可以理解为new,获取静态属性,设置静态属性,调用静态方法。
a. new 一个类的时候会发生初始化
b.调用类中的静态成员,除了final字段,看下面这个例子,final被调用但是没有初始化类
这里注意是除了final字段,因为final字段在编译期已经将值存储到了类的常量池中,因此引用final的静态成员是,不会导致初始化动作。
c. 调用某个类中的静态方法,那个类一定先被初始化了
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3.当初始化一个类的时候,如果发现其父类还没进行过初始化,则需要先触发其父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。
在初始化类之前,会先初始化父类,以此类推。初始化父类的意思在后面会讲到。而在真正初始化的时候,JVM会先进行加载字节码,连接动作,前两个动作保证所有的类成员变量(static修饰,除final修饰的外)都已经被分配内存,并赋予默认值。初始化动作包括初始化类变量,成员变量,执行构造函数,从字节码来看,类变量的初始化和static静态块被放到了static{}中,并且是按照类变量声明的顺序进行初始化。前面提到初始化父类就是先执行父类的static{},然后执行子类的static{}。成员变量的初始化被放到了构造函数中,并且构造函数默认第一行先调用父类的<init>(可以理解为构造函数,因此在进行成员变量初始化的时候,还是会先对父类的成员变量进行初始化),接着是成员变量的初始化动作(包括普通代码块{}中的内容),最后是构造函数中的初始化动作,初始化的过程都是按照这种顺序进行的,通常同类型的初始化都和变量的位置有关。
整个初始化的流程是:父类静态块---->子类静态块----------->父类成员变量-------->父类构造函数------------>子类成员变量---------------->子类构造函数
如果能给出下面代码的输出内容,说明对类加载,初始化和多态掌握的比较好了:
public class C {
public static void main(String[] args) {
new B();
}
}
class A {
private static String a = init2();
private String i=init1();
{
i = "A";
System.out.println(i);
}
static {
a = "Static A";
System.out.println(a);
}
public String init1()
{
System.out.println("A----init1");
return "NA";
}
public static String init2()
{
System.out.println("A----init2");
return "NA";
}
public A() {
System.out.println("Construct A");
}
}
class B extends A {
private static String b = init2();
private String j=init1();
{
j = "B";
System.out.println(j);
}
static {
b = "Static B";
System.out.println(b);
}
public B() {
System.out.println("Construct B");
}
public String init1()
{
System.out.println("B----init1");
return "NB";
}
public static String init2()
{
System.out.println("B----init2");
return "NB";
}
}