
?在6.1小節中曾經講過:創建對象前會完成類加載的操作。實際上,如果在程序中使用new關鍵字來創建一個對象,虛擬機會在創建對象之前需要完成一系列準備工作,類的加載只是這些工作中的一步。具體來說,這一系列工作可以分為類的加載、連接和初始化三步。多數情況下虛擬機都是連續完成這些工作的,因此這三個步驟也可以統稱為“類的加載”或“類的初始化”,本小節將詳細講解這些步驟的過程和原理。
在Java語言中,每一個類都會編譯成一個獨立的字節碼文件(.class文件),因此字節碼文件中記錄著類的各種信息,包括類有哪些屬性,屬性的類型和名稱是什么,訪問度是什么等等。類的加載就是把字節碼文件讀入內存的操作,把字節碼讀入內存是為了獲得記錄在字節碼文件中關于類的所有信息。類的加載會在很多情況下進行,最典型的情況就是程序中第一次創建某個類的對象時會進行類的加載。當把一個類的信息讀取到內存中后,虛擬機會把這個類的信息保存在java.lang包下的Class類的對象中。此處特別需要提醒初學Java的讀者:“Class”是一個類的名稱,它與表示類的、首字母為小寫的“class”關鍵字是不同的。由此可見:Java虛擬機內存中,每一個Class類對象都保存了一個類(或接口、枚舉)的信息。?
類的加載是由“類加載器”完成的。類加載器通常都是由Java虛擬機產品提供的,實際上程序員也可以通過繼承ClassLoader類來實現自己的類加載器。類加載器能夠加載不同來源的字節碼文件,例如可以加載工程文件夾中的那些字節碼文件,也可以加載.jar文件中那些被打包的字節碼文件,甚至可以加載來自于網絡的字節碼文件。類加載器除了可以完成加載這個操作以外,還可以對一個java源文件進行動態編譯并執行加載。?
(資料圖片)
當類被加載之后,系統為之生成一個對應的Class對象,接著將會進入連接階段,連接階段負責把類的二進制數據合并到JRE中。類連接又可分為如下三個階段。?
驗證:驗證階段的主要工作檢驗被加載的類是否有正確的內部結構,并和其他類協調一致?準備:類準備階段負責為類的靜態屬性分配內存,并設置默認初始值?解析:將類的二進制數據中的符號引用替換成直接引用?類的連接完成后,就要進行類的初始化。初始化階段主要負責對類的靜態屬性進行初始化。連接階段只是對類的靜態屬性賦予默認值,而初始化階段則是為類的靜態屬性賦予程序員指定的初始值。程序員對類的靜態屬性指定初始值有兩種方式:1、定義類時指定靜態屬性初始值。2、使用靜態塊指定靜態屬性的初始值,例如:?
public class Test{? static int a = 5;//①? static int b;? static int c;? static{? b = 6;//② }?}?
在以上代碼中,語句①在定義類時指定了靜態屬性a的初始值,而語句②是在靜態塊中指定了靜態屬性的初始值。語句①和②都會被當作類的初始化語句,虛擬機會按從上到下的順序執行這些初始化語句。初始化語句甚至可以出現在靜態屬性的定義語句之前,下面的【例19_01】就能夠說明類的初始化過程。?
【例19_01 類的初始化1】
Exam19_01.java?
public class Exam19_01 { static{ b = 6;//① } static int a = 5; static int b = 9;//② public static void main(String[] args) { System.out.println(Exam19_01.b); }}
【例19_01】中,語句①是對靜態屬性b的初始化,這條語句出現在定義靜態屬性b的語句②之前,但編譯器并不會因此報錯。語句①把b的初始值設置為6,更靠下的語句②把b的值設置為9,按照從上到下的初始化順序,語句②會把語句①對b設置的初始值修改為9,因此【例19_01】運行的結果是在控制臺上輸出9,讀者可以自行運行這個程序以觀察初始化語句的執行效果。?
需要注意:子類的初始化總是晚于父類的初始化的,因此一個類在完成初始化之前虛擬機會先初始化其父類,如果父類還有父類,則更早初始化父類的父類,以此類推。?
前文介紹過:多數情況下類的加載、連接和初始化都是連續完成的,但并不是每次類的加載都會引起類的初始化,下列幾種情況會引起類的加載并會在加載和連接之后同時完成初始化。?
創建類的對象,具體方式包括:使用new關鍵字來創建對象,通過反射來創建對象,通過反序列化的方式來創建對象。?調用某個類的靜態方法。?訪問某個類或接口的類靜態屬性,或為該靜態屬性賦值。?使用反射方式來強制創建某個類或接口對應的Class的對象。例如:?Class.forName("Person");如果系統還未初始化Person類,則這行代碼將會導致該Person類被初始化,并返回Person類對應的Class類的對象。關于Class的forName方法請參考18.3節。?初始化某個類的子類。當初始化某個類的子類時,該子類的所有父類都會被初始化。?直接使用java.exe命令來運行含有main()方法的類,當運行這個類時程序會先初始化該類。?在學習過程中,如果希望檢驗某個類有沒有被初始化,只需要在這個類中添加一個靜態塊,如果靜態塊中的代碼被執行,說明這個類被初始化了,否則說明這個類沒有被初始化。對于一個final修飾的靜態屬性而言,如果該靜態屬性的值在編譯時就可以確定下來,那么這個靜態屬性會在編譯時直接被替換成一個常量,因此即使程序使用該靜態屬性也不會導致該類的初始化。反之,如果final 修飾的靜態屬性的值不能在編譯時確定下來,也就是說必須等到運行時才可以確定該靜態屬性的值,那么程序中訪問這個靜態屬性會導致該類被初始化。下面的【例19_02】就很好的展示了這個特性。?
【例19_02 類的初始化2】?
Exam19_02.java?
class A{ //編譯時就能確定值的final靜態屬性 static final String str = "我喜歡學Java"; static { System.out.println("A類被初始化"); }}class B{ //運行時才能確定值的final靜態屬性 static final String str = System.currentTimeMillis()+""; static { System.out.println("B類被初始化"); }}public class Exam19_02 { public static void main(String[] args) { System.out.println(A.str); System.out.println(B.str); }}
【例19_02】中,A類和B類各有一個被final關鍵字修飾的靜態屬性str。A類的靜態屬性str在編譯時就能確定值,而B類的str值是當前系統時間和一個空字符拼接的結果,因此這個值只有在運行時才能確定。main()方法中輸出了A類和B類的str,【例19_02】的運行結果如圖19-1所示。?
圖19-1【例19_02】運行結果?
從圖19-1可以看出:輸出A類的str之前沒有執行A類靜態塊中的代碼,這證明調用在編譯階段能夠確定值的靜態屬性不會引起類的初始化。?
本文字版教程還配有更詳細的視頻講解,小伙伴們可以??點擊這里??觀看。