Forse hai sentito che il whisky scozzese single malt Singleton è buono? Bene, l'alcol fa male alla salute, quindi oggi ti parleremo invece del modello di progettazione singleton in Java.

Abbiamo precedentemente esaminato la creazione di oggetti, quindi sappiamo che per creare un oggetto in Java, devi scrivere qualcosa del tipo:


Robot robot = new Robot(); 
    

Ma cosa succede se vogliamo assicurarci che venga creata solo un'istanza della classe?

La nuova istruzione Robot() può creare molti oggetti e nulla ci impedisce di farlo. È qui che il modello singleton viene in soccorso.

Supponiamo di dover scrivere un'applicazione che si connetterà a una stampante - solo UNA stampante - e dirle di stampare:


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

Sembra una classe normale... MA! Ce n'è uno "ma": posso creare più istanze del mio oggetto stampante e chiamare metodi su di esse in luoghi diversi. Questo potrebbe danneggiare o addirittura rompere la mia stampante. Quindi dobbiamo assicurarci che ci sia solo un'istanza della nostra stampante, ed è quello che un singleton farà per noi!

Modi per creare un singleton

Ci sono due modi per creare un singleton:

  • utilizzare un costruttore privato;
  • esportare un metodo statico pubblico per fornire l'accesso a una singola istanza.

Consideriamo innanzitutto l'utilizzo di un costruttore privato. Per fare ciò, dobbiamo dichiarare un campo come final nella nostra classe e inizializzarlo. Poiché l'abbiamo contrassegnato come final , sappiamo che sarà immutabile , ovvero non possiamo più modificarlo.

È inoltre necessario dichiarare il costruttore come privato per impedire la creazione di oggetti al di fuori della classe . Questo ci garantisce che non ci saranno altre istanze della nostra stampante nel programma. Il costruttore verrà chiamato solo una volta durante l'inizializzazione e creerà il nostro Printer :


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

Abbiamo utilizzato un costruttore privato per creare un singleton PRINTER: ci sarà sempre e solo un'istanza. ILSTAMPANTEvariabile ha il modificatore static , perché non appartiene a nessun oggetto, ma alla stessa classe Printer .

Consideriamo ora la possibilità di creare un singleton utilizzando un metodo statico per fornire l'accesso a una singola istanza della nostra classe (e notare che il campo è ora private ):


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

Non importa quante volte chiamiamo il metodo getInstance() qui, otterremo sempre lo stessoSTAMPANTEoggetto.

La creazione di un singleton utilizzando un costruttore privato è più semplice e concisa. Inoltre, l'API è ovvia, poiché il campo public è dichiarato final , il che garantisce che conterrà sempre un riferimento allo stesso oggetto.

L'opzione del metodo statico ci offre la flessibilità di modificare il singleton in una classe non singleton senza modificare la relativa API. Il metodo getInstance() ci fornisce una singola istanza del nostro oggetto, ma possiamo modificarla in modo che restituisca un'istanza separata per ogni utente che la chiama.

L'opzione static ci consente anche di scrivere una factory singleton generica.

Il vantaggio finale dell'opzione static è che puoi usarla con un riferimento al metodo.

Se non hai bisogno di nessuno dei vantaggi di cui sopra, ti consigliamo di utilizzare l'opzione che prevede un campo pubblico .

Se abbiamo bisogno della serializzazione, non sarà sufficiente implementare solo l' interfaccia Serializable . Dobbiamo anche aggiungere il metodo readResolve , altrimenti otterremo una nuova istanza singleton durante la deserializzazione.

La serializzazione è necessaria per salvare lo stato di un oggetto come sequenza di byte e la deserializzazione è necessaria per ripristinare l'oggetto da quei byte. Puoi leggere ulteriori informazioni sulla serializzazione e la deserializzazione in questo articolo .

Ora riscriviamo il nostro singleton:


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

Ora lo serializziamo e deserializziamo.

Si noti che l'esempio seguente è il meccanismo standard per la serializzazione e la deserializzazione in Java. Una comprensione completa di ciò che sta accadendo nel codice verrà dopo aver studiato "I/O stream" (nel modulo Java Syntax) e "Serializzazione" (nel modulo 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);
    

E otteniamo questo risultato:

Singleton 1 è: Printer@6be46e8f
Singleton 2 è: Printer@3c756e4d

Qui vediamo che la deserializzazione ci ha fornito un'istanza diversa del nostro singleton. Per risolvere questo problema, aggiungiamo il metodo readResolve alla nostra classe:


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

Ora serializziamo e deserializziamo di nuovo il nostro singleton:


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

E otteniamo:

Singleton 1 è: com.company.Printer@6be46e8f
Singleton 2 è: com.company.Printer@6be46e8f

Il metodo readResolve() ci consente di ottenere lo stesso oggetto che abbiamo deserializzato, impedendo così la creazione di singleton non autorizzati.

Riepilogo

Oggi abbiamo imparato a conoscere i singleton: come crearli e quando usarli, a cosa servono e quali opzioni offre Java per crearli. Di seguito sono riportate le caratteristiche specifiche di entrambe le opzioni:

Costruttore privato Metodo statico
  • Più semplice e conciso
  • API ovvia poiché il campo singleton è finale pubblico
  • Può essere utilizzato con un riferimento al metodo
  • Può essere utilizzato per scrivere una factory singleton generica
  • Può essere utilizzato per restituire un'istanza separata per ogni utente