¿Has escuchado que el whisky de malta única Singleton es bueno? Bueno, el alcohol es malo para tu salud, así que hoy te hablaremos del patrón de diseño singleton en Java en su lugar.

Anteriormente revisamos la creación de objetos, por lo que sabemos que para crear un objeto en Java, necesitas escribir algo como:


Robot robot = new Robot();
    

Pero ¿qué pasa si queremos asegurarnos de que se cree solo una instancia de la clase?

La sentencia new Robot() puede crear muchos objetos, y nada nos impide hacerlo. Aquí es donde entra en juego el patrón singleton.

Supongamos que necesitas escribir una aplicación que se conecte a una impresora, solo a UNA impresora, y le indique que imprima:


public class Printer {

	public Printer() {
	}

	public void print() {
    	…
	}
}
    

Esto parece una clase ordinaria... ¡PERO! Hay un "pero": puedo crear varias instancias de mi objeto impresora y llamar a métodos en ellos en diferentes lugares. Esto podría dañar o incluso romper mi impresora. Así que necesitamos asegurarnos de que solo haya una instancia de nuestra impresora, ¡y eso es lo que hará un singleton por nosotros!

Formas de crear un singleton

Hay dos formas de crear un singleton:

  • usar un constructor privado;
  • exportar un método público estático para proporcionar acceso a una única instancia.

Primero consideremos el uso de un constructor privado. Para hacer esto, necesitamos declarar un campo como final en nuestra clase e inicializarlo. Dado que lo marcamos como final, sabemos que será inmutable, es decir, ya no podemos cambiarlo.

También debemos declarar el constructor como private para evitar la creación de objetos fuera de la clase. Esto garantiza para nosotros que no habrá otras instancias de nuestra impresora en el programa. El constructor solo se llamará una vez durante la inicialización y creará nuestra Impresora:


public class Printer {

	public static final Printer PRINTER = new Printer();

	private Printer() {
	}

	public void print() {
        // Printing...

	}
}
    

Utilizamos un constructor privado para crear un singleton de IMPRESORA - solo habrá una instancia. La variable IMPRESORA tiene el modificador estático, porque no pertenece a ningún objeto, sino a la clase Printer en sí.

Ahora consideremos la creación de un singleton utilizando un método estático para proporcionar acceso a una única instancia de nuestra clase (y tenga en cuenta que el campo ahora es private):


public class Printer {

	private static final Printer PRINTER = new Printer();

	private Printer() {
	}

	public static Printer getInstance() {
    	return PRINTER;
	}

	public void print() {
        // Imprimiendo...
	}
}
    

No importa cuántas veces llamemos al método getInstance() aquí, siempre obtendremos el mismo objeto PRINTER.

Crear un singleton utilizando un constructor private es más simple y conciso. Además, la API es obvia, ya que el campo público se declara como final, lo que garantiza que siempre contendrá una referencia al mismo objeto.

La opción del método estático nos brinda la flexibilidad de cambiar el singleton a una clase no singleton sin cambiar su API. El método getInstance() nos da una sola instancia de nuestro objeto, pero podemos cambiarlo para que devuelva una instancia separada para cada usuario que lo llama.

La opción estática también nos permite escribir una fábrica genérica de singletons.

El último beneficio de la opción estática es que podemos usarla con una referencia de método.

Si no necesitamos ninguna de las ventajas mencionadas anteriormente, entonces recomendamos usar la opción que implica un campo público.

Si necesitamos serialización, no será suficiente con solo implementar la interfaz Serializable. También debemos agregar el método readResolve, de lo contrario obtendremos una nueva instancia de singleton durante la deserialización.

La serialización es necesaria para guardar el estado de un objeto como una secuencia de bytes, y la deserialización es necesaria para restaurar el objeto a partir de esos bytes. Puedes leer más sobre la serialización y deserialización en este artículo.

Ahora escribiremos nuestro singleton:


public class Printer implements Serializable {

	private static final Printer PRINTER = new Printer();

	private Printer() {
	}

	public static Printer getInstance() {
    	return PRINTER;
	}
}
    

Ahora lo serializaremos y deserializaremos.

Nota que el ejemplo a continuación es el mecanismo estándar para la serialización y deserialización en Java. Una comprensión completa de lo que está sucediendo en el código vendrá después de estudiar "Flujos de E/S" (en el módulo de Sintaxis de Java) y "Serialización" (en el módulo de Core de Java).

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

Y obtenemos este resultado:

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

Aquí vemos que la deserialización nos dio una instancia diferente de nuestro singleton. Para solucionar esto, agreguemos el método readResolve a nuestra clase:


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

Ahora vamos a serializar y deserializar nuestro singleton de nuevo:


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

Y obtenemos:

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

El método readResolve() nos permite obtener el mismo objeto que deserializamos, evitando así la creación de singletons no autorizados.

Resumen

Hoy aprendimos acerca de los singletons: cómo crearlos y cuándo usarlos, para qué sirven y qué opciones ofrece Java para crearlos. A continuación se presentan las características específicas de ambas opciones:

Constructor privado Método estático
  • Más fácil y conciso
  • La API es obvia ya que el campo singleton es public final
  • Puede ser usado con una referencia de método
  • Puede ser usado para escribir una fábrica genérica de singletons
  • Puede ser usado para devolver una instancia separada para cada usuario