Día 6 de 30 Fase 1: Java Vanilla — El Núcleo del Lenguaje (17 & 21)

Programación Funcional: Streams, Optional y Pattern Matching en 17/21

Pipelines Perezosos, Sealed Interfaces y el Salto del Pattern Matching de Java 17 a Java 21

Para dominar. Al terminar deberías poder hacerlo sin ayuda.

  • 4h de estudio
  • 5 conceptos
  • 3 preguntas
  • 1 katas
Java 17 LTSJava 21 LTSStream APIOptionalSealed InterfacesPattern Matching SwitchLambdas
Al terminar este día sabrás 4
  • Predecir el orden en que se ejecuta un pipeline de streams y cuándo se detiene.
  • Escribir switch con pattern matching y record patterns en Java 21.
  • Modelar jerarquías cerradas con sealed y aprovechar la exhaustividad.
  • Usar Optional y las interfaces funcionales del JDK como un profesional.

Evaluación Perezosa (Lazy Evaluation) de la Streams API

Las operaciones intermedias (filter, map) no procesan ningún dato cuando se declaran; solo construyen un plan. El procesamiento solo arranca al invocar la operación terminal (toList, collect).

En una línea lista.stream().filter(x -> x > 10).map(String::valueOf).toList();

Ver el orden real de ejecución java
Optional<String> primero = Stream.of("ana", "bea", "carlos", "dani")
    .filter(n -> {
        System.out.println("filter " + n);
        return n.length() > 3;
    })
    .map(n -> {
        System.out.println("map " + n);
        return n.toUpperCase();
    })
    .findFirst();

// filter ana
// filter bea
// filter carlos
// map carlos       <- "dani" nunca se procesa: findFirst ya tiene respuesta

// Fuente infinita: solo funciona gracias a la pereza
List<Integer> potencias = Stream.iterate(1, x -> x * 2)
    .limit(10)
    .toList();
Cómo funciona por dentro

Un stream se compone de una fuente, cero o más operaciones intermedias (filter, map, sorted) y una operación terminal (toList, count, findFirst). Las intermedias no hacen nada por sí solas: construyen una receta. Solo cuando invocas la terminal empieza a fluir el primer elemento.

El procesamiento es vertical: cada elemento recorre todo el pipeline antes de que entre el siguiente, en lugar de aplicar filter a toda la lista y después map a todo el resultado. Eso permite el cortocircuito: con findFirst, anyMatch o limit, el stream deja de pedir elementos en cuanto tiene la respuesta, y funciona incluso con fuentes infinitas.

Las excepciones son las operaciones con estado como sorted o distinct, que necesitan ver todos los elementos antes de emitir el primero. Un stream además solo se puede consumir una vez; reutilizarlo lanza IllegalStateException.

Permite optimizaciones extremas: si encadenas 5 filtros y terminas con .findFirst(), el Stream se detiene en cuanto encuentra el primer elemento sin procesar el resto de la lista.

Se conecta con Interfaces FuncionalesPipelinesOptimización

Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.

  1. Bloque 1: Anatomía de Streams y Laziness (0 - 25 min)

    Qué estudiar. Operaciones intermedias vs terminales. Comportamiento perezoso.

    Pregunta a responder. ¿Cómo verificar con peek() qué elementos realmente son procesados en un pipeline corto?

    Resultado esperado. Pipeline con filter, map y limit(1) demostrando que solo procesa los elementos mínimos necesarios.

  2. Bloque 2: Sealed Interfaces y Java 17 vs 21 (25 - 50 min)

    Qué estudiar. Sintaxis de sealed interface y compilación en Java 17 frente a Pattern Matching switch en Java 21.

    Pregunta a responder. ¿Por qué el compilador no exige default: cuando cubre todos los permisos de la interfaz sellada?

    Resultado esperado. Jerarquía EstadoPago sellada evaluada con switch exhaustivo.

  3. Bloque 3: Dominio de Optional Idiomático (50 - 80 min)

    Qué estudiar. Métodos .map(), .flatMap(), .orElseGet(), .orElseThrow().

    Pregunta a responder. ¿Cuál es la diferencia de rendimiento entre orElse("defecto") y orElseGet(() -> calcularDefecto())?

    Resultado esperado. Refactorización de código imperativo con nulos a Optional funcional limpio.

  4. Cierre y Flash-Recall (80 - 90 min)

    Qué estudiar. Repaso activo de las diferencias Java 17 vs 21.

    Pregunta a responder. Explicar qué es un Record Pattern y en qué versión de Java se convirtió en estándar.

    Resultado esperado. Checklist de katas del día registradas en la base de datos local.

