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 emptyLa 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 isPresentLos 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 emptyopt.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 exceptionorElse(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()— lanzaNoSuchElementExceptionsi está ausente. La forma sin argumentos de Java 10+ es el equivalente moderno deopt.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 orElse — or 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 esSerializable, cuesta una asignación por campo, y un camponulles más corto y claro para "esta entidad no tiene segundo nombre." La opción correcta es un campo sin Optional que puede sernull, con un getter que devuelveOptional. - Un parámetro de método. No aceptes
Optional<String>como argumento. Sobrecarga el método, o aceptaStringy documenta quenullsignifica 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 sernully envoltura extra. UsaList<T>y filtra los nulos en el límite, o usaflatMap(Optional::stream)para descartar los ausentes en un pipeline. - Una forma de evitar todo
null. Java todavía tienenullen cada tipo de referencia;Optionales 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.
Qué aprender de la ejecución:
- Los tres constructores
of,empty,ofNullablese 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. orElseevaluó su argumento cada vez, incluso cuando el optional estaba presente. El supplier deorElseGetsolo se ejecutó cuando fue necesario. UsaorElsepara literales baratos yorElseGetpara cualquier cosa que asigne, consulte o lance.mapyflatMaphicieron que toda la cadenauserById(...).flatMap(User::address).map(Address::city)se lea como un único pipeline — sin verificaciones denull, sin ifs anidados, y cualquier paso vacío hace cortocircuito aOptional.empty()al final.flatMap(Optional::stream)convirtió unStream<Optional<User>>en unStream<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.OptionalIntes lo que devuelven los terminales de streams primitivos comoIntStream.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.addressera un campoOptional<Address>— bien porque el ejemplo quería demostrar la API, pero en código de producción el campo sería unaAddressposiblementenullcon un getterOptional<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í.