Patrón Singleton en Java
Implementa el patrón singleton en Java de forma segura con enfoques eager, lazy y basado en enum.
El patrón singleton restringe una clase a una única instancia y proporciona un punto de acceso global a ella. Una fachada de registro, un registro de configuración, una caché en proceso — estas son el tipo de cosas que encajan. En Java, el patrón tiene un puñado de formas estándar, cada una con sus propias compensaciones de seguridad de hilos y carga perezosa. También hay un enfoque que evita silenciosamente la mayoría de los problemas.
Una breve advertencia primero. "Singleton" es famosamente fácil de abusar — cada singleton es, en efecto, un global, y los globales hacen que el código sea más difícil de probar y razonar. La mayoría de las aplicaciones Java modernas prefieren la inyección de dependencias: el framework conecta una única instancia y la entrega a los componentes que la necesitan, sin que ninguno de ellos tenga que llamar a Foo.getInstance(). Recurre a un singleton explícito cuando la DI no está disponible o es genuinamente excesiva.
Lo que necesita todo singleton
Cualquier implementación de singleton comparte tres piezas:
- Un constructor privado, para que nadie fuera de la clase pueda llamar a
new. - Un campo estático privado que contiene la única instancia.
- Un accesor estático público que la devuelve.
Las variaciones se refieren principalmente a cuándo se crea la instancia y cómo el acceso permanece seguro para hilos.
Inicialización eager
La forma más simple crea la instancia cuando se carga la clase:
public final class Eager {
private static final Eager INSTANCE = new Eager();
private Eager() {}
public static Eager getInstance() { return INSTANCE; }
}La inicialización de clases en la JVM tiene garantía de ser segura para hilos y de ejecutarse exactamente una vez, por lo que INSTANCE se establece de forma segura sin bloqueos. Usa esto cuando:
- La construcción es barata, o sabes que siempre necesitarás la instancia.
- No te importa pagar el costo en el momento de carga de la clase.
Inicialización lazy con bloqueo de doble verificación
Si la construcción es costosa y es posible que nunca necesites la instancia, puedes diferirla. La versión lazy ingenua no es segura para hilos; la correcta usa bloqueo de doble verificación con volatile:
public final class Lazy {
private static volatile Lazy instance;
private Lazy() {}
public static Lazy getInstance() {
Lazy local = instance; // local read avoids re-reading the volatile field
if (local == null) {
synchronized (Lazy.class) {
local = instance;
if (local == null) {
local = new Lazy();
instance = local;
}
}
}
return local;
}
}volatile es esencial — sin él, otro hilo podría ver el campo establecido en una referencia no nula cuyo constructor no ha terminado. Engorroso, pero correcto.
El holder de inicialización bajo demanda
La forma lazy más limpia usa una clase anidada privada. La JVM solo carga el holder cuando alguien llama getInstance() por primera vez, por lo que el trabajo se difiere — y las garantías de inicialización de clase de la JVM se encargan de la seguridad de hilos:
public final class Holder {
private Holder() {}
private static class H {
private static final Holder INSTANCE = new Holder();
}
public static Holder getInstance() { return H.INSTANCE; }
}Sin synchronized, sin volatile, sin bloqueo de doble verificación — y aún así lazy. Esta es la forma lazy a usar en la mayoría del código.
El singleton enum
El singleton correcto más corto en Java es un enum con una constante:
public enum Config {
INSTANCE;
public String get(String key) { /* ... */ }
}Config.INSTANCE es el singleton. La recomendación de Joshua Bloch (Effective Java, Item 3) es que esta es la mejor implementación de singleton, porque la JVM garantiza:
- Exactamente una instancia. Los enums se construyen exactamente una vez por JVM.
- Construcción segura para hilos. Las mismas garantías de inicialización de clase que el patrón holder.
- Seguro frente a reflexión. La reflexión no puede invocar el constructor de un enum; los singletons ordinarios pueden ser derrotados por
Constructor.setAccessible(true). - Seguro frente a serialización. Deserializar un singleton normal puede producir silenciosamente una segunda instancia a menos que manejes
readResolve. Los enums son inmunes.
Lo único que no puede hacer es extender otra clase — los enums extienden implícitamente java.lang.Enum. Aún puede implementar interfaces.
Cosas que rompen los singletons ingenuos
Vigila estos — son la razón por la que "simplemente usa un campo estático" no siempre es suficiente:
- Múltiples cargadores de clases. Un singleton es uno por cargador de clases, no uno por JVM. En contenedores que aíslan aplicaciones con sus propios cargadores, la misma clase puede tener varias instancias "únicas".
- Reflexión.
setAccessible(true)másConstructor.newInstance()puede construir una segunda instancia de cualquier singleton que no sea enum. Protege el constructor conif (INSTANCE != null) throw ...si esto es una preocupación real. - Serialización. Un singleton
Serializablenecesitaprivate Object readResolve() { return INSTANCE; }para evitar producir una segunda copia en cada deserialización. - Pruebas. Los singletons son notoriamente difíciles de sustituir o reiniciar. Prefiere la inyección de dependencias en el código que esperas someter a pruebas unitarias.
Un ejemplo completo
Qué sigue
Eso concluye la Parte 6 y el recorrido completo por la programación orientada a objetos en Java — desde clases, herencia y polimorfismo hasta interfaces, enums, records, jerarquías selladas y los métodos que hereda cada objeto. La siguiente parte hace zoom hacia atrás para ver cómo se organiza el código Java: espacios de nombres, la distribución del sistema de archivos que los refleja y la maquinaria import que trae tipos de otros lugares al tuyo. Continúa con paquetes de Java.