Padrão Singleton em Java OOP: uso criterioso, implementação e alternativas

Capítulo 16

Tempo estimado de leitura: 8 minutos

+ Exercício

O que é Singleton (e o que ele realmente faz)

Singleton é um padrão cujo objetivo é garantir que exista uma única instância de uma classe em todo o processo e oferecer um ponto de acesso global a ela. Em Java, isso normalmente aparece como um método getInstance() que retorna sempre o mesmo objeto.

O ponto crítico: o Singleton não é apenas “uma instância única”. Ele também cria um acoplamento global (qualquer parte do código pode acessar), o que afeta testabilidade, previsibilidade e evolução do design.

Quando Singleton pode ser aceitável

1) Recurso realmente único no processo

Alguns recursos são naturalmente únicos por processo e não fazem sentido em múltiplas instâncias, por exemplo:

  • Um gerenciador de acesso a um dispositivo/porta (quando o próprio recurso é exclusivo).
  • Um registrador de métricas central (desde que seja bem encapsulado e thread-safe).

2) Configuração imutável e estável

Se a aplicação carrega uma configuração imutável (por exemplo, a partir de variáveis de ambiente) e ela é usada em vários pontos, um Singleton pode ser aceitável, desde que:

  • O objeto seja imutável (ou efetivamente imutável).
  • O ciclo de vida seja claro (carrega uma vez, não muda).
  • O acesso global não vire “atalho” para evitar passar dependências.

Quando Singleton tende a ser prejudicial

1) Estado global mutável

Se o Singleton guarda estado que muda (cache mutável, flags, usuário atual, “modo debug”, contadores sem sincronização), você introduz:

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

  • Dependências ocultas (qualquer classe pode alterar o estado).
  • Comportamento difícil de prever (ordem de execução importa).
  • Risco de condições de corrida em concorrência.

2) Testes difíceis e frágeis

Singleton frequentemente dificulta testes porque:

  • Você não consegue trocar facilmente a implementação por um stub/fake.
  • O estado pode “vazar” entre testes (um teste influencia o outro).
  • É comum precisar de hacks (reset de singleton, reflection) para isolar cenários.

3) Ciclo de vida e recursos (leaks)

Se o Singleton abre recursos (threads, conexões, file handles) e não tem um ciclo de vida explícito, ele pode:

  • Manter recursos abertos até o fim do processo.
  • Impedir descarte/recarga controlada (por exemplo, em testes ou em ferramentas).

Implementações seguras de Singleton em Java

Opção A: Inicialização estática (eager initialization)

Simples e thread-safe por definição do carregamento de classes em Java. Bom quando o custo de criar a instância é baixo e você tem certeza de que ela será usada.

public final class AppConfigSingleton {    private static final AppConfigSingleton INSTANCE = new AppConfigSingleton();    private final String apiBaseUrl;    private AppConfigSingleton() {        this.apiBaseUrl = System.getenv().getOrDefault("API_BASE_URL", "http://localhost");    }    public static AppConfigSingleton getInstance() {        return INSTANCE;    }    public String apiBaseUrl() {        return apiBaseUrl;    }}

Passo a passo do que torna isso seguro:

  • private static final cria a instância uma vez.
  • Construtor private impede new externo.
  • A JVM garante inicialização segura e visibilidade entre threads ao carregar a classe.

Opção B: Holder idiom (lazy e thread-safe)

Cria a instância apenas quando for realmente usada, mantendo thread-safety sem sincronização explícita.

public final class MetricsRegistry {    private MetricsRegistry() {}    private static class Holder {        private static final MetricsRegistry INSTANCE = new MetricsRegistry();    }    public static MetricsRegistry getInstance() {        return Holder.INSTANCE;    }    // Exemplo de API (evite estado mutável sem sincronização)    public void record(String name, long value) {        // implementar com estruturas thread-safe, se necessário    }}

Passo a passo:

  • A classe interna Holder só é carregada quando getInstance() é chamado.
  • O carregamento de classe é thread-safe, então a instância é criada uma única vez.

Opção C: Enum Singleton (recomendado quando apropriado)

Em Java, enum é uma forma robusta de Singleton: simplifica serialização e evita problemas comuns de reflection/duplicação.

public enum ClockSingleton {    INSTANCE;    public long nowMillis() {        return System.currentTimeMillis();    }}

Quando usar enum:

  • Quando a instância é realmente única e você não precisa herdar de outra classe.
  • Quando quer simplicidade e robustez contra armadilhas de serialização.

Quando evitar enum:

  • Quando você precisa controlar ciclo de vida (inicializar/encerrar recursos explicitamente).
  • Quando quer substituir facilmente em testes (ainda dá, mas tende a ser mais rígido).

Concorrência: o que pode dar errado (e como evitar)

1) Singleton thread-safe não significa “estado interno thread-safe”

Mesmo que a instância seja criada de forma segura, o que ela faz por dentro pode não ser. Exemplo de problema: contador mutável sem sincronização.

public final class BadCounterSingleton {    private static final BadCounterSingleton INSTANCE = new BadCounterSingleton();    private int count = 0;    private BadCounterSingleton() {}    public static BadCounterSingleton getInstance() {        return INSTANCE;    }    public void inc() {        count++; // condição de corrida    }    public int get() {        return count;    }}

Se for inevitável manter estado mutável compartilhado, use estruturas apropriadas (por exemplo, AtomicInteger, locks, coleções concorrentes) e defina claramente as garantias de thread-safety.

2) Lazy com double-checked locking (use com cuidado)

É possível implementar lazy com volatile, mas é mais fácil errar e geralmente é menos legível do que o Holder idiom.

public final class LazySingleton {    private static volatile LazySingleton instance;    private LazySingleton() {}    public static LazySingleton getInstance() {        LazySingleton local = instance;        if (local == null) {            synchronized (LazySingleton.class) {                local = instance;                if (local == null) {                    local = new LazySingleton();                    instance = local;                }            }        }        return local;    }}

Prefira Holder ou enum quando possível.

Ciclo de vida: inicialização, descarte e recursos

Singleton costuma “viver para sempre” no processo. Isso é aceitável para objetos leves e imutáveis, mas perigoso para objetos que:

