Saltar al contenido
optional.java · devschool

Optional en Java

Lección 33 de 39 · 12 min de lectura · Actualizado el

En esta lección
  1. El problema de null
  2. Qué es Optional
  3. Comprobar si hay valor
  4. Sacar el valor con un plan B
  5. Actuar solo si hay valor: ifPresent
  6. Transformar sin abrir la caja: map, filter y flatMap
  7. Optional en los streams
  8. Cuándo NO usar Optional
  9. Errores frecuentes
  10. Resumen

Si llevas un tiempo programando en Java, ya te habrás encontrado con el error más famoso del lenguaje: NullPointerException. Aparece cuando usas una variable que no apunta a ningún objeto, y casi siempre ocurre porque algún método devolvió null y nadie lo comprobó. Optional es la herramienta que Java ofrece desde la versión 8 para decir de forma explícita “este método puede no devolver nada”. En esta lección aprenderás a crearlo, a sacar su valor de forma segura, a transformarlo con map y filter, a usarlo con streams y, muy importante, cuándo no usarlo.

El problema de null

Imagina el programa de un gimnasio con una lista de socios. Un método busca un socio por su DNI y, si no lo encuentra, devuelve null:

record Socio(String dni, String nombre, String entrenador, int edad) {}

static final List<Socio> SOCIOS = List.of(
        new Socio("111A", "Marta", "Rubén", 34),
        new Socio("222B", "Gonzalo", null, 19),     // aún no tiene entrenador
        new Socio("333C", "Irene", "Sonia", 52));

static Socio buscarSocioViejo(String dni) {
    for (Socio s : SOCIOS) {
        if (s.dni().equals(dni)) {
            return s;
        }
    }
    return null;   // no encontrado
}

Mientras el DNI exista, todo va bien. Pero quien llama al método no tiene ninguna pista de que puede recibir null: la firma solo dice “devuelve un Socio”. Si se le olvida comprobarlo:

Socio s1 = buscarSocioViejo("111A");
System.out.println(s1.nombre());               // Marta

Socio s2 = buscarSocioViejo("999Z");
System.out.println(s2.nombre().toUpperCase());
// Exception in thread "main" java.lang.NullPointerException:
//   Cannot invoke "Socio.nombre()" because "s2" is null

Desde Java 14 el mensaje te dice qué variable era null, lo que ayuda a depurar, pero el programa se ha detenido igualmente. El inventor de la referencia nula, Tony Hoare, la llamó su “error del billón de dólares” por la cantidad de fallos que ha causado.

La solución clásica es comprobar if (s2 != null) en todas partes. Funciona, pero es fácil olvidarlo, y llena el código de comprobaciones. Optional ataca el problema desde otro ángulo: hace visible en el tipo que el valor puede faltar.

Qué es Optional

Un Optional<T> es una caja que puede estar en dos estados:

  • Llena: contiene exactamente un valor de tipo T.
  • Vacía: no contiene nada.

Un método que devuelve Optional<Socio> está diciendo claramente: “puede que haya socio y puede que no; tú decides qué hacer en cada caso”. El compilador no te deja usar el Socio directamente: primero tienes que abrir la caja.

Crear un Optional

MétodoCuándo se usa
Optional.of(valor)Tienes un valor y seguro que no es null
Optional.ofNullable(valor)El valor puede ser null (si lo es, la caja queda vacía)
Optional.empty()Quieres devolver una caja vacía
Optional<String> lleno = Optional.of("café");
Optional<String> vacio = Optional.empty();

String quizas = null;
Optional<String> desdeNull = Optional.ofNullable(quizas);

System.out.println(lleno);     // Optional[café]
System.out.println(vacio);     // Optional.empty
System.out.println(desdeNull); // Optional.empty

Optional.of(quizas);           // NullPointerException: of no admite null

Ahora reescribimos la búsqueda para que devuelva un Optional:

static Optional<Socio> buscarSocio(String dni) {
    for (Socio s : SOCIOS) {
        if (s.dni().equals(dni)) {
            return Optional.of(s);
        }
    }
    return Optional.empty();
}

Con un stream queda todavía más corto, porque findFirst() ya devuelve un Optional:

static Optional<Socio> buscarSocio(String dni) {
    return SOCIOS.stream()
            .filter(s -> s.dni().equals(dni))
            .findFirst();
}

