Talvez você já tenha ouvido falar que o uísque escocês single malt Singleton é bom? Bem, o álcool é ruim para sua saúde, então hoje falaremos sobre o padrão de design singleton em Java.

Anteriormente revisamos a criação de objetos, então sabemos que para criar um objeto em Java, você precisa escrever algo como:

Robot robot = new Robot();

Mas e se quisermos garantir que apenas uma instância da classe seja criada?

A nova instrução Robot() pode criar muitos objetos e nada nos impede de fazê-lo. É aqui que o padrão singleton vem em socorro.

Suponha que você precise escrever um aplicativo que se conectará a uma impressora - apenas UMA impressora - e diga para imprimir:

public class Printer {

	public Printer() {
	}

	public void print() {}
}

Parece uma aula normal... MAS! Há um "mas": posso criar várias instâncias do meu objeto de impressora e chamá-las de métodos em locais diferentes. Isso pode danificar ou até mesmo quebrar minha impressora. Portanto, precisamos garantir que haja apenas uma instância de nossa impressora, e é isso que um singleton fará por nós!

Formas de criar um singleton

Existem duas maneiras de criar um singleton:

  • use um construtor privado;
  • exporte um método estático público para fornecer acesso a uma única instância.

Vamos primeiro considerar o uso de um construtor privado. Para fazer isso, precisamos declarar um campo como final em nossa classe e inicializá-lo. Como o marcamos como final , sabemos que será imutável , ou seja, não podemos mais alterá-lo.

Você também precisa declarar o construtor como privado para evitar a criação de objetos fora da classe . Isso nos garante que não haverá outras instâncias de nossa impressora no programa. O construtor será chamado apenas uma vez durante a inicialização e criará nossa impressora :

public class Printer {

	public static final Printer PRINTER = new Printer();

	private Printer() {
	}

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

	}
}

Usamos um construtor privado para criar um singleton PRINTER — haverá apenas uma instância. OIMPRESSORAvariável possui o modificador static , pois não pertence a nenhum objeto, mas sim à própria classe Printer .

Agora vamos considerar a criação de um singleton usando um método estático para fornecer acesso a uma única instância de nossa classe (e observe que o campo agora é private ):

public class Printer {

	private static final Printer PRINTER = new Printer();

	private Printer() {
	}

	public static Printer getInstance() {
    	return PRINTER;
	}

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

Não importa quantas vezes chamemos o método getInstance() aqui, sempre obteremos o mesmoIMPRESSORAobjeto.

Criar um singleton usando um construtor privado é mais simples e conciso. Além disso, a API é óbvia, pois o campo public é declarado como final , o que garante que sempre conterá uma referência ao mesmo objeto.

A opção de método estático nos dá a flexibilidade de alterar o singleton para uma classe não singleton sem alterar sua API. O método getInstance() nos dá uma única instância de nosso objeto, mas podemos alterá-lo para que retorne uma instância separada para cada usuário que o chamar.

A opção estática também nos permite escrever uma fábrica genérica de singleton.

O benefício final da opção estática é que você pode usá-la com uma referência de método.

Se você não precisa de nenhuma das vantagens acima, recomendamos usar a opção que envolve um campo público .

Se precisarmos de serialização, não será suficiente apenas implementar a interface Serializable . Também precisamos adicionar o método readResolve , caso contrário, obteremos uma nova instância singleton durante a desserialização.

A serialização é necessária para salvar o estado de um objeto como uma sequência de bytes e a desserialização é necessária para restaurar o objeto desses bytes. Você pode ler mais sobre serialização e desserialização neste artigo .

Agora vamos reescrever nosso singleton:

public class Printer implements Serializable {

	private static final Printer PRINTER = new Printer();

	private Printer() {
	}

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

Agora vamos serializá-lo e desserializá-lo.

Observe que o exemplo abaixo é o mecanismo padrão para serialização e desserialização em Java. Uma compreensão completa do que está acontecendo no código virá depois que você estudar "fluxos de E/S" (no módulo Java Syntax) e "Serialização" (no módulo 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);

E obtemos este resultado:

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

Aqui vemos que a desserialização nos deu uma instância diferente de nosso singleton. Para corrigir isso, vamos adicionar o método readResolve à nossa classe:

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

Agora vamos serializar e desserializar nosso singleton novamente:

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

E obtemos:

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

O método readResolve() nos permite obter o mesmo objeto que desserializamos, evitando assim a criação de singletons não autorizados.

Resumo

Hoje aprendemos sobre singletons: como criá-los e quando usá-los, para que servem e quais opções o Java oferece para criá-los. As características específicas de ambas as opções são dadas abaixo:

Construtor privado método estático
  • Mais fácil e conciso
  • API óbvia, pois o campo singleton é public final
  • Pode ser usado com uma referência de método
  • Pode ser usado para escrever uma fábrica genérica de singleton
  • Pode ser usado para retornar uma instância separada para cada usuário