Tres formas de comprobar si lo has entendido, de menos a más exigente: explicarlo con tus palabras, responder preguntas trampa y resolver el reto del tutor.

Explícalo desde cero

Lee cada ficha y explícala en voz alta antes de seguir: si puedes contárselo a alguien que empieza, lo has entendido.

La diferencia entre una operación intermedia y terminal en Streams

Lo que debes entender

Una operación intermedia es como programar una máquina en una fábrica (poner la cuchilla de corte). La operación terminal es pulsar el botón de encendido: hasta que no pulsas el botón, no se mueve una sola pieza.

Analogía para contarlo

Una cinta transportadora en un aeropuerto: colocar las compuertas de inspección (filter) no mueve las maletas; el motor sólo arranca cuando llega el contenedor final (toList).

Lo que suele explicarse mal

Pensar que los Streams son colecciones. Un Stream no almacena nada; es una tubería por la que circulan datos.

Señal de que aún no lo dominas

Si intentas depurar un stream poniendo un peek() sin poner una operación terminal y te preguntas por qué no imprime nada.

Preguntas trampa

Responde de memoria, en voz alta o por escrito, y después abre la respuesta razonada: lo importante es el porqué, no la palabra exacta.

Nivel 1 (Básico) ¿Por qué Stream.of(1, 2, 3).peek(System.out::println); no imprime absolutamente nada en la consola? Ver respuesta

Respuesta razonada

Porque peek es una operación intermedia y el pipeline no tiene operación terminal. Las intermedias solo describen el trabajo; nada se ejecuta hasta que una terminal (toList, forEach, count...) pide elementos.

Un matiz más avanzado: incluso con terminal, desde Java 9 count() puede no ejecutar peek ni map si puede calcular el tamaño directamente de la fuente. Por eso peek sirve para depurar, nunca para efectos secundarios de los que dependa tu lógica.

Qué está evaluando la pregunta

Porque peek es una operación intermedia. Al no existir una operación terminal (.toList(), .count()), el stream nunca llega a ejecutarse por evaluación perezosa.

Nivel 2 (Intermedio) ¿Cómo manejas en Java 17 un switch exhaustivo sobre una jerarquía sealed frente a Java 21? Ver respuesta

Respuesta razonada

En Java 17, el pattern matching en switch era preview. Sin activar --enable-preview, lo habitual era una cadena de if (x instanceof Tipo t) terminada en throw, porque el compilador no puede verificar que cubres todos los casos. Lo que sí se aprovechaba de sealed era que la jerarquía estaba documentada y cerrada.

En Java 21 escribes un switch con un case por subtipo y sin default. El compilador comprueba la exhaustividad: si mañana se añade un subtipo a la lista permits, todos los switch que no lo tratan dejan de compilar. Con default perderías esa red de seguridad.

Qué está evaluando la pregunta

En Java 17 se recurre a cadenas de if (o instanceof Subtipo s) o switch clásico. En Java 21 se usa directamente switch (objeto) { case Subtipo s -> ... } sin necesidad de default.

Nivel 3 (Entrevista Senior) ¿Por qué Optional no implementa Serializable y qué impacto tiene si lo usas como atributo en una entidad JPA o DTO enviado por red? Ver respuesta