Comprobar si hay valor

isPresent() devuelve true si la caja está llena; isEmpty() (Java 11) justo lo contrario. Y get() saca el valor:

Optional<Socio> marta = buscarSocio("111A");
Optional<Socio> nadie = buscarSocio("999Z");

System.out.println(marta.isPresent() + " " + marta.isEmpty()); // true false
System.out.println(nadie.isPresent() + " " + nadie.isEmpty()); // false true

if (marta.isPresent()) {
    System.out.println("Hola, " + marta.get().nombre()); // Hola, Marta
}

nadie.get(); // NoSuchElementException: No value present

Esta forma funciona, pero es casi lo mismo que el if (x != null) de siempre, y get() sobre una caja vacía lanza una excepción. Los métodos que vienen a continuación son más seguros y más cortos.

Cuidado: si te ves escribiendo if (opt.isPresent()) { ... opt.get() ... } muchas veces, probablemente hay un método de Optional que lo hace mejor. get() sin comprobar antes es el error más común con Optional.

Sacar el valor con un plan B

orElse: valor por defecto

orElse(x) devuelve el contenido de la caja o, si está vacía, x:

String nombre1 = buscarSocio("333C").map(Socio::nombre).orElse("Invitado");
String nombre2 = buscarSocio("999Z").map(Socio::nombre).orElse("Invitado");
System.out.println(nombre1 + " / " + nombre2); // Irene / Invitado

El map(Socio::nombre) transforma la caja de Socio en una caja de String; lo verás enseguida.

orElseGet: valor por defecto calculado

orElseGet recibe una lambda (un Supplier) que solo se ejecuta si la caja está vacía. La diferencia con orElse es sutil pero importante: el argumento de orElse se calcula siempre, haga falta o no. Lo vemos con un método que avisa cuando se ejecuta:

static String nombrePorDefecto() {
    System.out.println("  (calculando nombre por defecto...)");
    return "Invitado";
}
System.out.println("orElse con socio existente:");
String a = buscarSocio("111A").map(Socio::nombre).orElse(nombrePorDefecto());
System.out.println("  -> " + a);

System.out.println("orElseGet con socio existente:");
String b = buscarSocio("111A").map(Socio::nombre).orElseGet(() -> nombrePorDefecto());
System.out.println("  -> " + b);
// orElse con socio existente:
//   (calculando nombre por defecto...)
//   -> Marta
// orElseGet con socio existente:
//   -> Marta

Con orElse, nombrePorDefecto() se ha ejecutado aunque Marta existía y el resultado se ha tirado a la basura. Si ese cálculo fuera una consulta a una base de datos o crear un objeto pesado, estarías malgastando recursos en cada llamada.

MétodoEl valor por defecto se calcula…Úsalo con…
orElse(x)SiempreConstantes y valores ya calculados: "Invitado", 0
orElseGet(() -> ...)Solo si la caja está vacíaLlamadas a métodos, objetos nuevos, cálculos

orElseThrow: si no hay valor, es un error

A veces que falte el valor no es una situación normal, sino un error que hay que avisar. orElseThrow lanza una excepción si la caja está vacía:

Socio irene = buscarSocio("333C").orElseThrow();   // NoSuchElementException si falta
System.out.println(irene.nombre());                 // Irene

Socio x = buscarSocio("999Z")
        .orElseThrow(() -> new IllegalArgumentException("No existe el socio 999Z"));
// IllegalArgumentException: No existe el socio 999Z

La versión sin argumentos (Java 10) hace lo mismo que get(), pero su nombre deja claro que puede lanzar una excepción. Si sabes que el valor tiene que estar, prefiérela a get().

Actuar solo si hay valor: ifPresent

Cuando no quieres un resultado sino hacer algo con el valor, usa ifPresent. Si la caja está vacía, simplemente no pasa nada:

buscarSocio("222B").ifPresent(s -> System.out.println("Bienvenido, " + s.nombre()));
// Bienvenido, Gonzalo

buscarSocio("999Z").ifPresent(s -> System.out.println("Bienvenido, " + s.nombre()));
// (no imprime nada)

Si también quieres hacer algo en el caso vacío, ifPresentOrElse (Java 9) recibe dos acciones:

buscarSocio("999Z").ifPresentOrElse(
        s -> System.out.println("Bienvenido, " + s.nombre()),
        () -> System.out.println("DNI no registrado"));
