設計模式之單例模式的幾個問題(1)
單例對象的初始化時機:
上篇博文設計模式之單例模式給出了5種單例模式的實現方法,其中靜態代碼塊與餓漢模式的本質一致,都歸為餓漢模式。其中餓漢模式和枚舉方式都屬於立即加載,懶漢式和靜態代碼塊屬於延時加載。如何理解立即加載和延時加載,需要從類加載機制聊一下。
Java虛擬機的類加載過程主要有七個步驟:Loading、verification、preparation、resolution、initialization、using、unloading。翻譯中文就是:加載,驗證,准備,解析,初始化,使用和卸載。
類什么時候加載:
類的加載是通過類加載器(Classloader)完成的,它既可以是立即加載[eagerly load](只要有其它類引用了它就加載)加載類,也可以是延時[lazy load](等到類初始化發生的時候才加載),由不同的JVM實現有關。
類什么時候初始化:
加載完類后,類的初始化就會發生,意味着它會初始化所有類靜態成員,以下情況一個類被初始化:
實例通過使用new()關鍵字創建或者使用class.forName()反射,但它有可能導致ClassNotFoundException。
類的靜態方法被調用
類的靜態域被賦值
靜態域被訪問,而且它不是常量
在頂層類中執行assert語句
反射同樣可以使類初始化,比如java.lang.reflect包下面的某些方法。
類是如何被初始化的:
現在我們知道什么時候觸發類的初始化了,他精確地寫在Java語言規范中。但了解清楚 域(fields,靜態的還是非靜態的)、塊(block靜態的還是非靜態的)、不同類(子類和超類)和不同的接口(子接口,實現類和超接口)的初始化順序也很重要類。
下面是類初始化的一些規則:
類從頂至底的順序初始化,所以聲明在頂部的字段的早於底部的字段初始化
超類早於子類和衍生類的初始化
如果類的初始化是由於訪問靜態域而觸發,那么只有聲明靜態域的類才被初始化,而不會觸發超類的初始化或者子類的初始化即使靜態域被子類或子接口或者它的實現類所引用。
接口初始化不會導致父接口的初始化。
靜態域的初始化是在類的靜態初始化期間,非靜態域的初始化時在類的實例創建期間。這意味這靜態域初始化在非靜態域之前。
非靜態域通過構造器初始化,子類在做任何初始化之前構造器會隱含地調用父類的構造器,他保證了非靜態或實例變量(父類)初始化早於子類。
單例模式與類加載:
回到單例,餓漢模式屬於立即加載模式在類一旦加載就會就會實例化單例對象。不管有沒有使用到該單例類,都會導致單例對象在內存中存在,知道程序退出結束。如何理解這句話,看下面的代碼:
單例類:
//餓漢式單例模式 public class Singleton { public static int MIN_USER = 10000; //私有構造函數防止外部創建對象 private Singleton(){ System.out.println("對象初始化"); } //靜態對象對象初始化 private static Singleton singleton = new Singleton(); //靜態工程方法 public static Singleton getInstance(){ return singleton; } public void doSomething(){ System.out.println(this.getClass().getName()); } public static void doSomethingStatic(){ System.out.println("這是一個靜態方法"); } }
在這個類中添加了一個靜態屬性,一個靜態方法。
調用:
public class SingletonTest { public static void main(String[] args){ Singleton singleton = null; System.out.println(singleton == null); } }
輸出:
true Process finished with exit code 0
可以看到雖然引用了單例類,卻沒有生成單例對象。
訪問靜態屬性調用:
public class SingletonTest { public static void main(String[] args){ int max_User = Singleton.MIN_USER; System.out.println(max_User); } }
輸出:
對象初始化 10000 Process finished with exit code 0
如果為了像上述一樣只為訪問其某些靜態屬性或靜態方法,卻創建的單例對象。同理如果只調用doSomethingStatic()方法也會生成對象。
靜態方法調用:
public class SingletonTest { public static void main(String[] args){ Singleton.doSomethingStatic(); } }
輸出:
對象初始化
這是一個靜態方法
Process finished with exit code 0
考慮一下final:
被final修飾的靜態屬性的訪問不會觸發實例化,被final修飾的靜態方法仍然會觸發實例化。被final修飾的類屬性會被作為編譯期常量加入常量池,以后訪問對應類的常量池,不會在常量池中保存一個指向類字段的符號引用,不觸發類的初始化。
總結:
餓漢模式是通過類的靜態屬性初始化來實現單例模式的實例化 private static Singleton singleton = new Singleton(); 立即加載在類完成初始化時也完成了單例對象的實例化。枚舉方式也是同樣的道理。
懶漢式(延時加載)是在顯式調用 getInstance() 方法來完成單例對象的實例化,即類加載的七步中的使用階段。
靜態內部類能夠實現延時加載是由於對於沒有使用的類jvm是不會加載,即便其是一個內部類。其通過private修飾只能在外部類中訪問。外部類中訪問的唯一地方就是在 getInstance() 方法中。
public static Singleton5 getInstance(){ return InnerObjcet.singleton5; }
只有在觸發內部類加載時才實例化單例對象。
為了便於理解用下面的代碼理解:
單例類:
//靜態內部類 public class Singleton5 { private Singleton5(){ System.out.println("對象實例化"); if(InnerObjcet.singleton5 != null){ throw new IllegalStateException(); } } private static class InnerObjcet{ static String str = "TEST"; private static Singleton5 singleton5 = new Singleton5(); static { System.out.println("內部類被加載"); } } public static Singleton5 getInstance(){ return InnerObjcet.singleton5; } public static void doSomethingStatic(){ System.out.println(InnerObjcet.str); } public void doSomething(){ System.out.println(this.getClass().getName()); } }
調用靜態方法:
public class SingletonTest { public static void main(String[] args){ Singleton5.doSomethingStatic(); } }
輸出:
對象實例化
內部類被加載
TEST
Process finished with exit code 0