也许您听说过 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
  • 可以与方法参考一起使用
  • 可用于编写通用的单例工厂
  • 可用于为每个用户返回一个单独的实例