Misschien heb je gehoord dat Singleton single malt Scotch whisky goed is? Nou, alcohol is slecht voor je gezondheid, dus vandaag zullen we je in plaats daarvan vertellen over het singleton-ontwerppatroon in Java.

We hebben eerder het maken van objecten beoordeeld, dus we weten dat om een ​​object in Java te maken, je iets moet schrijven als:


Robot robot = new Robot(); 
    

Maar wat als we ervoor willen zorgen dat er slechts één instantie van de klasse wordt gemaakt?

De nieuwe instructie Robot() kan veel objecten maken en niets houdt ons tegen om dit te doen. Dit is waar het singleton-patroon te hulp komt.

Stel dat u een toepassing moet schrijven die verbinding maakt met een printer - slechts ÉÉN printer - en deze opdracht geeft om af te drukken:


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

Dit ziet eruit als een gewone les... MAAR! Er is één "maar": ik kan meerdere exemplaren van mijn printerobject maken en er op verschillende plaatsen methoden op aanroepen. Dit kan mijn printer beschadigen of zelfs kapot maken. We moeten er dus voor zorgen dat er maar één exemplaar van onze printer is, en dat is wat een singleton voor ons zal doen!

Manieren om een ​​singleton te maken

Er zijn twee manieren om een ​​singleton te maken:

  • gebruik een privé constructeur;
  • exporteer een openbare statische methode om toegang te geven tot één instantie.

Laten we eerst eens kijken naar het gebruik van een private constructor. Om dit te doen, moeten we een veld als definitief in onze klas declareren en initialiseren. Aangezien we het als definitief hebben gemarkeerd , weten we dat het onveranderlijk zal zijn , dat wil zeggen dat we het niet langer kunnen wijzigen.

U moet de constructor ook als privé declareren om te voorkomen dat er objecten buiten de klasse worden gemaakt . Dit garandeert voor ons dat er geen andere instanties van onze printer in het programma zullen zijn. De constructor wordt tijdens de initialisatie slechts één keer aangeroepen en maakt onze printer :


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

We hebben een privéconstructor gebruikt om een ​​PRINTER-singleton te maken - er zal maar één instantie zijn. DePRINTERvariabele heeft de statische modifier , omdat deze niet tot een object behoort, maar tot de klasse Printer zelf.

Laten we nu eens kijken naar het maken van een singleton met behulp van een statische methode om toegang te geven tot een enkele instantie van onze klasse (en merk op dat het veld nu privé is ):


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

Het maakt niet uit hoe vaak we de methode getInstance() hier aanroepen, we krijgen altijd hetzelfdePRINTERvoorwerp.

Het maken van een singleton met behulp van een privéconstructor is eenvoudiger en beknopter. Bovendien ligt de API voor de hand, aangezien het openbare veld is gedeclareerd als final , wat garandeert dat het altijd een verwijzing naar hetzelfde object zal bevatten.

De optie statische methode geeft ons de flexibiliteit om de singleton-klasse te wijzigen in een niet-singleton-klasse zonder de API te wijzigen. De methode getInstance() geeft ons een enkele instantie van ons object, maar we kunnen het zo wijzigen dat het een afzonderlijke instantie retourneert voor elke gebruiker die het aanroept.

Met de statische optie kunnen we ook een generieke singleton-fabriek schrijven.

Het laatste voordeel van de statische optie is dat u deze kunt gebruiken met een methodereferentie.

Heb je geen behoefte aan een van bovenstaande voordelen, dan raden we je aan om de optie met een openbaar veld te gebruiken.

Als we serialisatie nodig hebben, is het niet voldoende om alleen de Serializable- interface te implementeren. We moeten ook de methode readResolve toevoegen , anders krijgen we een nieuwe singleton-instantie tijdens deserialisatie.

Serialisatie is nodig om de status van een object op te slaan als een reeks bytes, en deserialisatie is nodig om het object uit die bytes te herstellen. U kunt meer lezen over serialisatie en deserialisatie in dit artikel .

Laten we nu onze singleton herschrijven:


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

Nu gaan we het serialiseren en deserialiseren.

Merk op dat het onderstaande voorbeeld het standaardmechanisme is voor serialisatie en deserialisatie in Java. Een volledig begrip van wat er in de code gebeurt, krijgt u nadat u "I/O-streams" (in de Java Syntax-module) en "Serialisatie" (in de Java Core-module) hebt bestudeerd.

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

En we krijgen dit resultaat:

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

Hier zien we dat deserialisatie ons een ander exemplaar van onze singleton gaf. Om dit op te lossen, voegen we de methode readResolve toe aan onze klasse:


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

Nu zullen we onze singleton opnieuw serialiseren en deserialiseren:


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

En we krijgen:

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

Met de methode readResolve() kunnen we hetzelfde object verkrijgen dat we hebben gedeserialiseerd, waardoor het ontstaan ​​van malafide singletons wordt voorkomen.

Samenvatting

Vandaag leerden we over singletons: hoe je ze maakt en wanneer je ze gebruikt, waar ze voor zijn en welke opties Java biedt om ze te maken. De specifieke kenmerken van beide opties worden hieronder weergegeven:

Particuliere constructeur Statische methode
  • Makkelijker en beknopter
  • Voor de hand liggende API omdat het singleton-veld openbaar definitief is
  • Kan worden gebruikt met een methodereferentie
  • Kan worden gebruikt om een ​​generieke singleton-fabriek te schrijven
  • Kan worden gebruikt om voor elke gebruiker een afzonderlijke instantie te retourneren