关于类的初始化

网上看到一个非常有意思的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";
    }
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值