// DNI no registrado

Transformar sin abrir la caja: map, filter y flatMap

Lo mejor de Optional es que puedes trabajar con el valor sin sacarlo, igual que con un stream. Si la caja está vacía, estas operaciones no hacen nada y la caja sigue vacía.

map

Aplica una función al contenido y devuelve una caja con el resultado:

Optional<String> mayus = buscarSocio("111A")
        .map(Socio::nombre)
        .map(String::toUpperCase);
System.out.println(mayus); // Optional[MARTA]

Si la función devuelve null, map devuelve una caja vacía. Eso permite recorrer atributos que pueden faltar sin un solo if:

for (String dni : List.of("111A", "222B", "999Z")) {
    String entrenador = buscarSocio(dni)
            .map(Socio::entrenador)
            .orElse("sin asignar");
    System.out.println(dni + ": " + entrenador);
}
// 111A: Rubén
// 222B: sin asignar
// 999Z: sin asignar

Gonzalo existe pero no tiene entrenador; el DNI 999Z ni siquiera existe. En los dos casos el resultado es el mismo plan B, sin NullPointerException.

filter

Deja el valor solo si cumple una condición; si no, la caja queda vacía. Por ejemplo, un descuento para mayores de 50:

Optional<Socio> senior = buscarSocio("333C").filter(s -> s.edad() >= 50);
Optional<Socio> noSenior = buscarSocio("111A").filter(s -> s.edad() >= 50);
System.out.println(senior.map(Socio::nombre) + " " + noSenior.map(Socio::nombre));
// Optional[Irene] Optional.empty

String descuento = buscarSocio("333C")
        .filter(s -> s.edad() >= 50)
        .map(s -> "20 %")
        .orElse("0 %");
System.out.println("descuento: " + descuento); // descuento: 20 %

flatMap

Si la función que aplicas ya devuelve un Optional, map te dejaría una caja dentro de otra caja. flatMap lo aplana:

static Optional<String> entrenadorDe(Socio s) {
    return Optional.ofNullable(s.entrenador());
}
System.out.println(buscarSocio("111A").map(Opt::entrenadorDe));     // Optional[Optional[Rubén]]
System.out.println(buscarSocio("111A").flatMap(Opt::entrenadorDe)); // Optional[Rubén]
System.out.println(buscarSocio("222B").flatMap(Opt::entrenadorDe)); // Optional.empty

(Opt es el nombre de la clase donde está el método entrenadorDe.)

or: otra caja como alternativa

or (Java 9) se parece a orElseGet, pero el plan B es otro Optional. Sirve para encadenar búsquedas: “busca aquí; si no está, busca allí”:

Optional<Socio> s = buscarSocio("999Z").or(() -> buscarSocio("222B"));
System.out.println(s.map(Socio::nombre).orElse("-")); // Gonzalo

Optional en los streams

Ya lo intuiste en lambdas y streams: las operaciones finales que pueden no tener resultado devuelven un Optional. Si el stream está vacío o ningún elemento cumple el filtro, no hay “primero” ni “máximo”.

OperaciónDevuelve
findFirst(), findAny()Optional<T>
max(comp), min(comp)Optional<T>
reduce(operacion)Optional<T>
average() en IntStream/DoubleStreamOptionalDouble
max(), min() en IntStreamOptionalInt
List<Integer> tiempos = List.of(48, 52, 45, 60);   // minutos en una carrera
Optional<Integer> mejor = tiempos.stream().min(Integer::compare);
System.out.println("Mejor tiempo: " + mejor.orElseThrow() + " min"); // Mejor tiempo: 45 min

Optional<Socio> mayor = SOCIOS.stream().max(Comparator.comparingInt(Socio::edad));
System.out.println(mayor.map(Socio::nombre).orElse("nadie")); // Irene

Optional<String> conR = SOCIOS.stream()
        .map(Socio::nombre)
        .filter(n -> n.startsWith("R"))
        .findFirst();
System.out.println(conR); // Optional.empty

Para los números primitivos existen OptionalInt, OptionalLong y OptionalDouble, con getAsInt(), getAsDouble()… en lugar de get():

OptionalDouble media = SOCIOS.stream().mapToInt(Socio::edad).average();
System.out.println(media);                // OptionalDouble[35.0]
System.out.println(media.orElse(0));      // 35.0

