Perhaps you've heard that Singleton single malt Scotch whisky is good? Well, alcohol is bad for your health, so today we will tell you about the singleton design pattern in Java instead.
We previously reviewed the creation of objects, so we know that to create an object in Java, you need to write something like:
Robot robot = new Robot();
But what if we want to ensure that only one instance of the class is created?
The new Robot() statement can create many objects, and nothing stops us from doing so. This is where the singleton pattern comes to the rescue.
Suppose you need to write an application that will connect to a printer — just ONE printer — and tell it to print:
public class Printer {
public Printer() {
}
public void print() {
…
}
}
This looks like an ordinary class... BUT! There is one "but": I can create multiple instances of my printer object and call methods on them in different places. This could harm or even break my printer. So we need to make sure that there is only one instance of our printer, and that's what a singleton will do for us!
Ways to create a singleton
There are two ways to create a singleton:
- use a private constructor;
- export a public static method to provide access to a single instance.
Let's first consider using a private constructor. To do this, we need to declare a field as final in our class and initialize it. Since we marked it as final, we know it will be immutable, i.e. we can no longer change it.
You also need to declare the constructor as private to prevent creating objects outside the class. This guarantees for us that there will be no other instances of our printer in the program. The constructor will be called just once during initialization and will create our Printer:
public class Printer {
public static final Printer PRINTER = new Printer();
private Printer() {
}
public void print() {
// Printing...
}
}
We used a private constructor to create a PRINTER singleton — there will only ever be a one instance. The PRINTER variable has the static modifier, because it belongs not to any object, but to the Printer class itself.
Now let's consider creating a singleton using a static method to provide access to a single instance of our class (and note that the field is now private):
public class Printer {
private static final Printer PRINTER = new Printer();
private Printer() {
}
public static Printer getInstance() {
return PRINTER;
}
public void print() {
// Printing...
}
}
No matter how many times we call the getInstance() method here, we will always get the same PRINTER object.
Creating a singleton using a private constructor is simpler and more concise. What's more, the API is obvious, since the public field is declared as final, which guarantees that it will always will contain a reference to the same object.
The static method option gives us the flexibility to change the singleton to a non-singleton class without changing its API. The getInstance() method gives us a single instance of our object, but we can change it so that it returns a separate instance for each user that calls it.
The static option also lets us write a generic singleton factory.
The final benefit of the static option is that you can use it with a method reference.
If you don't need any of the above advantages, then we recommend using the option that involves a public field.
If we need serialization, then it won't be enough to just implement the Serializable interface. We also need to add the readResolve method, otherwise we will get a new singleton instance during deserialization.
Serialization is needed to save the state of an object as a sequence of bytes, and deserialization is needed to restore the object from those bytes. You can read more about serialization and deserialization in this article. |
Now let's rewrite our singleton:
public class Printer implements Serializable {
private static final Printer PRINTER = new Printer();
private Printer() {
}
public static Printer getInstance() {
return PRINTER;
}
}
Now we will serialize and deserialize it.
Note that the example below is the standard mechanism for serialization and deserialization in Java. A complete understanding of what is happening in the code will come after you study "I/O streams" (in the Java Syntax module) and "Serialization" (in the Java Core module). |
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);
And we get this result:
Singleton 2 is: Printer@3c756e4d
Here we see that deserialization gave us a different instance of our singleton. To fix this, let's add the readResolve method to our class:
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;
}
}
Now we'll serialize and deserialize our singleton again:
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);
And we get:
Singleton 2 is: com.company.Printer@6be46e8f
The readResolve() method lets us get the same object that we deserialized, thereby preventing the creation of rogue singletons.
Summary
Today we learned about singletons: how to create them and when to use them, what they are for, and what options Java offers for creating them. The specific features of both options are given below:
Private constructor | Static method |
---|---|
|
|
GO TO FULL VERSION