Un Optional es un contenedor que puede incluir un valor o estar vacío y hace explícita esa posibilidad en la API.
Tony Hoare, el inventor de la referencia nula (null), llamó a su propia creación “el error del billón de dólares”.
NullPointerException aparece cuando una operación necesita un objeto, pero la referencia contiene null. Es un fallo frecuente, aunque no existe un porcentaje universal que describa todos los proyectos Java.
Para evitarlo, nuestro código solía llenarse de “defensas”:
// El infierno de los IF anidados
public String obtenerNombreCiudad(Usuario u) {
if (u != null) {
Direccion d = u.getDireccion();
if (d != null) {
return d.getCiudad();
}
}
return "Desconocida";
}
Para so tenemos java.util.Optional<T> para representar de forma explícita que un método puede no devolver un valor. No elimina null del lenguaje, pero mejora determinadas APIs de retorno.
¿Qué es un Optional?
Piensa en Optional como una Caja.
Cuando un método devuelve Optional<String>, te está entregando una caja cerrada.
- Puede que dentro haya un String (el valor).
- Puede que la caja sea puro aire (vacía).
Lo importante es que tú (el programador) estás obligado a abrir la caja con cuidado. Ya no puedes ignorar la posibilidad de que esté vacía.
Cómo crear un Optional
Rara vez crearás Optionals manualmente (normalmente los recibes de librerías o Streams), pero debes saber cómo meter cosas en la caja.
// 1. Caja VACÍA
Optional<String> vacio = Optional.empty();
// 2. Caja con algo NO NULO (Si metes null, explota aquí mismo)
Optional<String> lleno = Optional.of("Hola");
// 3. Caja SEGURA (Si es null, crea una caja vacía; si no, mete el valor)
// ESTA ES LA QUE MÁS USARÁS
String textoPosiblementeNull = getTextoDeBD();
Optional<String> caja = Optional.ofNullable(textoPosiblementeNull);
Cómo sacar el valor (la forma segura)
Aquí es donde Optional brilla. Nos ofrece una API fluida para decidir qué hacer si la caja está vacía.
Valor por defecto (orElse)
“Si hay algo, dámelo. Si no, dame este valor por defecto.”
String nombre = caja.orElse("Usuario Anónimo");
Valor por defecto perezoso (orElseGet)
“Si hay algo, dámelo. Si no, ejecuta este código para calcular el valor por defecto.”
Esto usa un Supplier y evita calcular el valor alternativo si no hace falta, algo importante cuando la operación es costosa.
// Solo llama a consultarBD() si la caja estaba vacía
String nombre = caja.orElseGet(() -> consultarBD());
Lanzar excepción (orElseThrow)
“Si hay algo, dámelo. Si no, lanza este error.”
// Muy útil para validaciones obligatorias
String nombre = caja.orElseThrow(() -> new UserNotFoundException("No existe"));
Transformando sin abrir la caja (map)
Así puedes transformar el contenido sin extraerlo manualmente. Si la caja estaba vacía, el resultado sigue siendo una caja vacía. Si estaba llena, se aplica la transformación.
Volvamos al ejemplo del principio. Mira qué limpieza:
public String obtenerNombreCiudad(Optional<Usuario> optUsuario) {
return optUsuario
.map(Usuario::getDireccion) // Si hay usuario, dame su dirección
.map(Direccion::getCiudad) // Si hay dirección, dame su ciudad
.orElse("Desconocida"); // Si algo falló por el camino, devuelve esto.
}
¡Ni un solo if! El Optional gestiona los nulos intermedios automáticamente.
El anti-patrón: .get() y .isPresent()
Cuando los novatos empiezan con Optional, suelen hacer esto:
// MAL ❌: Esto es lo mismo que usar != null pero más feo
if (caja.isPresent()) {
System.out.println(caja.get());
}
El método .get() es peligroso. Si la caja está vacía y llamas a .get(), Java lanza NoSuchElementException (que es básicamente el nuevo NullPointerException).
Evita usar .get() sin comprobar antes la presencia del valor. Según el caso, utiliza orElse, orElseGet, orElseThrow, ifPresent o map.
Si solo quieres hacer algo si existe (efecto secundario):
// BIEN ✅: Estilo funcional
caja.ifPresent(valor -> System.out.println(valor));
Optional y los streams
Optional es el mejor amigo de los Streams. Muchas operaciones terminales devuelven un Optional porque no saben si el stream traía datos.
// Buscar el primer usuario que sea de "Madrid"
Optional<Usuario> posibleMadrileño = usuarios.stream()
.filter(u -> "Madrid".equals(u.getCiudad()))
.findFirst();
// Usamos el resultado de forma segura
posibleMadrileño.ifPresent(u -> enviarCorreo(u));