Java類的連接與初始化 (及2013阿里初始化筆試題解析)

jopen 8年前發布 | 25K 次閱讀 Java開發

  Java虛擬機通過裝載、連接、初始化來使得一個Java類型可以被Java程序所使用,如下圖所示,其中連接過程又分為驗證、準備、解析三個部分。其中部分類的解析過程可以推遲到程序真正使用其某個符號引用時再去解析。
這里寫圖片描述
解析過程可以推遲到類的初始化之后再進行,但這是有條件的,Java虛擬機必須在每個類或接口主動使用時進行初始化。
以下為主動使用的情況:
(1).(無論直接通過new創建出來的,還是通過反射、克隆、序列化創建的)創建某個類新的實例
(2).使用某個類的靜態方法
(3).訪問某個類或接口的靜態字段
(4).調用JavaAPI中的某些反射方法
(5).初始化某個類的子類(要求其祖先類都要被初始化,否則無法正確訪問其繼承的成員)
(6).啟動某個標明為啟動類的類(含有main()方法)
主動使用會導致類的初始化,其超類均將在該類的初始化之前被初始化,但通過子類訪問父類的靜態字段或方法時,對于子類(或子接口、接口的實現類)來說,這種訪問就是被動訪問,或者說訪問了該類(接口)中的不在該類(接口)中聲明的靜態成員。
如:
Grandpa的定義如下:

package com.ice.passiveaccess;

public class Grandpa {
    static{
        System.out.println("Grandpa was initialized.");
    }
}

Parent的定義如下:

package com.ice.passiveaccess;

public class Parent extends Grandpa{
    static String language = "Chinese";
    static{
        System.out.println("Parent was initialized.");
    }
}

Cindy的定義如下:

package com.ice.passiveaccess;

public class Cindy extends Parent{
    static{
        System.out.println("Child was initialized.");
    }
}

現在通過Cindy訪問父類的language成員

package com.ice.passiveaccess;

public class PassiveAccessTest {
    public static void main(String args[]){
        System.out.println(Cindy.language);
    }
}

結果如下:
這里寫圖片描述
可見這是被動訪問,Cindy自身并沒有初始化

下面簡要介紹裝載、驗證與初始化過程:
1.裝載:
(1).找到該類型的class文件,產生一個該類型的class文件二進制數據流(ClassLoader需要實現的loadClassData()方法)
(2).解析該二進制數據流為方法區內的數據結構
(3).創建一個該類型的java.lang.Class實例
在加載器的相關代碼中可以看到,最終通過defineClass()創建一個Java類型對象(Class對象)。
2.驗證:
class文件校驗器需要四趟獨立的掃描來完成驗證工作,其中:
第一趟掃描在裝載時進行,會對class文件進行結構檢查,如
(1).對魔數進行檢查,以判斷該文件是否是一個正常的class文件
(2).對主次版本號進行檢查,以判斷class文件是否與java虛擬機兼容
(3).對class文件的長度和類型進行檢查,避免class文件部分缺失或被附加內容。
第二趟掃描在連接過程中進行,會對類型數據進行語義檢查,主要檢查各個類的二進制兼容性(主要是查看超類和子類的關系)和類本身是否符合特定的語義條件
(1).final類不能擁有子類
(2).final方法不能被重寫(覆蓋)
(3).子類和超類之間沒有不兼容的方法聲明
(4).檢查常量池入口類型是否一致(如CONSTANT_Class常量池的內容是否指向一個CONSTANT_Utf8字符串常量池)
(5).檢查常量池的所有特殊字符串,以確定它們是否是其所屬類型的實例,以及是否符合特定的上下文無關語法、格式
第三趟掃描為字節碼驗證,其驗證內容和實現較為復雜,主要檢驗字節碼是否可以被java虛擬機安全地執行。
第四趟掃描在解析過程中進行,為對符號引用的驗證。在動態連接過程中,通過保存在常量池的符號引用查找被引用的類、接口、字段、方法時,在把符號引用替換成直接引用時,首先需要確認查找的元素真正存在,然后需要檢查訪問權限、查找的元素是否是靜態類成員而非實例成員。
3.準備:
為類變量分配內存、設置默認初始值(內存設置初始值,而非對類變量真正地進行初始化,即類中聲明int i = 5,但實際上這里是分配內存并設置初始值為0)
4.解析:
在類的常量池中尋找類、接口、字段、方法的符號引用,將這些符號引用替換成直接引用
5.初始化:
對類變量賦予指定的初始值(這個時候int i = 5就必須賦予i以初值5)。這個初始值的給定方式有兩種,一種是通過類變量的初始化語句,一種是靜態初始化語句。而這些初始化語句都將被Java編譯器一起放在方法中。
如前面所述,一個類的初始化需要初始化其直接超類,并遞歸初始化其祖先類,初始化是通過調用類的初始化方法完成的。此外,對于接口,并不需要初始化其父接口,而只需要執行該接口的接口初始化方法就可以了。
注意:
(1). 在初始化階段,只會為類變量(靜態全局變量)進行初始化工作,并且當類變量聲明為final類型切初始化語句采用了常量表達式方式進行初始化賦值,那么, 也不會對其進行初始化,它將會直接被編譯器計算并保存在常量池中,并且對這些變量的使用也將直接將其變量值嵌入到字節碼中。
如UsefulParameter類如下:

