EffectiveJava----私有構造器強化單例屬性之最優解決方案列舉
阿新 • • 發佈:2018-12-18
#私有建構函式強化singleton屬性
公有的靜態成員是一個final域,成員的宣告很清楚的表達了這個類是一個singleton。
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { } public void leaveTheBuilding() { System.out.println("Who a baby, I'm outta here!"); } // This code would normally appear outside the class! public static void main(String[] args) { Elvis elvis = Elvis.INSTANCE; elvis.leaveTheBuilding(); } }
提供一個公有的靜態方法,而不是公有的靜態final域。該方式提供了更大的靈活性,在不改變API的前提下,可以把該類改成singleton或者非singleton的。
public class Elvis { private static final Elvis INSTANCE = new Elvis(); private Elvis() { } public static Elvis getInstance() { return INSTANCE; } public void leaveTheBuilding() { System.out.println("Who a baby, I'm outta here!"); } // This code would normally appear outside the class! public static void main(String[] args) { Elvis elvis = Elvis.getInstance(); elvis.leaveTheBuilding(); } }
一般來說,第一種方法效率稍微高一些,然後,採用第一種方法實現singleton後,就沒有改變的餘地了,當你想把該類改成非singleton,顯然是不行的了。所以,除非確實確定該類是一個singleton,那就用第一個方法吧。用第2種方法的時候,假如該類實現了serializable介面,那應該重寫(override)readResolve()方法,否則再反序列化的時候是會產生一個新的例項,這與singleton相違背了!
現代jvm幾乎都能夠將靜態工廠方法進行呼叫內聯化。
- 方法呼叫 函式呼叫先轉移到該函式的記憶體地址,程式內容讀取完畢後轉到函式執行前方法。這種操作要求保護現場並記憶執行此地址,執行完恢復現場。這就是通常說的出棧和入棧,這需要一定時間和記憶體的開銷
- 行內函數。怎麼解決這個效能消耗問題呢,這個時候需要引入內聯函數了程式編譯時,編譯器將程式中的呼叫表示式用目標函式體直接替換。這樣就不會產生轉去轉回的問題,但是由於在編譯時將函式體中的程式碼被替代到程式中,因此會增加目標程式程式碼量,進而增加空間開銷,而在時間代銷上不象函式呼叫時那麼大,可見它是以目的碼的增加為代價來換取時間的節省。
- 寫C程式碼時,我們都學到將一些簡短的邏輯定義在巨集裡。這樣做的好處是,在編譯器編譯的時候會將用到該巨集的地方直接用巨集的程式碼替換。這樣就不再需要象呼叫方法那樣的壓棧、出棧,傳參了。效能上提升了。行內函數的處理方式與巨集類似,但與巨集又有所不同,行內函數擁有函式的本身特性(型別、作用域等等)。在C++裡有個行內函數,使用inline關鍵字修飾。另外,寫在Class定義內的函式也會被編譯器視為行內函數
- C++是否為行內函數由自己決定,Java由編譯器決定。Java不支援直接宣告為行內函數的,如果想讓他內聯,你只能夠向編譯器提出請求: 關鍵字final修飾 用來指明那個函式是希望被JVM內聯的。
public final void dealSomthing() {}
jvm內聯執行時優化,a、短方法更利於jvm優化。(流程更明顯,作用域更短,副作用也更明顯),長方法直接就跪了。b、小方法頻繁執行,jvm會執行內聯。(它會把方法的呼叫替換成方法體本身)
private int add4(int x1, int x2, int x3, int x4) { return add2(x1, x2) + add2(x3, x4); } private int add2(int x1, int x2) { return x1 + x2; } // 執行一段時間後 jvm會把程式碼翻譯成下面這樣 private int add4(int x1, int x2, int x3, int x4) { return x1 + x2 + x3 + x4; }
- 方法呼叫 函式呼叫先轉移到該函式的記憶體地址,程式內容讀取完畢後轉到函式執行前方法。這種操作要求保護現場並記憶執行此地址,執行完恢復現場。這就是通常說的出棧和入棧,這需要一定時間和記憶體的開銷
從Java1.5之後 實現Singleton還有第三種方法,只需要編寫一個包含單個元素的列舉型別。他和公有域方法類似。但是它無償提供了序列化機制(自由序列化),絕對的防止多次例項化(執行緒安全) 即使面對複雜的序列化和反射攻擊的時候。
enum SingletonDemo{ INSTANCE; public void otherMethods(){ System.out.println("Something"); } }
我們之前用的列舉 一般都是多個屬性的常量 用於switch
enum Color{ RED,GREEN,BLUE; } public class Hello { public static void main(String[] args){ Color color=Color.RED; int counter=10; switch (color){ case RED: System.out.println("Red"); color=Color.BLUE; break; case BLUE: System.out.println("Blue"); color=Color.GREEN; break; case GREEN: System.out.println("Green"); color=Color.RED; break; } } } }
- enum是通過繼承了Enum類實現的,enum結構不能夠作為子類繼承其他類,但是可以用來實現介面。此外,enum類也不能夠被繼承,在反編譯中,我們會發現該類是final的
- enum有且僅有private的構造器,防止外部的額外構造,這恰好和單例模式吻合,也為保證單例性做了一個鋪墊。這裡展開說下這個private構造器,如果我們不去手寫構造器,則會有一個預設的空參構造器,我們也可以通過給列舉變數參量來實現類的初始化。
private修飾符對於構造器是可以省略的,但這不代表構造器的許可權是預設許可權。
enum Color{ RED(1),GREEN(2),BLUE(3); private int code; Color(int code){ this.code=code; } public int getCode(){ return code; } }
- enum是如何工作的,就要對其進行反編譯。使用列舉其實和使用靜態類內部載入方法原理類似。列舉會被編譯成如下形式:
public final class T extends Enum{}
列舉量的實現其實是public static final T 型別的未初始化變數,之後,會在靜態程式碼中對列舉量進行初始化。所以,如果用列舉去實現一個單例,這樣的載入時間其實有點類似於餓漢模式,並沒有起到lazy-loading的作用。 - 對於序列化和反序列化,因為每一個列舉型別和列舉變數在JVM中都是唯一的,即Java在序列化和反序列化列舉時做了特殊的規定,列舉的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法是被編譯器禁用的,因此也不存在實現序列化介面後呼叫readObject會破壞單例的問題。