Có lẽ bạn đã nghe nói rằng rượu whisky mạch nha đơn Singleton là tốt? Chà, rượu có hại cho sức khỏe của bạn, vì vậy hôm nay chúng tôi sẽ cho bạn biết về mẫu thiết kế singleton trong Java.

Trước đây chúng ta đã xem xét việc tạo các đối tượng, vì vậy chúng ta biết rằng để tạo một đối tượng trong Java, bạn cần phải viết một cái gì đó như:


Robot robot = new Robot(); 
    

Nhưng nếu chúng ta muốn đảm bảo rằng chỉ có một thể hiện của lớp được tạo thì sao?

Câu lệnh Robot() mới có thể tạo nhiều đối tượng và không có gì ngăn chúng ta làm như vậy. Đây là nơi mô hình singleton đến để giải cứu.

Giả sử bạn cần viết một ứng dụng sẽ kết nối với máy in — chỉ MỘT máy in — và yêu cầu nó in:


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

Đây trông giống như một lớp học bình thường ... NHƯNG! Có một "nhưng": Tôi có thể tạo nhiều phiên bản của đối tượng máy in của mình và gọi các phương thức trên chúng ở những nơi khác nhau. Điều này có thể gây hại hoặc thậm chí làm hỏng máy in của tôi. Vì vậy, chúng tôi cần đảm bảo rằng chỉ có một phiên bản máy in của chúng tôi và đó là điều mà một máy in đơn lẻ sẽ làm cho chúng tôi!

Các cách để tạo một singleton

Có hai cách để tạo một singleton:

  • sử dụng một hàm tạo riêng;
  • xuất một phương thức tĩnh công khai để cung cấp quyền truy cập vào một phiên bản duy nhất.

Trước tiên hãy xem xét việc sử dụng một hàm tạo riêng. Để làm điều này, chúng ta cần khai báo một trường là cuối cùng trong lớp của mình và khởi tạo nó. Vì chúng tôi đã đánh dấu nó là cuối cùng , nên chúng tôi biết nó sẽ là bất biến , tức là chúng tôi không thể thay đổi nó được nữa.

Bạn cũng cần khai báo hàm tạo là private để ngăn việc tạo các đối tượng bên ngoài lớp . Điều này đảm bảo với chúng tôi rằng sẽ không có phiên bản máy in nào khác trong chương trình. Hàm tạo sẽ được gọi chỉ một lần trong quá trình khởi tạo và sẽ tạo Máy in của chúng tôi :


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

Chúng tôi đã sử dụng một hàm tạo riêng để tạo một máy in đơn PRINTER — sẽ chỉ có một phiên bản duy nhất. CácMÁY INbiến có công cụ sửa đổi tĩnh , bởi vì nó không thuộc về bất kỳ đối tượng nào, mà thuộc về chính lớp Máy in .

Bây giờ, hãy xem xét việc tạo một singleton bằng một phương thức tĩnh để cung cấp quyền truy cập vào một thể hiện duy nhất của lớp chúng ta (và lưu ý rằng trường hiện là private ):


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

Cho dù chúng ta gọi phương thức getInstance() bao nhiêu lần ở đây, chúng ta sẽ luôn nhận được cùng mộtMÁY INsự vật.

Tạo một singleton bằng cách sử dụng một hàm tạo riêng đơn giản và ngắn gọn hơn. Hơn nữa, API là hiển nhiên, vì trường công khai được khai báo là cuối cùng , điều này đảm bảo rằng nó sẽ luôn chứa tham chiếu đến cùng một đối tượng.

Tùy chọn phương thức tĩnh cho phép chúng ta linh hoạt thay đổi lớp đơn thành lớp không phải lớp đơn mà không thay đổi API của nó. Phương thức getInstance () cung cấp cho chúng ta một thể hiện duy nhất của đối tượng, nhưng chúng ta có thể thay đổi nó để nó trả về một thể hiện riêng biệt cho mỗi người dùng gọi nó.

Tùy chọn tĩnh cũng cho phép chúng tôi viết một nhà máy đơn lẻ chung.

Lợi ích cuối cùng của tùy chọn tĩnh là bạn có thể sử dụng nó với tham chiếu phương thức.

Nếu bạn không cần bất kỳ ưu điểm nào ở trên, thì chúng tôi khuyên bạn nên sử dụng tùy chọn liên quan đến trường công khai .

Nếu chúng ta cần tuần tự hóa, thì sẽ không đủ nếu chỉ triển khai giao diện Có thể tuần tự hóa . Chúng ta cũng cần thêm phương thức readResolve , nếu không, chúng ta sẽ nhận được một cá thể đơn lẻ mới trong quá trình khử lưu huỳnh.

Việc tuần tự hóa là cần thiết để lưu trạng thái của một đối tượng dưới dạng một chuỗi byte và quá trình giải tuần tự hóa là cần thiết để khôi phục đối tượng từ các byte đó. Bạn có thể đọc thêm về serialization và deserialization trong bài viết này .

Bây giờ hãy viết lại singleton của chúng ta:


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

Bây giờ chúng tôi sẽ tuần tự hóa và giải tuần tự hóa nó.

Lưu ý rằng ví dụ dưới đây là cơ chế tiêu chuẩn để tuần tự hóa và giải tuần tự hóa trong Java. Bạn sẽ hiểu đầy đủ về những gì đang xảy ra trong mã sau khi bạn nghiên cứu "Luồng I/O" (trong mô-đun Cú pháp Java) và "Tuần tự hóa" (trong mô-đun Java Core).

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);
    

Và chúng tôi nhận được kết quả này:

Singleton 1 là: Printer@6be46e8f
Singleton 2 là: Printer@3c756e4d

Ở đây, chúng tôi thấy rằng quá trình khử lưu huỳnh đã cho chúng tôi một phiên bản khác của singleton của chúng tôi. Để khắc phục điều này, hãy thêm phương thức readResolve vào lớp của chúng ta:


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; 
	} 
}
    

Bây giờ chúng tôi sẽ tuần tự hóa và giải tuần tự hóa singleton của chúng tôi một lần nữa:


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); 
    

Và chúng tôi nhận được:

Singleton 1 là: com.company.Printer@6be46e8f
Singleton 2 là: com.company.Printer@6be46e8f

Phương thức readResolve() cho phép chúng ta lấy cùng một đối tượng mà chúng ta đã giải tuần tự hóa, do đó ngăn chặn việc tạo ra các singleton giả mạo.

Bản tóm tắt

Hôm nay chúng ta đã học về singletons: cách tạo chúng và khi nào sử dụng chúng, chúng dùng để làm gì và những tùy chọn mà Java cung cấp để tạo chúng. Các tính năng cụ thể của cả hai tùy chọn được đưa ra dưới đây:

nhà xây dựng riêng phương pháp tĩnh
  • Dễ dàng và ngắn gọn hơn
  • API rõ ràng vì trường singleton là công khai cuối cùng
  • Có thể được sử dụng với một tham chiếu phương pháp
  • Có thể được sử dụng để viết một nhà máy đơn lẻ chung
  • Có thể được sử dụng để trả về một phiên bản riêng cho mỗi người dùng