Optional<T>

Valores potencialmente ausentes sin null

El problema de null

null en Java no distingue entre "no existe", "no se ha cargado todavía" y "hubo un error". Optional<T> hace explícita en el tipo de retorno la posibilidad de ausencia, obligando al llamador a tratarla.

// El problema clásico: null como ausencia de valor
public String obtenerCiudad(Usuario usuario) {
    if (usuario != null) {
        Direccion dir = usuario.getDireccion();
        if (dir != null) {
            return dir.getCiudad(); // puede ser null igualmente
        }
    }
    return "desconocida";
}
// Esto es el "billion-dollar mistake" de Tony Hoare

Crear un Optional

import java.util.Optional;

// Optional.of: el valor NO puede ser null (lanza NullPointerException si lo es)
Optional<String> conValor = Optional.of("Madrid");

// Optional.ofNullable: puede ser null; si lo es, crea un Optional vacío
String posibleNull = obtenerCiudadDeDB(); // puede devolver null
Optional<String> seguro = Optional.ofNullable(posibleNull);

// Optional.empty: sin valor
Optional<String> vacio = Optional.empty();

Extraer el valor

Preferir orElse, orElseGet u orElseThrow a get(). Un get() sin comprobar isPresent() antes es tan peligroso como el null que se intenta evitar.

Optional<String> ciudad = Optional.of("Barcelona");
Optional<String> sinCiudad = Optional.empty();

// get(): lanza NoSuchElementException si está vacío — evitar sin comprobar
String c1 = ciudad.get(); // "Barcelona"

// orElse: valor por defecto si vacío (siempre se evalúa)
String c2 = sinCiudad.orElse("desconocida"); // "desconocida"

// orElseGet: valor por defecto lazy (solo se evalúa si es necesario)
String c3 = sinCiudad.orElseGet(() -> calcularCiudadPorDefecto());

// orElseThrow: lanza excepción si vacío
String c4 = sinCiudad.orElseThrow(() -> new IllegalStateException("sin ciudad"));

// isPresent / isEmpty (Java 11+)
if (ciudad.isPresent()) { System.out.println(ciudad.get()); }

Transformar sin desempaquetar

map, filter y flatMap permiten encadenar operaciones sobre el valor interno sin salir del Optional ni comprobar isPresent() manualmente.

// map: transforma el valor si presente, no hace nada si vacío
Optional<String> ciudad = Optional.of("  Madrid  ");
Optional<String> limpia = ciudad.map(String::trim);       // Optional["Madrid"]
Optional<Integer> longitud = ciudad.map(String::length);  // Optional[9]

// filter: descarta el valor si no cumple el predicado
Optional<String> larga = ciudad.filter(c -> c.length() > 5); // vacío si no cumple

// flatMap: como map pero cuando la función ya devuelve Optional
// Evita Optional<Optional<T>>
Optional<Usuario> usuario = buscarUsuario(id);
Optional<String> ciudadUsuario = usuario.flatMap(u -> Optional.ofNullable(u.getCiudad()));

Ejemplo real

// Reescritura del ejemplo inicial con Optional
public Optional<String> obtenerCiudad(Optional<Usuario> usuario) {
    return usuario
        .map(Usuario::getDireccion)       // Optional<Direccion>
        .map(Direccion::getCiudad);       // Optional<String>
}

// Uso
String ciudad = obtenerCiudad(Optional.ofNullable(usuario))
    .orElse("desconocida");

Cuándo NO usar Optional

// MAL: Optional como campo de una clase (no serializable, overhead)
public class Persona {
    private Optional<String> telefono; // incorrecto
}

// BIEN: null en los campos, Optional solo en el retorno de métodos
public class Persona {
    private String telefono; // puede ser null internamente

    public Optional<String> getTelefono() {
        return Optional.ofNullable(telefono);
    }
}
Regla de uso: Optional es para valores de retorno de métodos que pueden no tener resultado. No usarlo como tipo de parámetro, como campo de clase ni en colecciones. Su propósito es hacer visible la ausencia en la API, no eliminar todo uso de null del código.

Siguiente apartado → Nueva API de fechas: LocalDate, LocalDateTime y ZonedDateTime

Índice de la sección

Índice del curso