“讀過書,……我便考你一考,茴香豆的茴字,怎樣寫的?”——魯迅《孔乙己》
0x00 大綱
目錄- 0x00 大綱
- 0x01 前言
- 0x02 單例的正確性
- new關鍵字
- clone方法克隆
- 反序列化
- 反射
- 0x03 最佳實踐方案
- 0x04 驗證測驗
- 0x05 真的是最佳實踐嗎
- 不能顯式繼承
- 無法延遲加載
- 0x06 小結
0x01 前言
最近在重溫設計模式(in Java)的相關知識,然后在單例模式的實作上面進行了一些較深入的探究,有了一些以前不曾注意到的發現,遂將其整理成文,以作后用,
單例模式最初的定義出現于《設計模式》(艾迪生維斯理, 1994):“保證一個類僅有一個實體,并提供一個訪問它的全域訪問點,”
其應用場景可以說是十分廣泛,尤其是在涉及到資源管理方面的代碼,像應用配置(實體)、部分工具類或工廠類、JDK里的Runtime等,都有出現單例模式的身影,
0x02 單例的正確性
探討單例模式有多少種實作方式的意義不是很大,因為單例模式的實作方式比茴字的寫法還多,但是正確的實作卻不多,我們不妨將重點放在如何保證單例的正確性上,從而尋求最佳實踐方案,
單例模式的關鍵在于如何保證“一個類僅有一個實體”,首先思考一下創建實體的方式有哪些?在Java語言里面,有這幾種方式:new關鍵字、clone方法克隆、反序列化、反射,
new關鍵字
public class Main {
public static void main(String[] args) {
Singleton instance = new Singleton();
}
}
如果要保證一個類是單例,則必須阻止用戶通過new關鍵字來隨意創建物件,最簡單粗暴的方法就是將構造方法私有化,然后提供一個靜態方法來進行實體的外部訪問:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}
此時就不能在類的外部通過new來創建物件了,
clone方法克隆
clone方法是原型模式中創建復雜物件的方法,在Java中,clone方法是Object基類的方法,因此所有的類都會繼承該方法,但只有實作了Cloneable介面的類才能正常呼叫clone方法克隆物件實體,否則會拋出型別為CloneNotSupportedException的例外,單例的類要防止用戶通過clone方法克隆就不能實作Cloneable介面,
反序列化
在Java里面,實作了Serializable介面的類可以通過ObjectOutputStream將其實體序列化,然后再通過ObjectInputStream進行反序列化,而在默認情況下,反序列之后得到的是一個新的實體,這就違背了單例的法則了,幸好JDK的開發人員也想到了這點,再Serializable介面的檔案中有這樣一段描述:
Classes that need to designate a replacement when an instance of it is read from the stream should implement this special method with the exact signature.
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
意思就是在反序列化時可以通過在類里面定義readResolve方法來指定反序列化時回傳的物件,例如:
public class Singleton implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private static Singleton instance = new Singleton();
private Singleton() {
if(instance != null) {
throw new RuntimeException("Not Allowed.");
}
}
public static Singleton getInstance() {
return instance;
}
private Object readResolve() throws java.io.ObjectStreamException {
return getInstance();
}
}
反射
聰明的你也許注意到了,上面的readResolve方法是private的,那么它是怎么被呼叫的呢?答案就是通過反射,想了解更詳細的呼叫程序可以去看看ObjectInputStream類原始碼中的readOrdinaryObject方法,
通過反射可以無視private修飾符的限制呼叫類里面的各種方法,也就是說用戶可以利用反射來呼叫我們的私有構造方法,像這樣:
public class Main {
public static void main(String[] args) throws Exception {
// 這句代碼無法執行,因為我們的構造方法是private的
// Singleton singleton = new Singleton();
// 通過反射來創建實體
java.lang.reflect.Constructor<Singleton> constructor;
constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton = constructor.newInstance();
// 兩個實體不一樣,單例完蛋
if(singleton != Singleton.getInstance()) {
System.out.println("哦嚯,完蛋");
}
}
}
解決方法是在構造方法里面判斷類的實體是否已經被創建過,如果已經創建過的,拋出例外從而阻止反射呼叫,把單例類的代碼修改如下:
public class Singleton implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private static Singleton instance = new Singleton();
private Singleton() {
if(instance != null) {
throw new RuntimeException("Not Allowed.");
}
}
public static Singleton getInstance() {
return instance;
}
/**
* 顯式指定反序列化時回傳的單例物件
* @return
* @throws java.io.ObjectStreamException
*/
private Object readResolve() throws java.io.ObjectStreamException {
return getInstance();
}
}
再次通過反射進行物件創建時,就會拋出型別為RuntimeException的例外,從而阻止新實體的創建,
0x03 最佳實踐方案
可以看到,我們為了實作單例模式,加入了一大堆膠水代碼,用于保證其正確性,這一點都不簡潔,那么有沒有更簡單更有效的方式呢?有,而且已經有人幫我們驗證過了,
Joshua Bloch在《Effective Java》一書中寫道:
使用列舉實作單例的方法雖然還沒有廣泛采用,但是單元素的列舉型別已經成為實作Singleton的最佳方法,
我們直接上代碼看看:
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("do something.");
}
}
就是這么簡單,再看看呼叫它的代碼:
public class Main {
public static void main(String[] args) {
EnumSingleton.INSTANCE.doSomething();
}
}
使用列舉實作單例模式,不僅代碼簡潔,而且可以輕松阻止用戶通過new關鍵字、clone方法克隆、反序列化、反射等方式創建重復實體,還保證執行緒安全,這一切由JVM替你操辦,不需要添加額外代碼,
0x04 驗證測驗
列舉實作單例模式能不能保證上面的提到的各種屬性呢?我們用代碼逐一驗證一下:
public class Main {
public static void main(String[] args) throws Exception {
// TEST-1: 驗證是否單一實體
EnumSingleton s1 = EnumSingleton.INSTANCE;
EnumSingleton s2 = EnumSingleton.INSTANCE;
if (s1.hashCode() != s2.hashCode()) {
System.out.println("哦嚯,完蛋");
} else {
System.out.println("TEST-1 PASSED.");
}
// TEST-2: 驗證反射創建
java.lang.reflect.Constructor<EnumSingleton> constructor;
// 注意這里用的是列舉的父構造器,因為我們沒有定義構造方法
constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
boolean passed = false;
try {
EnumSingleton s3 = constructor.newInstance("NEW_INSTANCE", 2);
} catch (Exception ex) {
// 報錯說明反射不能創建
passed = true;
}
if (!passed) {
System.out.println("哦嚯,完蛋");
} else {
System.out.println("TEST-2 PASSED.");
}
// TEST-3: 驗證反序列化
EnumSingleton s4 = EnumSingleton.INSTANCE;
EnumSingleton s5;
try (java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(new java.io.FileOutputStream("EnumObject"))) {
oos.writeObject(s4);
}
try (java.io.ObjectInputStream ois = new java.io.ObjectInputStream(new java.io.FileInputStream("EnumObject"))) {
s5 = (EnumSingleton) ois.readObject();
}
if (s4.hashCode() != s5.hashCode()) {
System.out.println("哦嚯,完蛋");
} else {
System.out.println("TEST-3 PASSED.");
}
// TEST-4: 多執行緒測驗
java.util.concurrent.CountDownLatch begin = new java.util.concurrent.CountDownLatch(10);
java.util.concurrent.CountDownLatch end = new java.util.concurrent.CountDownLatch(10);
java.util.Set<EnumSingleton> set = new java.util.HashSet<>(1024);
java.util.stream.IntStream.range(0, 20).forEach(
i -> {
new Thread(() -> {
try {
begin.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
set.add(EnumSingleton.INSTANCE);
System.out.println(System.currentTimeMillis() + ":" + Thread.currentThread().getName() + "->" + EnumSingleton.INSTANCE.hashCode());
end.countDown();
}).start();
begin.countDown();
}
);
end.await();
if(set.size() != 1) {
System.out.println("哦嚯,完蛋");
} else {
System.out.println("TEST-4 PASSED.");
}
}
}
測驗結果:
TEST-1 PASSED.
TEST-2 PASSED.
TEST-3 PASSED.
...
TEST-4 PASSED.
0x05 真的是最佳實踐嗎
在 Java Language Specification 列舉型別這一章節中,具體闡述了若干點對于列舉型別的強制和隱性約束:
An enum declaration specifies a new enum type, a special kind of class type.
It is a compile-time error if an enum declaration has the modifier abstract or final.
An enum declaration is implicitly final unless it contains at least one enum constant that has a class body (§8.9.1).
A nested enum type is implicitly static. It is permitted for the declaration of a nested enum type to redundantly specify the static modifier.
This implies that it is impossible to declare an enum type in the body of an inner class (§8.1.3), because an inner class cannot have static members except for constant variables.
It is a compile-time error if the same keyword appears more than once as a modifier for an enum declaration.
The direct superclass of an enum type E is Enum
(§8.1.4). An enum type has no instances other than those defined by its enum constants. It is a compile-time error to attempt to explicitly instantiate an enum type (§15.9.1).
其中最為突出和有影響是以下兩點:
不能顯式繼承
和常規類一樣,列舉可以實作介面,并提供公共實作或每個列舉值的單獨實作,但不能繼承,因為所有的列舉默認隱式繼承了Enum<E>型別,不能繼承也就意味著喪失了一部分的抽象能力(不能定義abstract方法),雖然可以通過組合的方式變通實作,但這無疑犧牲了擴展性和靈活性,
無法延遲加載
因為列舉實體化的特殊性,所有的構造器屬性都必須在列舉創建時指定,無法在運行時通過代碼動態傳遞和構造,
0x06 小結
非列舉的單例實作除開少數極端場景,在大多數時候下也都夠用了,且保留了OOP的靈活特性,方便日后業務擴展,基于列舉的單例實作有序列化和執行緒安全的保證,而且只要幾行代碼就能實作,不失為一種有效的方案,但并不無敵,具體的實作方案還是要根據業務背景和實際情況來進行選擇,畢竟,軟體工程沒有銀彈,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/539564.html
標籤:設計模式
上一篇:跨端開發浪潮中的變與不變
下一篇:Java單例模式的最佳實踐?
