Saturday, October 30, 2021

Java Design Pattern: Singleton Class

Chapters

Singleton class

Singletons are classes that have only one instance that may be accessed globally. To create a singleton class, we need three elements: static reference, private constructor and static factory method. Also, there are two types of singleton design pattern: Early Instantiation and Lazy Instantiation.

Early Instantiation

Early Instantiation is the creation of an instance at load time. This example demonstrates Early Instantiation.
public class SampleClass{

  public static void main(String[] args){
    
    //get singleton instance
    Box box1 = Box.getInstance();
    Box box2 = Box.getInstance();
    System.out.println(box1 == box2);
  }
}

class Box{

  //static reference
  //myBox is instantiated early
  private static Box myBox = new Box();
  
  private static String name = "myBox";
  
  //private constructor
  private Box(){}
  
  //static factory method. This
  //method produces and returns
  //the singleton instance
  public static Box getInstance(){
    return myBox;
  }
  
  //getters
  public static String getName(){
    return name;
  }
  
}
Lazy Instantiation

Lazy Instantation is the creation of an instance when it's only needed. This example demonstrates Lazy Instantiation.
public class SampleClass{

  public static void main(String[] args){
    
    //get singleton instance
    Box box1 = Box.getInstance();
    Box box2 = Box.getInstance();
    System.out.println(box1 == box2);
  }
}

class Box{

  //static reference
  private static Box myBox;
  
  private static String name = "myBox";
  
  //private constructor
  private Box(){}
  
  //static factory method. This
  //method produces the singleton
  //instance
  //singleton instance is instantiated
  //when it's needed
  public static Box getInstance(){
    
    if(myBox == null)
      myBox = new Box();
      
    return myBox;
  }
  
  //getters
  public static String getName(){
    return name;
  }
  
}
In the getInstance() method, we use null to check if there's already a singleton instance. Though, we can use boolean variable to check if a singleton instance exists.
class Box{

  private static Box myBox;
  //boolean variable for checking
  private static boolean isExisting = false;
  
  private static String name = "myBox";
  
  //private constructor
  private Box(){}
  
  public static Box getInstance(){
    
    if(!isExisting){
      myBox = new Box();
      isExisting = true;
    }
      
    return myBox;
  }
  
  public static String getName(){
    return name;
  }
  
}
Enum Singleton

An enum class can be a singleton. An enum class is inherently constant, so, if we want a singleton that is constant then, consider using enum class as singleton. Creation of enum instance is thread safe. However, the methods are not.
public class SampleClass{

  public static void main(String[] args){
    System.out.println(Box.BOX.getName());
  }
}

enum Box{
  
  BOX("myBox");
  
  private String name;
  
  private Box(String name){
    this.name = name;
  }
  
  public Box getInstance(){
    return BOX;
  }
  
  public String getName(){
    return name;
  }
}
Singleton Issues

Singleton is easy to implement and can be useful in some situations. Though, this design pattern has issues with other java concepts.

Classloaders

If a singleton class is loaded by two classloaders, each classloader will create an instance of the singleton class. This breaks the principle of singleton class.

Deserialization

If we deserialize a serialized singleton object, the singleton object that has been returned won't be the same as its previous form. Take a look at this example.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Serializable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException,
                            ClassNotFoundException{
                            
    File file = new File("C:\\test\\testObj.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file));
        ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file))){
            
      Box input = Box.getInstance();
      
      oos.writeObject(input);
      oos.flush();
      
      Box output = (Box)ois.readObject();
      
      System.out.println(input == output);
    }
      
  }
}

class Box implements Serializable{

  private static Box myBox;
  private static boolean isExisting = false;
  
  private static String name = "myBox";
  
  private Box(){}
  
  public static Box getInstance(){
    
    if(!isExisting){
      myBox = new Box();
      isExisting = true;
    }
      
    return myBox;
  }
  
  public static String getName(){
    return name;
  }
  
}

Result
false
As you can see, the Box object in input variable is different from the Box object in output variable. This issue, however, has a solution. We need to manually define the readResolve() method in a serializable class which is the Box class in this case. readResolve() method allows a class to replace/resolve the object read from the stream before it is returned to the caller.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Serializable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException,
                            ClassNotFoundException{
                            
    File file = new File("C:\\test\\testObj.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file));
        ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file))){
            
      Box input = Box.getInstance();
      
      oos.writeObject(input);
      oos.flush();
      
      Box output = (Box)ois.readObject();
      
      System.out.println(input == output);
    }
      
  }
}

class Box implements Serializable{

  private static Box myBox;
  private static boolean isExisting = false;
  
  private static String name = "myBox";
  
  //Constructor
  private Box(){}
  
  //manually define readResolve() method
  protected Object readResolve(){
    return getInstance();
  }
  
  public static Box getInstance(){
    
    if(!isExisting){
      myBox = new Box();
      isExisting = true;
    }
      
    return myBox;
  }
  
  public static String getName(){
    return name;
  }
  
}

Result
true
Remember that readResolve() replaces the object from the stream. So, expect that the some values of the fields of the deserialized object may not be the same as their previous values.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Serializable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException,
                            ClassNotFoundException{
                            
    File file = new File("C:\\test\\testObj.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file));
        ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file))){
            
      Box input = Box.getInstance();
      System.out.println("Input name: " + input.getName());
      
      System.out.println("Serializing input object...");
      oos.writeObject(input);
      oos.flush();
      
      System.out.println("Changing input name...");
      input.setName("boxBox");
      System.out.println("New input name: " + input.getName());
      
      System.out.println("Deserializing...");
      Box output = (Box)ois.readObject();
      
      System.out.println("Output name: " + output.getName());
      System.out.println(input == output);
    }
      
  }
}

class Box implements Serializable{

  private static Box myBox;
  private static boolean isExisting = false;
  
  private static String name = "myBox";
  
  //Constructor
  private Box(){}
  
  //manually define readResolve() method
  protected Object readResolve(){
    return getInstance();
  }
  
  public static Box getInstance(){
    
    if(!isExisting){
      myBox = new Box();
      isExisting = true;
    }
      
    return myBox;
  }
  
  public static String getName(){
    return name;
  }
  
  public static void setName(String newName){
    name = newName;
  }
  
}

Result
Input name: myBox
Serializing input object...
Changing input name...
New input name: boxBox
Deserializing...
Output name: boxBox
true
As you can see from the result, the name of the Box object in input variable before serialization is different from the name of the Box object in output variable after serialization.

JVMs

A singleton is unique per JVM. A system that involves multiple JVMs breaks the principle of singleton class. Systems that rely on distributed technologies may involve multiple JVMs.

Garbage Collection

A singleton object might be garbage-collected if it's not being referenced. If this happens, new instance will be created and the old one will be replaced. This may cause problems in some situations.

No comments:

Post a Comment