Desde Java 8, el lenguaje incluye una herramienta pensada específicamente para resolver este problema, y es la clase Optional. En este artículo vamos a ver qué es, por qué existe, y cómo usarla bien con ejemplos de código.
El problema que resuelve
Imagina este método clásico:
public User findUserById(String id) {
// puede devolver un Usuario o null si no lo encuentra
return db.get(id);
}
Y luego, en algún otro lugar del código:
User user = findUserById("123");
System.out.println(user.getName()); // NullPointerException si no existe
El problema real no es solo que el programa se rompa. Es que el método no te avisa. Es un contrato invisible ya que la regla existe porque puede no haber usuario, pero no está escrita en ningún lado visible. Solo se descubre cuando aparece el NullPointerException.

¿Qué es Optional?
Optional<T> es un contenedor que puede tener un valor de tipo T o estar vacío. Por dentro es básicamente una caja con una referencia que puede ser null o no, pero te obliga a manejarlo explícitamente en vez de dejarte acceder al valor "a ciegas".
Reescribamos el ejemplo anterior:
public Optional<User> findUserById(String id) {
User user = db.get(id);
return Optional.ofNullable(user);
}
Ahora, quien llama a este método sabe que el usuario podría no existir.
Creando un Optional
Hay tres formas principales de crear uno:
// 1. Cuando estás SEGURO de que el valor no es null
Optional<String> name = Optional.of("Ana");
// 2. Cuando el valor PUEDE ser null
Optional<String> potentiallyNullName = Optional.ofNullable(potentiallyNullName);
// 3. Un Optional explícitamente vacío
Optional<String> empty = Optional.empty();
Ojo con Optional.of(), si le pasas null, lanza una excepción inmediatamente (NullPointerException). Se usa solo cuando tienes la certeza de que el valor existe. Para todo lo demás, ofNullable es tu amigo.
Comprobando si hay valor
Optional<User> result = findUserById("123");
if (result.isPresent()) {
System.out.println("Encontrado: " + result.get());
} else {
System.out.println("No existe ese usuario");
}
Esto funciona, pero es prácticamente el mismo patrón que un if (usuario != null). No estamos aprovechando lo que Optional realmente ofrece.
La mejor forma, evita el `get()`
Llamar a .get() sin comprobar antes si hay valor es un antipatrón muy común. Si el Optional está vacío, .get() lanza NoSuchElementException, así que solo estarías cambiando un tipo de excepción por otro.
En su lugar, Optional ofrece métodos mucho más expresivos:
orElse: un valor por defecto
String name = findUserById("123")
.map(User::getName)
.orElse("Usuario desconocido");
orElseGet: un valor por defecto
String name = findUserById("123")
.map(User::getName)
.orElseGet(() -> getDefaultNameFromConfiguration());
La diferencia con orElse es sutil pero importante, orElseGet solo ejecuta el Supplier si realmente hace falta. orElse siempre evalúa su argumento, aunque haya valor presente. Si calcular el valor por defecto es costoso (una llamada a base de datos, por ejemplo), usa siempre orElseGet.
orElseThrow: lanzar una excepción con sentido
Usuario user = findUserById("123")
.orElseThrow(() -> new UserNotFoundException("No existe el usuario 123"));
Esto es mucho más claro que un NullPointerException genérico ya que la excepción explica exactamente qué pasó.
Transformando valores con map y flatMap
Aquí es donde Optional resulta muy útil, porque te permite encadenar operaciones sin tener que anidar ifs por todas partes.
Optional<User> user = findUserById("123");
Optional<String> nameInUppercase = user
.map(User::getName)
.map(String::toUpperCase);
System.out.println(nameInUppercase.orElse("SIN NOMBRE"));
Si user está vacío, cada map simplemente se "salta" y el resultado final es un Optional vacío. No hace falta comprobar nulos en cada paso.
flatMap se usa cuando la función que aplicas ya devuelve un Optional, para evitar terminar con un Optional<Optional<T>>:
public Optional<Address> searchAddress(User user) {
return Optional.ofNullable(user.getAddress());
}
Optional<String> city = findUserById("123")
.flatMap(this::searchAddress)
.map(Direccion::getCity);
Ejecutando acciones con `ifPresent` e `ifPresentOrElse`
En vez de preguntar si hay valor y luego hacer algo, puedes decirle directamente a Optional qué hacer en cada caso:
findUserById("123")
.ifPresent(u -> System.out.println("Bienvenido, " + u.getName()));
Desde Java 9 también existe ifPresentOrElse, para manejar ambos casos a la vez:
findUserById("123").ifPresentOrElse(
u -> System.out.println("Bienvenido, " + u.getName()),
() -> System.out.println("Usuario no encontrado")
);
Ejemplo completo
public String getBillingCity(String idUser) {
return findUserById(idUser)
.filter(User::isActive)
.flatMap(this::searchAddress)
.map(Address::getCity)
.map(String::toUpperCase)
.orElse("CIUDAD NO DISPONIBLE");
}
Comparado con la alternativa de comprobaciones anidadas de null, la diferencia gigante.
Optional no elimina el concepto de de null, pero sí elimina la ambigüedad que existe. Te obliga a pensar en el caso en que el valor no exista.