W3docs

Java Optional

Expresa la posible ausencia de un valor en Java con Optional y evita NullPointerException por diseño.

Optional<T> es un contenedor que almacena ya sea un valor de tipo T o nada — y te indica cuál, a nivel de tipo, para que el compilador pueda obligarte a manejar el caso de ausencia. Se añadió en Java 8 junto con los streams, y ambos están diseñados para funcionar juntos: findFirst, findAny, min, max, reduce devuelven todos Optional<T> precisamente porque la respuesta podría no existir, y la API te ofrece formas fluidas de seguir calculando sin escribir nunca if (x != null).

Optional no es un reemplazo de null en todas partes, y el JDK tiene una opinión definida sobre dónde debe usarse. Este capítulo recorre la API de principio a fin, y luego los tres lugares donde Optional no es la llamada correcta.

Construyendo un Optional

Tres constructores, cada uno con un significado preciso:

Optional<String> a = Optional.of("hello");           // present; null arg throws NPE
Optional<String> b = Optional.empty();                // absent
Optional<String> c = Optional.ofNullable(maybeNull);  // present if non-null, else empty

La distinción importa. Optional.of(x) es la afirmación "este valor definitivamente está aquí" — si pasas null, lanza NullPointerException de inmediato, que es lo que deseas (un error expuesto en el origen, no tres marcos más adelante). Optional.ofNullable(x) es el adaptador que envuelves alrededor de una API heredada que devuelve null para "ausente."

Casi nunca construyes un Optional a mano dentro de un pipeline de stream — terminales como findFirst y Collectors.maxBy los producen por ti.

Verificar si un valor está presente

Las dos consultas:

Optional<String> opt = lookup(id);
boolean has = opt.isPresent();      // true if a value is held
boolean none = opt.isEmpty();        // Java 11+ -- the opposite of isPresent

Los verás en código de producción, pero generalmente son un olor a código: la mayoría del código que llama a isPresent y luego a get se leería mejor como uno de los métodos de operar-con-él que se muestran a continuación. Los métodos de consulta son para código de frontera donde realmente necesitas un boolean — una cláusula de guardia, una decisión de ruta, una rama de advertencia registrada.

Leer el valor de forma segura

La manera incorrecta:

String name = opt.get();   // throws NoSuchElementException if empty

opt.get() es la lectura sin verificación. Es la forma de convertir un Optional de nuevo en un valor y una excepción en tiempo de ejecución, exactamente lo que el tipo debía prevenir. Úsalo solo después de haber probado que el optional está presente (o después de findFirst().orElseThrow() de un pipeline donde vacío sería un error del programador, no un caso esperado).

Las formas correctas, en orden de preferencia:

String name1 = opt.orElse("anonymous");                          // default value
String name2 = opt.orElseGet(() -> expensiveDefault());          // lazy default
String name3 = opt.orElseThrow();                                 // NoSuchElementException
String name4 = opt.orElseThrow(() -> new MyDomainError(id));      // custom exception
  • orElse(value) — proporciona un valor predeterminado. El valor siempre se evalúa, incluso cuando el optional está presente, así que no pases una expresión costosa.
  • orElseGet(supplier) — proporciona un valor predeterminado de forma perezosa. El supplier solo se ejecuta cuando el optional está vacío. Úsalo para cualquier valor predeterminado que cueste más que un literal.
  • orElseThrow() — lanza NoSuchElementException si está ausente. La forma sin argumentos de Java 10+ es el equivalente moderno de opt.get() cuando "esto absolutamente debería estar presente" es la única interpretación sensata en el sitio de llamada.
  • orElseThrow(supplier) — lanza una excepción específica del dominio. La forma estándar de traducir "ausente" en "404 no encontrado."

Transformar el valor — map

Si el optional está presente, aplica una función; de lo contrario, permanece vacío:

Optional<String> upper = opt.map(String::toUpperCase);
Optional<Integer> len   = opt.map(String::length);

