Poate ați auzit că whisky-ul scoțian single malt Singleton este bun? Ei bine, alcoolul este rău pentru sănătatea ta, așa că astăzi vă vom spune despre modelul de design singleton în Java.

Am analizat anterior crearea de obiecte, așa că știm că pentru a crea un obiect în Java, trebuie să scrieți ceva de genul:


Robot robot = new Robot(); 
    

Dar dacă vrem să ne asigurăm că este creată o singură instanță a clasei?

Noua instrucțiune Robot() poate crea multe obiecte și nimic nu ne împiedică să facem acest lucru. Aici este locul în care modelul singleton vine în ajutor.

Să presupunem că trebuie să scrieți o aplicație care se va conecta la o imprimantă — doar O imprimantă — și să îi spuneți să imprime:


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

Asta pare o clasă obișnuită... DAR! Există un „dar”: pot crea mai multe instanțe ale obiectului meu imprimantă și pot apela metode pe ele în locuri diferite. Acest lucru ar putea deteriora sau chiar sparge imprimanta mea. Așa că trebuie să ne asigurăm că există o singură instanță a imprimantei noastre și asta va face un singleton pentru noi!

Modalități de a crea un singleton

Există două moduri de a crea un singleton:

  • utilizați un constructor privat;
  • exportați o metodă publică statică pentru a oferi acces la o singură instanță.

Să luăm în considerare mai întâi utilizarea unui constructor privat. Pentru a face acest lucru, trebuie să declarăm un câmp ca final în clasa noastră și să-l inițializam. Din moment ce l-am marcat ca final , știm că va fi imuabil , adică nu îl mai putem schimba.

De asemenea, trebuie să declarați constructorul ca privat pentru a preveni crearea de obiecte în afara clasei . Acest lucru ne garantează că nu vor exista alte instanțe ale imprimantei noastre în program. Constructorul va fi apelat o singură dată în timpul inițializării și va crea imprimanta noastră :


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

Am folosit un constructor privat pentru a crea un singleton PRINTER - va exista o singură instanță. TheIMPRIMANTAvariabila are modificatorul static , deoarece nu aparține unui obiect, ci clasei Printer în sine.

Acum să luăm în considerare crearea unui singleton folosind o metodă statică pentru a oferi acces la o singură instanță a clasei noastre (și rețineți că câmpul este acum privat ):


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

Indiferent de câte ori apelăm metoda getInstance() aici, vom obține întotdeauna același lucruIMPRIMANTAobiect.

Crearea unui singleton folosind un constructor privat este mai simplă și mai concisă. În plus, API-ul este evident, deoarece câmpul public este declarat ca final , ceea ce garantează că va conține întotdeauna o referință la același obiect.

Opțiunea metodei statice ne oferă flexibilitatea de a schimba clasa singleton într-o clasă non-singleton fără a-i schimba API-ul. Metoda getInstance() ne oferă o singură instanță a obiectului nostru, dar o putem schimba astfel încât să returneze o instanță separată pentru fiecare utilizator care îl apelează.

Opțiunea statică ne permite, de asemenea, să scriem o fabrică singleton generică.

Beneficiul final al opțiunii statice este că o puteți utiliza cu o referință de metodă.

Dacă nu aveți nevoie de niciunul dintre avantajele de mai sus, atunci vă recomandăm să utilizați opțiunea care implică un domeniu public .

Dacă avem nevoie de serializare, atunci nu va fi suficient să implementăm doar interfața Serializable . De asemenea, trebuie să adăugăm metoda readResolve , altfel vom obține o nouă instanță singleton în timpul deserializării.

Serializarea este necesară pentru a salva starea unui obiect ca o secvență de octeți, iar deserializarea este necesară pentru a restabili obiectul din acești octeți. Puteți citi mai multe despre serializare și deserializare în acest articol .

Acum să rescriem singleton-ul nostru:


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

Acum îl vom serializa și deserializa.

Rețineți că exemplul de mai jos este mecanismul standard pentru serializare și deserializare în Java. O înțelegere completă a ceea ce se întâmplă în cod va veni după ce veți studia „fluxurile I/O” (în modulul Java Syntax) și „Serializarea” (în modulul 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);
    

Și obținem acest rezultat:

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

Aici vedem că deserializarea ne-a oferit o altă instanță a singleton-ului nostru. Pentru a remedia acest lucru, să adăugăm metoda readResolve la clasa noastră:


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

Acum vom serializa și deserializa din nou singleton-ul nostru:


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

Și obținem:

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

Metoda readResolve() ne permite să obținem același obiect pe care l-am deserializat, prevenind astfel crearea de singleton-uri necinstite.

rezumat

Astăzi am învățat despre singleton-uri: cum să le creăm și când să le folosim, pentru ce sunt și ce opțiuni oferă Java pentru a le crea. Caracteristicile specifice ale ambelor opțiuni sunt prezentate mai jos:

Constructor privat Metoda statica
  • Mai ușor și mai concis
  • API-ul evident, deoarece câmpul singleton este public final
  • Poate fi folosit cu o referință de metodă
  • Poate fi folosit pentru a scrie o fabrică singleton generică
  • Poate fi folosit pentru a returna o instanță separată pentru fiecare utilizator