1. 程式人生 > >JAVA類初始化及例項初始化時內部的執行順序

JAVA類初始化及例項初始化時內部的執行順序

       記得剛畢業時,應聘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()>,那它包含什麼東西呢?

      可知,<clinit>()方法包含了類中所有的類變數(用static修飾的變數,方法中的區域性變數是不能用static修飾的,除非是final static這樣修飾)和靜態語句塊(static{}塊),它們在<clinit>()中的順序和程式中的寫的順序一樣。靜態語句塊中只能訪問到定義在它之前的變數(類變數),不能訪問定義在它之後的類變數,但可以賦值。在eclipse下寫點程式碼驗證了一下,發現編譯時就提示錯誤了,呵呵。

     上面說了<clinit>()中的順序和程式中的順序一樣的,如果靜態變數在靜態塊前,會先執行靜態變數。下面看個例子,直觀點。

public class A {
	public A(){
		System.out.println("我是A類構造器");
	}
	public A(String a){
		System.out.println("我是A類構造器2"+a);
	}
	{
		System.out.println("我是A類普通塊");
	}
	static{
		System.out.println("我是A類靜態塊");
	}
}


public class B {
  static A a=new A("   hello!!");
  static{
	  System.out.println("我是B類中的第一個靜態塊");
  }
}


public class Test {
	public static void main(String[] args){
		new B();
	}
}


    執行結果

     我是A類靜態塊
     我是A類普通塊
     我是A類構造器2   hello!!
     我是B類中的第一個靜態塊

  這裡沒使用基礎,但是卻先執行了A,可見上面的說法沒錯。

  來個初始化順序圖

  下面來看一題有趣的題
public class T {
 public static T t=new T();
 public static  int a=0;
 public static  int b=1;
  private T(){
	  a++;
	  b++;
	  System.out.println("構造方法中:a="+a+"    b="+b);
  }
  public static T getInstance(){
	  return t;
  }
  public static void main(String[] args){
	  T tt=T.getInstance();
	  System.out.println("main方法中:tt.a="+tt.a+"   tt.b="+tt.b);
	  
  }
}


你心中的答案是什麼呢????呵呵,來看看結果,是否和你心中的答案一樣。

構造方法中:a=1    b=1
main方法中:tt.a=0   tt.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

那麼如果我把程式中的變數順序調一下呢

public class T {
 public static  int a=0;
 public static  int b=1;
 public static T t=new T();
  private T(){
	  a++;
	  b++;
	  System.out.println("構造方法中:a="+a+"    b="+b);
  }
  public static T getInstance(){
	  return t;
  }
  public static void main(String[] args){
	  T tt=T.getInstance();
	  System.out.println("main方法中:tt.a="+tt.a+"   tt.b="+tt.b);  
  }
}


這時的結果,聰明的你,肯定非常清楚了。

構造方法中:a=1    b=2
main方法中:tt.a=1   tt.b=2

    上面的a,b都用了static修飾,那如果去掉static呢?

public class T {
	 public static T t=new T();
	 public   int a=0;
	 public   int b=1;
	  private T(){
		  a++;
		  b++;
		  System.out.println("構造方法中:a="+a+"    b="+b);
	  }
	  public static T getInstance(){
		  return t;
	  }
	  public static void main(String[] args){
		  T tt=T.getInstance();
		  System.out.println("main方法中:tt.a="+tt.a+"   tt.b="+tt.b);
		  
	  }
	}

構造方法中:a=1    b=2
main方法中:tt.a=1   tt.b=2

再調一下public static T t=new T();的位置

public class T {
	
	 public   int a=0;
	 public   int b=1;
	 public static T t=new T();
	  private T(){
		  a++;
		  b++;
		  System.out.println("構造方法中:a="+a+"    b="+b);
	  }
	  public static T getInstance(){
		  return t;
	  }
	  public static void main(String[] args){
		  T tt=T.getInstance();
		  System.out.println("main方法中:tt.a="+tt.a+"   tt.b="+tt.b);
		  
	  }
	}

構造方法中:a=1    b=2
main方法中:tt.a=1   tt.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就保持了下來。