La firma es Optional<T>.map(Function<T, R>) -> Optional<R>. La función solo se ejecuta cuando hay un valor presente — no hay verificación de null, no hay if, y no hay else. Esta es la operación que hace que Optional valga la pena: la mayoría de las cadenas de "si no es null, haz esto; si no es null, entonces haz esto" colapsan en .map(...).map(...).map(...).

Hay un caso especial que el JDK maneja silenciosamente: si tu función map devuelve null (porque envuelve una API heredada que devuelve null para "sin resultado"), el Optional resultante es empty() — no Optional.of(null).

Componer optionals — flatMap

Cuando la función de mapeo en sí misma devuelve un Optional, map produciría Optional<Optional<T>>. flatMap lo aplana:

record User(String id, Optional<Address> address) {}
record Address(String city) {}

Optional<String> city = userById(id)
    .flatMap(User::address)        // Optional<Address>
    .map(Address::city);            // Optional<String>

flatMap es la operación que te permite encadenar varias búsquedas, cada una de las cuales puede fallar, en un único pipeline. Ambos casos de fallo colapsan a Optional.empty() al final, y el consumidor los maneja una vez con orElse / orElseThrow.

Filtrar — filter

Prueba el valor contra un Predicate<T>; devuelve el mismo optional si pasa, empty() si no:

Optional<String> nonBlank = opt.filter(s -> !s.isBlank());
Optional<Integer> positive = numberOpt.filter(n -> n > 0);

Actúa como una guardia dentro del pipeline del optional. Útil cuando la pregunta es "tengo un valor, pero ¿es el valor correcto para continuar?"

Efectos secundarios — ifPresent, ifPresentOrElse

Ejecuta código solo cuando el valor está presente:

opt.ifPresent(name -> log.info("hello, {}", name));

O ejecuta una rama cuando está presente y una diferente cuando está vacío (Java 9+):

opt.ifPresentOrElse(
    name -> log.info("hello, {}", name),
    () -> log.warn("no name on the request"));

Estas son la forma correcta de expresar "haz algo de paso." Reemplazan el patrón if (opt.isPresent()) { use(opt.get()); } por completo.

Conectar con streams — Optional.stream()

(Java 9+) Convierte un Optional<T> en un Stream<T> de cero o un elemento:

Stream<String> s = opt.stream();

Útil dentro de flatMap en un Stream<Optional<T>>:

List<String> presentCities = userIds.stream()
    .map(this::userById)           // Stream<Optional<User>>
    .flatMap(Optional::stream)      // Stream<User>     -- empties drop, presents pass through
    .map(User::city)
    .toList();

Eso reemplaza filter(Optional::isPresent).map(Optional::get) con un único flatMap(Optional::stream). Mismo resultado, pipeline más limpio.

or — recurrir a otro Optional

(Java 9+) Si está vacío, usa un supplier de otro Optional:

Optional<User> u = primaryLookup(id)
    .or(() -> fallbackLookup(id))
    .or(() -> Optional.of(User.anonymous()));

Se lee como "intenta primario; si ausente, intenta de respaldo; si ausente, usa anónimo." Los tres son Optional<User>; la cadena devuelve el primero que no esté vacío. Diferente de orElseor mantiene el resultado envuelto; orElse lo desenvuelve con un valor predeterminado T simple.

Especializaciones primitivas

Existen OptionalInt, OptionalLong, OptionalDouble para resultados primitivos — lo que devuelve IntStream.max(), por ejemplo:

OptionalInt max = nums.stream().mapToInt(Integer::intValue).max();
int hi = max.orElse(0);

Tienen una API más pequeña — sin map/flatMap/filter — porque se sitúan en el límite del mundo primitivo. Úsalos para leer resultados de streams primitivos; conviértelos a Optional<Integer> si necesitas la API completa.

Dónde Optional no pertenece

