第3條:用私有構造器或者枚舉類型強化Singleton屬性


第3條:用私有構造器或枚舉類型強化Singleton屬性

    單例模式(Singleton):當系統中只需要的某個類的唯一對象時,可以使用該模式。Singleton指僅僅被實例化一次的類。Singleton通常被用來代表那些本質上唯一的系統組件。

    為什么會用到該模式?因為有時候某些對象的創建需要耗費大量的資源、使用單一(唯一)的對象實例來維護某些共享數據等,在這些場景下即可采用單例模式進行設計,可以適當地漸少內存開銷,因為此時該唯一對象不會(被限制了)頻繁地創建。

    再Java 1.5發行版本之前,實現Singleton有兩種方法,為了保證單例,所有的構造方法都需要被聲明成private方法,並在該類中都持有自己的一個靜態實例。第一種是:

public class A {

//在正常情況下構造器只會被調用這一次,創建一個公用的實例變量
public static final A INSTANCE = new A();

private A() { ... }

public void leaveTheBuilding() { ... }
}

    私有構造器會被調用一次,實例化一個A類型的對象,再也沒有其他的途徑來實例化A類,除了一些使用反射機制的客戶端,可以借助AccessibleObject.setAccessible()方法來調用private方法。例如下面:

公有域方法

class A {

public static final A INSTANCE = new A();

private A() {}
}

public class Main {

public static void main(String[] args) {

//A has a private access in A
//A a = new A();

//use reflect to invoke the private method
Class clz = A.class;
try {
Constructor<A> constructor = clz.getDeclaredConstructor();
constructor.setAccessible(true);
A a = constructor.newInstance();
System.out.println(a.getClass());
} catch (Exception e) {
e.printStackTrace();
}
}
}

    = =當時看到這里的時候簡直了,當時就覺得反射真無恥,還好有相應的解決辦法,可以通過修改構造器來防止反射的無恥調用,例如:

class A {

private static boolean flag = false;
public static final A INSTANCE = new A();
private A() {
if (!flag) {
System.out.println("=========invoke==========");
flag = !flag;
} else {
try {
if (null != INSTANCE) throw new Exception("duplicate instance create error!" + A.class.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

public class Main {

public static void main(String[] args) {

//A has a private access in A
//A a = new A();

//use reflect to invoke the private method
Class clz = A.class;
try {
Constructor<A> constructor = clz.getDeclaredConstructor();
constructor.setAccessible(true);
A a = constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}

    這樣在使用反射調用private構造器企圖做壞事的時候,就會拋出異常,阻止反射的進行。

    第二種方法就是類中有一個公有的靜態工廠方法:

靜態工廠方法

public class A {
private static final A INSTANCE = new A();
private A() { ... }
public static A getInstance() {return INSTANCE;}
}

    在這里的INSTANCE被聲明成了private的,對外的接口暴露成了靜態工廠方法getInstance()

    公有域方法的好處主要在於,組成類的成員的聲明很清楚的表明了這個類是一個Singleton:公有的靜態域是final的,因此該域總是持有的一個相同的對象引用。而再性能上不再有任何的優勢了:現在改進過的JVM幾乎都能實現將靜態工廠方法的調用內聯化。

    方法內聯:在C++中,可以明確定義內聯函數,使用inline關鍵字。在Java中不能定義內聯函數,但是方法的內聯在JIT編譯中還是存在的,只不過是JIT自動優化的,我們無法在寫代碼的時候指定。所謂內聯函數就是指函數在被調用的地方直接展開,編譯器在調用時不用像一般函數那樣,參數壓棧,返回時參數出棧以及資源釋放等,這樣提高了程序執行速度。 一般函數的調用時,JVM會自動新建一個堆棧框架來處理參數和下一條指令的地址,當執行完函數調用后再撤銷該堆棧。


    傳送門

        JVM的方法內聯

        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會將add2方法去掉,並且直接將add4方法中的add2方法去掉,並將代碼翻譯成:

private int add4(int x1, int x2, int x3, int x4) {
return x1 + x2 + x3 + x4;
}

    工廠方法的優勢之一在於,提供了靈活性:在不改變其他API的前提下,可以改變該類是否應該為Singleton的想法,只需要將getInstance()方法中的具體實現改掉就可以實現。

    為了使上面兩種方法創建的Singleton類變成是可序列化的(Serializable),僅僅在聲明上加上”implements Serializable”是不夠的,例如下面的這個例子:

class A implements Serializable {

public static final A INSTANCE = new A();

private int a = 1;

private A() {
}


}

public class Main {

public static void main(String[] args) throws IOException,
ClassNotFoundException {

FileOutputStream fos = new FileOutputStream("11.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
try {
oos.writeObject(A.INSTANCE);
oos.close();
} catch (IOException e) {
e.printStackTrace();
}

A a ;

FileInputStream fis = new FileInputStream("11.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
try {
a = (A) ois.readObject();
ois.close();
System.out.println(A.INSTANCE == a);
} catch (IOException e) {
e.printStackTrace();
}

}
}
輸出:false

    可以在電腦上跑一跑這個例子,會得到false的結果,也就是說我們將INSTANCE序列化到文件中后,再讀取出來的時候,已經不是之前的那個INSTANCE了。每次反序列化一個序列化的實例的時候,都創建了一個新的實例,這樣就不能保證Singleton的唯一性了,為了防止這種情況發生,需要再A類里面加入下面這個方法:

@Transient
private Object readResolve() {
return INSTANCE;
}

    至於這個方法為什么可以行,追蹤了一段時間的源碼,發現ObjectInputStream.java類中存在checkResolve()等方法,會檢測類是否存在readResolve()等方法,來確定變量enableResolve的值,然后根據這個變量的值來判斷返回給反序列化調用者之前是否需要用readResolve()中提供的變量來進行替換。代碼跟蹤有點多,就不在這里給出了,可以自己去跟進,入口為a = (A) ois.readObject();

    實際上從Java 1.5發行版本開始,實現Singleton還有第三種方法。只需要編寫一個包含單個元素的枚舉類型:
public enum A {
INSTANCE;
}

    雖然這種方法能夠很有效的保證了Singleton,更加的簡潔,無償的提供了序列化機制,可以有效的防止多次實例化,面對反射攻擊也可以應付自如,雖然這種方法沒有廣泛采用,但是單元素的枚舉類型已經成為實現Singleton的最佳方法。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com