Respuesta razonada

Optional no implementa Serializable a propósito: se diseñó solo como tipo de retorno, no para guardarlo ni enviarlo. Si lo usas como campo de una clase que se serializa con la serialización nativa de Java, falla con NotSerializableException. JPA tampoco sabe mapear un Optional como columna.

Con Jackson se puede serializar registrando el módulo jdk8 (Spring Boot lo hace), pero sigue siendo mala práctica en un DTO: añade un nivel de indirección sin aportar nada, porque en JSON un campo ausente o null ya expresa la ausencia. La pauta es campo normal (posiblemente null) y getter que devuelve Optional.ofNullable(campo) si quieres expresar opcionalidad en la API.

Qué está evaluando la pregunta

Los creadores de Java intencionalmente no hicieron Optional serializable porque fue pensado solo para retornos de métodos. Si lo usas como campo en JPA o serializadores binarios de Java, lanzará NotSerializableException.

Reto del tutor

Un programador escribe: public String obtenerNombre(Optional<Usuario> usuarioOpt) { if (usuarioOpt.isPresent()) return usuarioOpt.get().getNombre(); else return "Desconocido"; }. Critica esta implementación como revisor senior.

Lo que una respuesta suele tener bien

Comprueba la presencia con isPresent() antes de llamar a get(), evitando NoSuchElementException.

Lo que suele quedarse en la superficie

Decir que "está bien porque no da error".

El concepto de fondo que suele faltar

Usa Optional de forma imperativa como si fuera una comprobación de nulo tradicional (!= null), ignorando la API funcional, y pone un Optional como parámetro de entrada en el método.

Cómo lo respondería alguien senior en una entrevista

El método tiene dos defectos graves de diseño: (1) NUNCA debe pasarse un Optional como parámetro de entrada en un método; debe pasarse Usuario usuario directamente (o lanzar IllegalArgumentException si es nulo); (2) Si se recibe un Optional, debe resolverse funcionalmente en una sola línea idiomática: return usuarioOpt.map(Usuario::getNombre).orElse("Desconocido");.

Proyecto «Pedidos» · paso 6 de 30

Informes de ventas con streams

Responder preguntas de negocio sobre los pedidos sin bucles manuales.

De qué va el proyecto

El backend de una tienda online: clientes, catálogo de productos y pedidos con sus líneas, pagos y envíos. Empieza como un proyecto Maven con records y termina como un sistema con API REST, seguridad, mensajería, contenedores y observabilidad. Cada paso construye sobre el anterior, así que trabaja siempre en el mismo repositorio.

Qué hacer

  1. Calcula el total vendido por producto con groupingBy y reducing.
  2. Obtén los cinco clientes que más han gastado.
  3. Cuenta los pedidos por estado y devuelve el resultado en un EnumMap.
  4. Calcula la comisión de cada pago con un switch con patrones sobre MetodoPago, sin default.

Está listo cuando

  • Las búsquedas que pueden no encontrar nada devuelven Optional, nunca null.
  • Cada informe tiene un test con datos conocidos y su resultado esperado.

Katas: ejercicios cortos

Reglas de oro

  • Usa Optional solo como tipo de retorno de métodos, jamás como parámetro ni atributo de clase.
  • En Java 21 aprovecha el pattern matching para switch con interfaces selladas para evitar cadenas de if-else.
  • Prefiere .orElseGet(() -> calculoLento()) sobre .orElse(calculoLento()) para evitar ejecuciones innecesarias.

Java 17 frente a Java 21

CaracterísticaJava 17 LTSJava 21 LTS
Sealed Classes/Interfaces Estándar oficial en 17 LTS Estándar oficial en 21 LTS
Pattern Matching en sentencias switch Solo Preview (no apto producción) Estándar oficial definitivo en 21 LTS
Stream.toList() inmutable Disponible desde Java 16/17 Disponible
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown