记得刚毕业时,应聘JAVA开发岗位,做招聘单位的笔试时,经常有JAVA类内部的执行顺序的考察,就是让你写出某个程序的打印结果的顺序,现在整理一下。
如果一个类,有构造器,普通块,静态块,那该类初始化时,它的执行顺序如何呢?如果它有父类,并且它的父类也有构造器,普通块,静态块呢?直接写个小程序,测一下,就一目了然。
public class A {
public A(){
System.out.println("我是构造器");
}
{
System.out.println("我是普通块");
}
static{
System.out.println("我是静态块");
}
public static void main(String[] args){
new A();
}
}
运行结果是:
我是静态块
我是普通块
我是构造器
顺序是如何的,就不用我多说了,一目了然。那么下面来看看,它编译后的样子,是不是顺序也是这样的。
Compiled from "A.java"
public class test.A extends java.lang.Object
SourceFile: "A.java"
minor version: 0
major version: 50
Constant pool:
const #1 = class #2; // test/A
const #2 = Asciz test/A;
const #3 = class #4; // java/lang/Object
const #4 = Asciz java/lang/Object;
const #5 = Asciz <clinit>;
const #6 = Asciz ()V;
const #7 = Asciz Code;
const #8 = Field #9.#11; // java/lang/System.out:Ljava/io/PrintStream;
const #9 = class #10; // java/lang/System
const #10 = Asciz java/lang/System;
const #11 = NameAndType #12:#13;// out:Ljava/io/PrintStream;
const #12 = Asciz out;
const #13 = Asciz Ljava/io/PrintStream;;
const #14 = String #15; // 我是静态块
const #15 = Asciz 我是静态块;
const #16 = Method #17.#19; // java/io/PrintStream.println:(Ljava/lang/String;)V
const #17 = class #18; // java/io/PrintStream
const #18 = Asciz java/io/PrintStream;
const #19 = NameAndType #20:#21;// println:(Ljava/lang/String;)V
const #20 = Asciz println;
const #21 = Asciz (Ljava/lang/String;)V;
const #22 = Asciz LineNumberTable;
const #23 = Asciz LocalVariableTable;
const #24 = Asciz <init>;
const #25 = Method #3.#26; // java/lang/Object."<init>":()V
const #26 = NameAndType #24:#6;// "<init>":()V
const #27 = String #28; // 我是普通块
const #28 = Asciz 我是普通块;
const #29 = String #30; // 我是构造器
const #30 = Asciz 我是构造器;
const #31 = Asciz this;
const #32 = Asciz Ltest/A;;
const #33 = Asciz main;
const #34 = Asciz ([Ljava/lang/String;)V;
const #35 = Method #1.#26; // test/A."<init>":()V
const #36 = Asciz args;
const #37 = Asciz [Ljava/lang/String;;
const #38 = Asciz SourceFile;
const #39 = Asciz A.java;
{
static {};
Code:
Stack=2, Locals=0, Args_size=0
0: getstatic #8; //Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #14; //String 我是静态块
5: invokevirtual #16; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 12: 0
line 3: 8
public test.A();
Code:
Stack=2, Locals=1, Args_size=1
0: aload_0
1: invokespecial #25; //Method java/lang/Object."<init>":()V
4: getstatic #8; //Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #27; //String 我是普通块
9: invokevirtual #16; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: getstatic #8; //Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #29; //String 我是构造器
17: invokevirtual #16; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: return
LineNumberTable:
line 5: 0
line 9: 4
line 6: 12
line 7: 20
LocalVariableTable:
Start Length Slot Name Signature
0 21 0 this Ltest/A;
public static void main(java.lang.String[]);
Code:
Stack=1, Locals=1, Args_size=1
0: new #1; //class test/A
3: invokespecial #35; //Method "<init>":()V
6: return
LineNumberTable:
line 16: 0
line 17: 6
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 args [Ljava/lang/String;
}
从第1行到45行,或者从47行到99行,都可以看出执行顺序。
从第1行到45行,可以看到,static{}块在JVM中会生成一个叫<clinit>的方法(第11行),普通块和构造方法合在一起会生成一个叫<init>的方法(第30行)。
总的来说:类初始化属于类加载的最有一个阶段(主要在方法区工作),会先执行<clinit>()(有静态变量和静态块组成,详细说明看后文);然后执行普通成员变量,当初始化实例时(也是对象初始化,实例初始化,相当于用new在堆中创建对象),会先执行<init>(),也就是构造方法(经过编译器处理后,普通块被放到构造方法中去了)。
普通块是不是被编译期嵌入到构造器中去了呢?下面写个程序对比一下,编译前和编译后代码的样子就知道了。
编译前的代码
public class B {
public static String s="abc";
static{
System.out.println("我是静态块");
}
public int a=1;
public B(){
System.out.println("我是构造器"+a);//打印后,发现a=1,而不是加载时a为默认值0,
// 说明普通成员的优先于构造方法执行
}
{
System.out.println("我是普通块");
}
public B(String ss){
int c=2;
System.out.println("我是构造器"+c);
}
public static void main(String[] args){
new B();
new B("");
}
}
编译后用反编译软件看到的代码
public class B
{
public static String s = "abc";
public int a = 1;
static
{
System.out.println("我是静态块");
}
public B()
{
System.out.println("我是普通块");
System.out.println("我是构造器" + this.a);
}
public B(String ss) {
System.out.println("我是普通块");
int c = 2;
System.out.println("我是构造器" + c);
}
public static void main(String[] args) {
new B();
new B("");
}
}
通过对比可看出,普通块都被编译期放到了所有的构造方法中,而且是方法体里面第一行。
那么如果父类中也有静态块,普通块,构造器呢?先看看程序。
public class P {
public P(){
System.out.println("我是父类构造器");
}
{
System.out.println("我是父类普通块");
}
static{
System.out.println("我是父类静态块");
}
}
public class A extends P{
public A(){
System.out.println("我是子类构造器");
}
{
System.out.println("我是子类普通块");
}
static{
System.out.println("我是子类静态块");
}
public static void main(String[] args){
new A();
}
}
运行结果是:
我是父类静态块
我是子类静态块
我是父类普通块
我是父类构造器
我是子类普通块
我是子类构造器
由于静态块是在类初始化的时候就执行了,所以最先输出,这点没问题。再看运行结果可知,JVM是先加载父类然后再加载子类,也就是说先执行父类的<clinit> 再执行子类的<clinit>;接着是对象初始化了,由运行结果可知,也是先实例初始化父类,然后再实例初始化子类。也许有人问,我没有new父类啊,父类怎么也实例初始化了?是因为当你new一个子类时,JVM会先检查子类是否有父类,有的话则先new父类。上面那题,执行new A()时,检查到有父类P,就先new P(),但是又检查到P有父类Object,所以又先new Object();最后的顺序是new Object()->new P()->new A();其实类初始化和实例初始化过程也是一样的,先父类,后子类。
一句话:先父类的<clinit>(),后子类的<clinit>();先父类的<init>(),后子类的<init>()
上面提到了<clinit()>,那它包含什么东西呢?
静态语句块中只能访问到定义在它之前的变量(类变量),不能访问定义在它之后的类变量,但可以赋值。在eclipse下写点代码验证了一下,发现编译时就提示错误了,呵呵。 |
上面说了<clinit>()中的顺序和程序中的顺序一样的,如果静态变量在静态块前,会先执行静态变量。下面看个例子,直观点。
运行结果 我是A类静态块 这里没使用基础,但是却先执行了A,可见上面的说法没错。 来个初始化顺序图 |
下面来看一题有趣的题 |
你心中的答案是什么呢????呵呵,来看看结果,是否和你心中的答案一样。 构造方法中:a=1 b=1 这就要说说类加载和类初始化,实例化赋值了。 1.JVM要执行main方法时,要先加载T类文件(class文件),这时的变量都有一个默认值。如程序中t=null,a=0(这个0是默认的0,不是程序上写的0),b=0 2.类初始化,先执行<clinit>(),而<clinit>()中有public static T t=new T();public static int a=0;public static int b=1。先执行第一句public static T t=new T(),执行这句,那就相当于new一个对象(实例化对象),它会先执行<init>(),接着执行private T()方法了,但是此时a=0,b=0;所以a++,b++后,a=1,b=1。和运行结果一致。然后再回到<clinit>()中执行public static int a=0(这个0就是程序上写的0),public static int b=1;这个时候a=0,b=1了,而不再是a=1,b=1了。 3.初始化结束了,然后执行到main方法中的T tt=T.getInstance(),这时tt指向的对象中的a=0,b=1 那么如果我把程序中的变量顺序调一下呢
这时的结果,聪明的你,肯定非常清楚了。 构造方法中:a=1 b=2 |
上面的a,b都用了static修饰,那如果去掉static呢?
构造方法中:a=1 b=2 |
再调一下public static T t=new T();的位置
构造方法中:a=1 b=2 |
发现结果都一致,为什么和a,b都加了static那2题不一样了呢?下面来说说a,b都不加 static修饰的这2题的执行顺序. 1.加载class文件时,成员变量(不管是类变量还是实例变量)都先给一个默认值,此时t=null,a=0,b=0 2.类初始化.<clinit>()中只有public static T t=new T();所以执行new T(),而执行new T()就是创建对象(实例化对象),此时接着执行<init>(),会先执行普通成员变量a=0,b=1,所以执行到这,a=0,b=1了。接着执行T()方法,a++,b++,这是a=1,b=2了。所以构造方法中打印a=1,b=2;然后,再返回<clinit>(),但是此时<clinit>()没有其他的了,所以a=1,b=2就保持了下来。 |