La intención de diseño del JDK es estrecha: Optional es un tipo de retorno para métodos cuya respuesta podría no existir. No es:

  • Un tipo de campo. No escribas private Optional<String> middleName;. No es Serializable, cuesta una asignación por campo, y un campo null es más corto y claro para "esta entidad no tiene segundo nombre." La opción correcta es un campo sin Optional que puede ser null, con un getter que devuelve Optional.
  • Un parámetro de método. No aceptes Optional<String> como argumento. Sobrecarga el método, o acepta String y documenta que null significa ausente. Los parámetros Optional requieren que el llamador envuelva, lo cual es ruido.
  • Un elemento de colección. List<Optional<T>> es casi siempre una lista con elementos que pueden ser null y envoltura extra. Usa List<T> y filtra los nulos en el límite, o usa flatMap(Optional::stream) para descartar los ausentes en un pipeline.
  • Una forma de evitar todo null. Java todavía tiene null en cada tipo de referencia; Optional es para la forma de retorno del código que produce valores que podrían no existir. Los tipos de referencia simples están bien para todo lo demás.

La regla más corta: un Optional que fluye hacia afuera de un método es buen diseño; un Optional que fluye hacia adentro casi siempre está mal.

Un ejemplo trabajado: cada método, más las reglas prácticas en código

El programa a continuación construye un pequeño grafo de usuario/dirección, recorre cada método de Optional con él, demuestra el tiempo de evaluación de orElse vs. orElseGet, el puente Optional.stream(), y la cadena or.

java— editable, runs on the server

Qué aprender de la ejecución:

  • Los tres constructores of, empty, ofNullable se mapean a tres intenciones claras: definitivamente presente, definitivamente ausente, y adaptador-legado, presente-si-no-null. Optional.of(null) lanza — y eso es el fallo deseado, no un error a solucionar.
  • orElse evaluó su argumento cada vez, incluso cuando el optional estaba presente. El supplier de orElseGet solo se ejecutó cuando fue necesario. Usa orElse para literales baratos y orElseGet para cualquier cosa que asigne, consulte o lance.
  • map y flatMap hicieron que toda la cadena userById(...).flatMap(User::address).map(Address::city) se lea como un único pipeline — sin verificaciones de null, sin ifs anidados, y cualquier paso vacío hace cortocircuito a Optional.empty() al final.
  • flatMap(Optional::stream) convirtió un Stream<Optional<User>> en un Stream<User> con todos los ausentes descartados de una vez. Esa es la forma limpia de conectar una lista de búsquedas "que-pueden-fallar" en un stream de éxitos.
  • OptionalInt es lo que devuelven los terminales de streams primitivos como IntStream.findFirst. Tiene su propia API pequeña (getAsInt, orElse, ifPresent) y existe para que los pipelines primitivos nunca tengan que encajonar.
  • La regla de los "lugares incorrectos" apareció implícitamente: User.address era un campo Optional<Address> — bien porque el ejemplo quería demostrar la API, pero en código de producción el campo sería una Address posiblemente null con un getter Optional<Address> address() que hace el envoltorio.

Qué sigue

La Parte 12 cubrió el vocabulario funcional de principio a fin: interfaces funcionales, lambdas, referencias a métodos, los integrados, el pipeline de stream, cada fuente, cada intermedio, cada terminal, collectors, ejecución paralela, y finalmente Optional como la expresión a nivel de tipo de la ausencia. El siguiente capítulo, Java Predicate Interface, vuelve a enfocarse en una única interfaz funcional — Predicate<T> — y el álgebra de combinadores (and, or, negate, isEqual, not) que te permite ensamblar predicados sin escribir nunca el pegamento booleano a mano. Desde ahí la parte continúa con Function, Consumer/Supplier, y la familia de operadores binarios — una interfaz por capítulo, cada una con la misma forma de ejemplo trabajado que has visto aquí.

Práctica

Práctica
Tienes `Optional<String> opt` y necesitas un valor predeterminado cuando esté vacío, donde el predeterminado es una llamada costosa a `loadDefaultFromDb()`. ¿Cuál es correcto *y* evita ejecutar la llamada costosa cuando `opt` está presente?
Tienes `Optional<String> opt` y necesitas un valor predeterminado cuando esté vacío, donde el predeterminado es una llamada costosa a `loadDefaultFromDb()`. ¿Cuál es correcto *y* evita ejecutar la llamada costosa cuando `opt` está presente?
Was this page helpful?