也許您聽說過 Singleton 單一麥芽蘇格蘭威士忌好喝?好吧,酒精對你的健康有害,所以今天我們將告訴你Java中的單例設計模式。

我們之前回顧過對象的創建,所以我們知道在Java中創建一個對象,需要這樣寫:


Robot robot = new Robot(); 
    

但是如果我們想確保只創建該類的一個實例怎麼辦?

new Robot()語句可以創建很多對象,沒有什麼能阻止我們這樣做。這就是單例模式派上用場的地方。

假設您需要編寫一個連接到打印機的應用程序——只有一台打印機——並告訴它打印:


public class Printer { 
 
	public Printer() { 
	} 
     
	public void print() { 
    	… 
	} 
}
    

這看起來像一個普通的類......但是!有一個“但是”:我可以創建我的打印機對象的多個實例並在不同的地方調用它們的方法。這可能會損壞甚至損壞我的打印機。所以我們需要確保我們的打印機只有一個實例,這就是單例將為我們做的!

創建單例的方法

創建單例有兩種方式:

  • 使用私有構造函數;
  • 導出公共靜態方法以提供對單個實例的訪問。

讓我們首先考慮使用私有構造函數。為此,我們需要在我們的類中將一個字段聲明為final並對其進行初始化。由於我們將其標記為final,我們知道它將是不可變的,即我們無法再更改它。

您還需要將構造函數聲明為私有的,以防止在類之外創建對象。這向我們保證程序中不會有我們的打印機的其他實例。構造函數將在初始化期間僅被調用一次,並將創建我們的Printer


public class Printer { 
     
	public static final Printer PRINTER = new Printer(); 
     
	private Printer() { 
	} 
 
	public void print() { 
        // Printing... 
 
	} 
}
    

我們使用私有構造函數來創建一個 PRINTER 單例——永遠只有一個實例。這打印機變量有static 修飾符,因為它不屬於任何對象,而是屬於Printer類本身。

現在讓我們考慮使用靜態方法創建一個單例來提供對我們類的單個實例的訪問(並註意該字段現在是私有的):


public class Printer { 
 
	private static final Printer PRINTER = new Printer(); 
 
	private Printer() { 
	} 
 
	public static Printer getInstance() { 
    	return PRINTER; 
	} 
     
	public void print() { 
        // Printing... 
	} 
} 
    

無論我們在這裡調用getInstance()方法多少次,我們得到的總是相同的打印機目的。

使用私有構造函數創建單例更簡單、更簡潔。更重要的是,API 是顯而易見的,因為公共字段被聲明為final,這保證它將始終包含對同一對象的引用。

靜態方法選項使我們可以靈活地將單例更改為非單例類,而無需更改其 API。getInstance ()方法為我們提供了對象的單個實例,但我們可以對其進行更改,以便它為調用它的每個用戶返回一個單獨的實例。

static 選項還允許我們編寫一個通用的單例工廠。

static 選項的最後一個好處是您可以將它與方法引用一起使用。

如果您不需要上述任何優勢,那麼我們建議使用涉及公共字段的選項。

如果我們需要序列化,那麼僅僅實現Serializable接口是不夠的。我們還需要添加readResolve方法,否則我們在反序列化時會得到一個新的單例實例。

需要序列化來將對象的狀態保存為字節序列,並且需要反序列化來從這些字節中恢復對象。您可以在本文中閱讀有關序列化和反序列化的更多信息。

現在讓我們重寫我們的單例:


public class Printer implements Serializable { 
 
	private static final Printer PRINTER = new Printer(); 
 
	private Printer() { 
	} 
 
	public static Printer getInstance() { 
    	return PRINTER; 
	} 
} 
    

現在我們將序列化和反序列化它。

請注意,下面的示例是 Java 中序列化和反序列化的標準機制。在您學習“I/O 流”(在 Java 語法模塊中)和“序列化”(在 Java 核心模塊中)之後,您將完全理解代碼中發生的事情。

var printer = Printer.getInstance(); 
var fileOutputStream = new FileOutputStream("printer.txt"); 
var objectOutputStream = new ObjectOutputStream(fileOutputStream); 
objectOutputStream.writeObject(printer); 
objectOutputStream.close(); 
 
var fileInputStream = new FileInputStream("printer.txt"); 
var objectInputStream = new ObjectInputStream(fileInputStream); 
var deserializedPrinter =(Printer) objectInputStream.readObject(); 
objectInputStream.close(); 
 
System.out.println("Singleton 1 is: " + printer); 
System.out.println("Singleton 2 is: " + deserializedPrinter);
    

我們得到這個結果:

單例 1 是:Printer@6be46e8f
單例 2 是:Printer@3c756e4d

在這裡我們看到反序列化給了我們一個不同的單例實例。為了解決這個問題,讓我們將readResolve方法添加到我們的類中:


public class Printer implements Serializable { 
 
	private static final Printer PRINTER = new Printer(); 
 
	private Printer() { 
	} 
 
	public static Printer getInstance() { 
    	return PRINTER; 
	} 
 
	public Object readResolve() { 
    	return PRINTER; 
	} 
}
    

現在我們將再次序列化和反序列化我們的單例:


var printer = Printer.getInstance(); 
var fileOutputStream = new FileOutputStream("printer.txt"); 
var objectOutputStream = new ObjectOutputStream(fileOutputStream); 
objectOutputStream.writeObject(printer); 
objectOutputStream.close(); 
 
var fileInputStream = new FileInputStream("printer.txt"); 
var objectInputStream = new ObjectInputStream(fileInputStream); 
var deserializedPrinter=(Printer) objectInputStream.readObject(); 
objectInputStream.close(); 
 
System.out.println("Singleton 1 is: " + printer); 
System.out.println("Singleton 2 is: " + deserializedPrinter); 
    

我們得到:

單例 1 是:com.company.Printer@6be46e8f
單例 2 是:com.company.Printer@6be46e8f

readResolve ()方法讓我們獲得與反序列化相同的對象,從而防止創建流氓單例。

概括

今天我們了解了單例:如何創建它們、何時使用它們、它們的用途以及 Java 為創建它們提供的選項。兩種方案的具體特點如下:

私有構造函數 靜態方法
  • 更簡單更簡潔
  • 顯而易見的 API,因為單例字段是public final
  • 可以與方法參考一起使用
  • 可用於編寫通用的單例工廠
  • 可用於為每個用戶返回一個單獨的實例