Java類初始化和實例化中的2個“雷區”。本站提示廣大學習愛好者:(Java類初始化和實例化中的2個“雷區”)文章只能為提供參考,不一定能成為您想要的結果。以下是Java類初始化和實例化中的2個“雷區”正文
在斟酌類初始化時,我們都曉得停止子類初始化時,假如父類沒有初始化要先初始化子類。但是工作並沒有一句話這麼簡略。
起首看看Java中初始化觸發的前提:
(1)在應用new實例化對象,拜訪靜態數據和辦法時,也就是碰到指令:new,getstatic/putstatic和invokestatic時;
(2)應用反射對類停止挪用時;
(3)現在始化一個類時,父類假如沒有停止初始化,先觸發父類的初始化;
(4)履行進口main辦法地點的類;
(5)JDK1.7靜態說話支撐中辦法句柄地點的類,假如沒有初始化觸提議初始化;
經由編譯後生成一個<clinit>辦法,類的初始化就在這個辦法中停止,該辦法只履行,由JVM包管這一點,並停止同步掌握;
個中前提(3),從辦法挪用的角度來看,是子類的<clinit>會在開端時遞歸的挪用父類的<clinit>,這相似與我們在子類結構器中必需起首挪用父類的結構器;
但須要留意的是“觸發”其實不是完成初始化,這意味著有能夠子類的初始化會提早於父類初始化停止,這就是“風險”的地點。
1. 一個類初始化的例子:
這個例子我應用一個核心類包括2個有繼續關系的靜態成員類,由於核心類的初始化和靜態成員類沒有因果關系,是以如許展現是平安和便利的;
父類A和子類B分離包括main函數,由下面的觸發前提(4)可知,經由過程分離挪用這個兩個main函數來觸發分歧的類初始化途徑;
這個例子的成績在於父類包括子類的static援用並在界說處停止初始化的成績:
public class WrapperClass { private static class A { static { System.out.println("類A初始化開端..."); } //父類包括子類的static援用 private static B b = new B(); protected static int aInt = 9; static { System.out.println("類A初始化停止..."); } public static void main(String[] args) { } } private static class B extends A { static { System.out.println("類B初始化開端..."); } //子類的域依附於父類的域 private static int bInt = 9 + A.aInt; public B() { //結構器依附類的static域 System.out.println("類B的結構器挪用 " + "bInt的值" + bInt); } static { System.out.println("類B初始化停止... " + "aInt的值:" + bInt); } public static void main(String[] args) { } } }
情形一:進口為類B的main函數時輸入成果:
/** * 類A初始化開端... * 類B的結構器挪用 bInt的值0 * 類A初始化停止... * 類B初始化開端... * 類B初始化停止... aInt的值:18 */
剖析:可以看到,main函數的挪用觸發了類B的初始化,進入類B的<clinit>辦法,類A作為其父類先開端初始化進入了A的<clinit>辦法,個中有一個語句new B();這時候會停止B的實例化,這是曾經在類B的<clinit>中了,main線程曾經取得鎖開端履行類B的<clinit>,我們開首說過JVM會包管一個類的初始化辦法只被履行一次,JVM收到new指令後不會再進入類B的<clinit>辦法而是直接停止實例化,然則此時類B還沒有完成類初始化,所以可以看到bInt的值為0(這個0是類加載中預備階段分派辦法區內存落後行的置零初始化);
是以,可以得出,再父類中包括子類類型的static域並停止賦值舉措,會能夠招致子類實例化在類初始化完成進步行;
情形二:進口為類A的main函數時輸入成果:
/** * 類A初始化開端... * 類B初始化開端... * 類B初始化停止... aInt的值:9 * 類B的結構器挪用 bInt的值9 * 類A初始化停止... */
剖析:經由情形一的剖析,我們曉得,由類B的初始化觸發類A的初始化,會招致類A中類變量b的實例化在類B初始化完成進步行,那假如先初始化類A是否是便可以在類變量實例化的時刻先觸發類B的初始化,從而使得初始化在實例化前呢?謎底是確定的,然則這依然有成績。
依據輸入,可以看到,類B的初始化在類A的初始化完成進步行了,這招致了像類變量aInt的變量在類B初始化完成後才停止初始化,所以類B中的域bInt獲得到的aInt的值是“0”,而不是我們預期的“18”;
結論:綜上,可以得出,在父類中包括子類類型的類變量,並在界說出停止實例化長短常風險的行動,詳細情形能夠不會向例子一樣直白,挪用辦法在界說處賦值一樣隱含著風險,即便要包括子類類型的static域,也應當經由過程static辦法停止賦值,由於JVM可以包管在static辦法挪用前完成一切的初始化舉措(固然這類包管也是你不該該包括static B b = new B();如許的初始化行動);
2. 一個實例化的例子:
起首須要曉得對象創立的進程:
(1)碰到new指令,檢討類能否完成了加載,驗證,預備,解析,初始化(解析進程就是符號援用解析成直接援用,好比辦法名就是一個符號援用,可以在初始化完成後應用這個符號援用的時刻停止,恰是為了支撐靜態綁定),沒有完成先輩行這些進程;
(2)分派內存,采取余暇列表或許指針碰撞的辦法,並將新分派的內存“置零”,是以一切的實例變量在此環節都停止了一次默許初始化為0(援用為null)的進程;
(3)履行<init>辦法,包含檢討挪用父類的<init>辦法(結構器),實例變量界說出的賦值舉措,實例化器次序履行,最初挪用結構器中的舉措。
這個例子能夠更加年夜家所熟知,也就是它違背了“不要在結構器,clone辦法和readObject辦法中挪用可被籠罩的辦法”。其緣由就在於Java中的多態,也就是靜態綁定。
父類A的結構器中包括一個protected辦法,類B是其子類。
public class WrongInstantiation { private static class A { public A() { doSomething(); } protected void doSomething() { System.out.println("A's doSomething"); } } private static class B extends A { private int bInt = 9; @Override protected void doSomething() { System.out.println("B's doSomething, bInt: " + bInt); } } public static void main(String[] args) { B b = new B(); } }
輸入成果:
/** * B's doSomething, bInt: 0 */
剖析:起首須要曉得,在沒有顯示供給結構器時Java編譯器會生成默許結構器,並在開端處挪用父類的結構器,是以類B的結構器開端會先挪用類A的結構器。
類A中挪用了protected辦法doSomething,從輸入成果中我們看到現實上挪用的是子類的辦法完成,而此時子類的實例化還未開端,是以bInt並沒有如“預期”那樣是9,而是0;
這就是因為靜態綁定,doSomething是一個protected辦法,是以它是經由過程invokevirtual指令挪用的,該指令依據對象實例的類型找到對應的辦法完成(這裡就是B的實例對象,對應辦法就是類B的辦法完成)履行,故而有此成果。
結論:正如後面說的“不要在結構器,clone辦法和readObject辦法中挪用可被籠罩的辦法”。
以上就是為年夜家引見的Java類初始化和實例化中的2個“雷區”,願望對年夜家的進修有所贊助。