Class UsefulParameter{ 
static final int height = 2; 
static final int width = height * 2; 
}

類Area的類變量初始化如下:

Class Area{ 
static int height = UsefulParameter.height * 2 ; 
static int width = UsefulParameter.width * 2; 
}

在Area的< clinit>中,將直接把2、4嵌入到字節碼中

(2).接口的初始化與類有所不同,在初始化階段,會為在接口中聲明的所有public、static和final類型的、無法被編譯為常量的字段進行初始化
6.類實例化
這里需要明白什么是類初始化,什么是類實例化,以及類的實例對象的初始化
如前面所述,類初始化時對類(靜態)變量賦予指定的初始值,類初始化之后就可以訪問類的靜態字段和方法,而訪問類的非靜態(實例)字段和方法,就需要創建類的對象實例,故類的實例化是在類的初始化之后,是在堆上創建一個該類的對象。
類的靜態方法和字段屬于類,作為類型數據保存在方法區,其生命周期取決于類,而實例方法和字段位于Java堆,其生命周期取決于對象的生命周期。
  類的初始化會從祖先類到子類、按出現順序,對類變量的初始化語句、靜態初始化語句塊依次進行初始化。而對類實例的初始化也類似,會從祖先類到子類、按出現順序,對類成員的初始化語句、實例初始化塊、構造方法依次進行初始化。
比如:

package com.ice.init;

public class Parent {
    public static int i = print("parent static:i");
    public int ii = print("parent:ii");

    static{
        print("父類靜態初始化");
    }

    {
        print("父類實例初始化");
    }

    public Parent(String str) {
        System.out.println("parent constructor:" + str);
    }

    public static int print(String str){
        System.out.println("initial:" + str);
        return i;
    }
}

子類Child如下:

package com.ice.init;

public class Child extends Parent{
    public static int i = print("child static:i");
    public int ii = print("child:ii");

    static{
        print("子類靜態初始化");
    }

    {
        print("子類實例初始化");
    }

    public Child(String str) {
        super(str);
        System.out.println("Child constructor:" + str);
    }

    public static int print(String str){
        System.out.println("initial:" + str);
        return i;
    }

    public static void main(String args[]){
        Child child = new Child("cindy");
    }
}

其初始化順序為:

Java編譯器為每個類生成了至少一個實例初始化方法< init >,一個< init >方法分為三部分: 另一個初始化方法< init >(),對任意實例成員的初始化的字節碼,構造方法的方法體的字節碼
< init >方法的調用如下:
若< init >指明從this()方法明確調用另一個構造方法,那么將調用另一個構造方法,否則,若該類有直接超類,那么,若< init >指明從super()方法明確調用其超類的構造方法,那么將調用超類的構造方法,否則,將默認調用超類的無參構造方法。這樣,將從其祖先類到該 類,分別完成對應的實例成員的初始化(可能被子類覆蓋)
接下來以一道題結束本節:
判斷輸出:

package com.ice.init;

class T  implements Cloneable{
      public static int k = 0;
      public static T t1 = new T("t1");
      public static T t2 = new T("t2");
      public static int i = print("i");
      public static int n = 99;

      public int j = print("j");
      {
          print("構造塊");
      }

      static {
          print("靜態塊");
      }

      public T(String str) {
          System.out.println((++k) + ":" + str + "    i=" + i + "  n=" + n);
          ++n; ++ i;
      }

      public static int print(String str){
          System.out.println((++k) +":" + str + "   i=" + i + "   n=" + n);
          ++n;
          return ++ i;
      }

      public static void main(String[] args){
          T t = new T("init");
      }
    }
題解如下

(1).首先T類被加載、連接后進行初始化,會先對字段k、t1、t2、i、n以及static塊進行初始化。 
(2).t1實例的初始化會初始化實例成員j,(實際上先進行父類實例內容的初始化)先調用靜態方法print,并執行實例初始化塊{},輸出: 
?1: j i=0 n= 0(i和n都還沒有初始化) 
?2:構造塊 i=1 n=1 
(3)隨后調用t1實例的構造函數,輸出: 
?3:t1 i=2 n=2 
(4).類似有t2實例的初始化: 
?4: j i=3 n= 3 
?5:構造塊 i=4 n=4 
?6:t2 i=5 n=5 
(5).i的初始化: 
?7.i i=6 n=6 
(6).n的初始化和靜態塊的初始化: 
?8.靜態塊 i=7 n=99(n已經被初始化) 
(7).t實例的初始化: 
?9.j i=8 n= 100 
?10.構造塊 i=9 n= 101 
?11.init i=10 n= 102
 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!