OptionalDouble mediaVacia = IntStream.empty().average();
System.out.println(mediaVacia.orElse(0)); // 0.0

Y al revés: Optional tiene un método stream() (Java 9) que da un stream de cero o un elemento. Con flatMap sirve para quedarte solo con los valores que existen:

List<String> entrenadores = SOCIOS.stream()
        .map(Opt::entrenadorDe)          // Stream<Optional<String>>
        .flatMap(Optional::stream)       // descarta los vacíos
        .toList();
System.out.println(entrenadores); // [Rubén, Sonia]

Cuándo NO usar Optional

Optional se diseñó con un propósito muy concreto: el valor de retorno de un método que puede no tener resultado. Fuera de ahí suele empeorar el código:

  • Atributos de una clase. No declares private Optional<String> entrenador;. Optional no es serializable y añade un objeto extra por cada atributo. Guarda el atributo normal (puede ser null) y, si quieres, haz que su getter devuelva Optional.ofNullable(entrenador).
  • Parámetros de métodos. void apuntar(Optional<String> clase) obliga a quien llama a envolver sus datos y, además, podría pasarte null en lugar de un Optional. Mejor dos versiones del método (sobrecarga) o un parámetro que admita null documentado.
  • Colecciones. No devuelvas Optional<List<Socio>>. Una lista vacía ya significa “no hay resultados”: devuelve List.of().
  • Para sustituir cualquier if. Optional.ofNullable(x).ifPresent(...) solo para evitar un if (x != null) en un método corto no aporta nada.
  • Nunca devuelvas null en un método que devuelve Optional. Rompe la única promesa que hace el tipo. Devuelve Optional.empty().
SituaciónMejor opción
Método que busca y puede no encontrarOptional<T>
Método que devuelve varios resultadosLista (vacía si no hay)
Atributo que puede faltarAtributo normal + getter con Optional
Parámetro opcionalSobrecarga de métodos

Errores frecuentes

  • Llamar a get() sin comprobar. En una caja vacía lanza NoSuchElementException. Usa orElse, orElseGet, orElseThrow o ifPresent.
  • Usar Optional.of con un valor que puede ser null. Lanza NullPointerException; usa ofNullable.
  • Poner un cálculo costoso en orElse. Se ejecuta siempre; usa orElseGet.
  • Devolver null desde un método que devuelve Optional. Devuelve Optional.empty().
  • Usar map con una función que devuelve Optional. Obtienes Optional<Optional<T>>; usa flatMap.
  • Usar Optional en atributos, parámetros o colecciones. Solo como tipo de retorno.
  • Comparar un Optional con null (if (opt == null)). Un Optional bien usado nunca es null; pregunta opt.isEmpty().

Resumen

ConceptoCódigo
CrearOptional.of(x), Optional.ofNullable(x), Optional.empty()
ConsultarisPresent(), isEmpty()
Valor o defectoorElse(x), orElseGet(() -> ...)
Valor o excepciónorElseThrow(), orElseThrow(() -> new ...)
Hacer algo si hay valorifPresent(...), ifPresentOrElse(..., ...)
Transformarmap(f), flatMap(f), filter(p), or(() -> ...)
En streamsfindFirst(), max(c), min(c), average()
Regla de oroSolo como tipo de retorno; nunca get() sin comprobar

En la siguiente lección aprenderás a trabajar con fechas, horas y duraciones con la API moderna de Java: fechas con java.time.

Pon a prueba lo que has aprendido

[Java] Un método recibe un texto que puede ser null y quieres un Optional que quede vacío en ese caso. ¿Qué usas?

[Java] ¿Qué imprime este programa?
static String nombrePorDefecto() {
    System.out.println("calculando");
    return "Invitado";
}

System.out.println(Optional.of("Marta").orElse(nombrePorDefecto()));

[Java] ¿Qué imprimen estas dos líneas?
Optional<String> o = Optional.of("a");
System.out.println(o.map(s -> Optional.of(s + "b")));
System.out.println(o.flatMap(s -> Optional.of(s + "b")));

[Java] ¿Qué imprime este código?
String r = Optional.of(17)
        .filter(e -> e >= 18)
        .map(e -> "adulto")
        .orElse("menor");
System.out.println(r);

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.