又被問到代碼執行順序?來看看init和clinit吧

在這裏插入圖片描述

前言

之前面試的時候經常會被問到,代碼的執行順序,偶爾一次蒙對了,還會繼續被懟能解釋下爲什麼麼?
在這裏插入圖片描述
下面就看看這到底怎麼解

init和clinit區別

  1. init是對象構造器方法,也就是說在程序執行 new 一個對象調用該對象類的 constructor 方法時纔會執行init方法,而clinit是類構造器方法,也就是在jvm進行類加載—–驗證—-解析—–初始化,中的初始化階段jvm會調用clinit方法。
  2. init是instance實例構造器,對非靜態變量解析初始化,而clinit是class類構造器對靜態變量,靜態代碼塊進行初始化。

詳解clinit

初始化階段就是執行類構造器()方法的過程,()並不是程序員在Java代碼中直接編寫 的方法,它是Javac編譯器的自動生成物。

  • clinit方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的 語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序決定的,靜態語句塊中只能訪問 到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊可以賦值,但是不能訪 問。

public class Test {
 	static { 
 		i = 0; // 給變量複製可以正常編譯通過 
 		System.out.print(i); // 這句編譯器會提示“非法向前引用” 
	}
	static int i = 1; 
}
  • <clinit>()方法與類的構造函數(即在虛擬機視角中的實例構造器<init>()方法)不同,它不需要顯 式地調用父類構造器,Java虛擬機會保證在子類的<clinit>()方法執行前,父類的<clinit>()方法已經執行 完畢。因此在Java虛擬機中第一個被執行的<clinit>()方法的類型肯定是java.lang.Object。

  • 由於父類的()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值 操作,如下面代碼中,字段B的值將會是2而不是1

    static class Parent {

        public static int A = 1;

        static {
            A = 2;
        }
    }

    static class Sub extends Parent {

        public static int B = A;
    }

    public static void main(String[] args) {
        System.out.println(Sub.B);
    }


  • <clinit>()方法對於類或接口來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變量的 賦值操作,那麼編譯器可以不爲這個類生成()方法。

  • 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法。但接口與類不同的是,執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法, 因爲只有當父接口中定義的變量被使用時,父接口才會被初始化。此外,接口的實現類在初始化時也 一樣不會執行接口的<clinit>()方法。

  • Java虛擬機必須保證一個類的<clinit>()方法在多線程環境中被正確地加鎖同步,如果多個線程同 時去初始化一個類,那麼只會有其中一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等 待,直到活動線程執行完畢<clinit>()方法。如果在一個類的<clinit>()方法中有耗時很長的操作,那就 可能造成多個進程阻塞。


static class DeadLoopClass {

        static { // 如果不加上這個if語句,編譯器將提示“Initializer does not complete normally” 並拒絕編譯
            if (true) {
                System.out.println(Thread.currentThread() + "init DeadLoopClass");
                while (true) {
                }
            }
        }
    }

    public static void main(String[] args) {
        Runnable script = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + "start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread() + " run over");
            }
        };
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]init DeadLoopClass

第二個線程會一直阻塞。

需要注意,其他線程雖然會被阻塞,但如果執行<clinit>()方法的那條線程退出<clinit>()方法後,其他線程喚醒後則不會再次進入<clinit>()方法。同一個類加載器下,一個類型只會被初始化一 次。

  • static修飾的字段在類加載過程中的準備階段被初始化爲0或null等默認值,而後在初始化階段(觸發類構造器<clinit>)纔會被賦予代碼中設定的值,如果沒有設定值,那麼它的值就爲默認值。
  • final修飾的字段在運行時被初始化(可以直接賦值,也可以在實例構造器中賦值),一旦賦值便不可更改;
  • static final修飾的字段在Javac時生成ConstantValue屬性,在類加載的準備階段根據ConstantValue的值爲該字段賦值,它沒有默認值,必須顯式地賦值,否則Javac時會報錯。可以理解爲在編譯期即把結果放入了常量池中。

init 方法執行順序

  1. 父類變量初始化
  2. 父類語句塊
  3. 父類構造函數
  4. 子類變量初始化
  5. 子類語句塊
  6. 子類構造函數

init加clinit的執行順序

  1. 父類靜態變量初始化(clinit)
  2. 父類靜態語句塊(clinit)
  3. 子類靜態變量初始化(clinit)
  4. 子類靜態語句塊(clinit)
  5. 父類變量初始化
  6. 父類語句塊
  7. 父類構造函數
  8. 子類變量初始化
  9. 子類語句塊
  10. 子類構造函數
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章