  • Abrem conexões (banco, sockets).
  • Iniciam threads/schedulers.
  • Precisam ser reinicializados (troca de ambiente, testes, recarga).

Se o objeto precisa de start/stop, normalmente um Singleton não é a melhor escolha. Um ciclo de vida explícito (criar, usar, fechar) tende a ser mais saudável.

Alternativas melhores em muitos casos

1) Injeção explícita de dependências (sem framework)

Em vez de chamar Singleton.getInstance() dentro das classes, passe a dependência por construtor. Isso torna a dependência visível e substituível em testes.

Exemplo: serviço que precisa de relógio

public interface Clock {    long nowMillis();}public final class SystemClock implements Clock {    @Override    public long nowMillis() {        return System.currentTimeMillis();    }}public final class BillingService {    private final Clock clock;    public BillingService(Clock clock) {        this.clock = clock;    }    public boolean isLate(long dueMillis) {        return clock.nowMillis() > dueMillis;    }}

Uso na aplicação:

Clock clock = new SystemClock();BillingService service = new BillingService(clock);

Uso em teste (fake):

final class FakeClock implements Clock {    private final long fixed;    FakeClock(long fixed) { this.fixed = fixed; }    public long nowMillis() { return fixed; }}BillingService service = new BillingService(new FakeClock(1_700_000_000_000L));

2) Objeto imutável compartilhado (sem “global” escondido)

Se a motivação do Singleton é “não quero criar várias vezes”, muitas vezes basta criar uma instância imutável e compartilhar por composição, mantendo o acesso explícito.

public record AppConfig(String apiBaseUrl, int timeoutMillis) {}public final class Application {    private final AppConfig config;    public Application(AppConfig config) {        this.config = config;    }    // repassa config para quem precisa, explicitamente}

3) Factory/Provider explícito

Quando você quer controlar criação e ciclo de vida, um Provider explícito pode ser melhor do que um Singleton global. Você pode ter um “composition root” que monta tudo.

public final class Services {    private final Clock clock;    private final BillingService billingService;    public Services() {        this.clock = new SystemClock();        this.billingService = new BillingService(clock);    }    public BillingService billingService() {        return billingService;    }}

Isso mantém “uma instância por aplicação” sem transformar em estado global acessível de qualquer lugar.

Exercício: comparar abordagens e avaliar testabilidade

Contexto

Você precisa implementar um componente que gera tokens com validade baseada no tempo atual. Compare duas abordagens: (A) usando Singleton para o relógio; (B) usando injeção explícita.

Parte 1 — Implementação com Singleton (para análise crítica)

Implemente:

  • ClockSingleton (pode ser enum) com nowMillis().
  • TokenService que chama ClockSingleton.INSTANCE.nowMillis() para calcular expiração.
public enum ClockSingleton {    INSTANCE;    public long nowMillis() {        return System.currentTimeMillis();    }}public final class TokenService {    public String issueToken(long ttlMillis) {        long exp = ClockSingleton.INSTANCE.nowMillis() + ttlMillis;        return "exp=" + exp;    }}

Tarefas:

  • Escreva um teste que valide que o token expira corretamente. Observe a dificuldade de controlar o tempo.
  • Liste pelo menos 3 problemas potenciais dessa abordagem (ex.: dependência oculta, testes flakey, acoplamento global).

Parte 2 — Implementação com injeção explícita (recomendado)

Refatore para:

  • Interface Clock.
  • SystemClock e FakeClock.
  • TokenService recebendo Clock no construtor.
public interface Clock {    long nowMillis();}public final class SystemClock implements Clock {    public long nowMillis() {        return System.currentTimeMillis();    }}public final class TokenService {    private final Clock clock;    public TokenService(Clock clock) {        this.clock = clock;    }    public String issueToken(long ttlMillis) {        long exp = clock.nowMillis() + ttlMillis;        return "exp=" + exp;    }}

Tarefas:

  • Crie FakeClock com tempo fixo e escreva um teste determinístico para issueToken.
  • Compare a legibilidade e a facilidade de manutenção.

Parte 3 — Checklist de avaliação

CritérioSingletonInjeção explícita
Dependência visível no construtorNãoSim
Facilidade de substituir em testesBaixaAlta
Risco de estado global acidentalMaiorMenor
Controle de ciclo de vidaLimitadoExplícito
Concorrência (estado interno)Frequentemente negligenciadaMais fácil de isolar

Agora responda o exercício sobre o conteúdo:

Ao implementar um serviço que depende do tempo atual (por exemplo, para calcular expiração de tokens), qual abordagem tende a aumentar a testabilidade e reduzir dependências ocultas em comparação com usar um Singleton para o relógio?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A injeção explícita torna a dependência visível e substituível, permitindo testes determinísticos com FakeClock. Já o Singleton cria acoplamento global e dificulta controlar o tempo, podendo tornar testes frágeis.

Próximo capitúlo

Aplicando padrões e boas práticas em Java OOP: projeto incremental orientado a refatoração

Arrow Right Icon
Capa do Ebook gratuito Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos
94%

Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos

Novo curso

17 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.