# Día 6: 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

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 6 de 30
- Fase 1: Java Vanilla — El Núcleo del Lenguaje (17 & 21)
- Dedicación: 4h
- Profundidad: para dominar
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/streams-optional-pattern-matching/

## Al terminar este día sabrás

- 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.


## Conceptos

### 1. 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).

**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`.

*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();
```

- **En una línea:** `lista.stream().filter(x -> x > 10).map(String::valueOf).toList();`
- **Por qué importa:** 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.
- **Error típico:** Creer que cada `.filter()` crea una lista intermedia en memoria; el Stream procesa elemento a elemento en una sola pasada vertical.
- **Java 17 frente a 21:** `toList()` inmutable disponible tanto en Java 17 como en 21.
- **Se conecta con:** Interfaces Funcionales, Pipelines, Optimización

### 2. Pattern Matching para switch y Jerarquías Sealed

En Java 21, `switch` puede evaluar directamente tipos de objetos complejos y desestructurarlos sin castings manuales `(Tipo) objeto`. Combinado con una jerarquía `sealed`, el compilador comprueba que tratas todos los casos sin necesidad de `default`.

**Cómo funciona por dentro.**

En Java 17 el pattern matching para `switch` existía solo como preview (había que compilar con `--enable-preview`). Lo estable era el pattern matching de `instanceof` (`if (obj instanceof String s)`) y los switch expressions con flecha, que devuelven valor y no tienen fall-through.

Java 21 lo hace definitivo (JEP 441) junto con los record patterns (JEP 440). Puedes hacer `switch` sobre cualquier tipo de objeto con casos por tipo, añadir condiciones con `when`, tratar `case null` explícitamente y desestructurar records directamente en el patrón, incluso anidados.

El compilador comprueba el orden (un caso más general antes que uno más específico no compila porque el segundo sería inalcanzable) y, si el tipo es `sealed`, comprueba que cubres todos los subtipos sin necesidad de `default`.

*Del instanceof encadenado al switch con patrones*

```java
sealed interface Evento permits Creada, Completada, Reasignada {}
record Creada(long id, String titulo) implements Evento {}
record Completada(long id, Instant cuando) implements Evento {}
record Reasignada(long id, String de, String a) implements Evento {}

// Java 17: instanceof con pattern matching, sin exhaustividad
String describir17(Evento e) {
    if (e instanceof Creada c) return "Nueva: " + c.titulo();
    if (e instanceof Completada c) return "Completada " + c.id();
    if (e instanceof Reasignada r) return r.de() + " -> " + r.a();
    throw new IllegalStateException();          // el compilador no sabe que no hay más
}

// Java 21: switch exhaustivo con record patterns y guardas
String describir(Evento e) {
    return switch (e) {
        case Creada(var id, var titulo) when titulo.length() > 40 -> "Nueva (título largo) #" + id;
        case Creada(var id, var titulo) -> "Nueva: " + titulo;
        case Completada(var id, var cuando) -> "Completada #" + id + " el " + cuando;
        case Reasignada(var id, var de, var a) -> "#" + id + ": " + de + " -> " + a;
    };                                          // sin default: si añades un evento, no compila
}
```

- **En una línea:** `return switch (figura) { case Circulo c -> Math.PI * c.radio(); case Cuadrado q -> q.lado() * q.lado(); };`
- **Por qué importa:** Elimina cadenas interminables de `if-else if-else instanceof` propensas a errores de tipado.
- **Error típico:** Intentar compilar un switch sobre tipos de clases en un entorno configurado con target de Java 17.
- **Java 17 frente a 21:** EN JAVA 17: solo `instanceof` con patrón. EN JAVA 21: `switch (objeto)` exhaustivo como estándar oficial.
- **Se conecta con:** Sealed Interfaces, Java 21 LTS, Clean Code

### 3. Lambdas frente a Clases Anónimas y Composición de Funciones

Antes de Java 8, pasar comportamiento exigía una clase anónima, una clase anidada sin nombre. Una lambda es una forma corta de implementar una interfaz funcional, y las referencias a método (`Tarea::titulo`) aún más. `Function`, `Predicate` y `Comparator` se combinan con `andThen`, `compose`, `and`, `negate` y `thenComparing`.

**Cómo funciona por dentro.**

Una clase anónima declara y crea a la vez una clase sin nombre que implementa una interfaz o extiende una clase: `new Comparator<Tarea>() { public int compare(...) { ... } }`. Es una de las cuatro formas de clase anidada (estática, interna, local y anónima), y antes de Java 8 era la única forma de pasar comportamiento. Una lambda es una sintaxis mucho más corta para implementar una interfaz funcional, es decir, con un único método abstracto; las referencias a método (`String::length`, `Tarea::titulo`, `this::validar`) son aún más cortas cuando la lambda solo llama a un método.

No son idénticas. Dentro de una clase anónima, `this` es la propia clase anónima; en una lambda, `this` es el objeto que la contiene. Las dos solo pueden capturar variables locales final o efectivamente final. Las clases internas no estáticas, además, guardan una referencia al objeto exterior, y eso puede retener memoria sin que lo notes.

Las interfaces funcionales del JDK traen métodos para componer: `Function.andThen` y `compose` encadenan transformaciones; `Predicate.and`, `or`, `negate` y `Predicate.not` combinan condiciones; y `Comparator.comparing(...).thenComparing(...).reversed()` construye ordenaciones complejas. Son funciones de orden superior: reciben o devuelven funciones.

*De la clase anónima a la composición*

```java
// Antes de Java 8: clase anónima
tareas.sort(new Comparator<Tarea>() {
    @Override
    public int compare(Tarea a, Tarea b) {
        return a.vence().compareTo(b.vence());
    }
});

// Lambda y referencia a método
tareas.sort((a, b) -> a.vence().compareTo(b.vence()));
tareas.sort(Comparator.comparing(Tarea::vence));

// Composición de comparadores
tareas.sort(Comparator.comparing(Tarea::prioridad).reversed()
                      .thenComparing(Tarea::vence)
                      .thenComparing(Tarea::titulo, String.CASE_INSENSITIVE_ORDER));

// Composición de predicados y funciones
Predicate<Tarea> urgente = t -> t.prioridad() == Prioridad.ALTA;
Predicate<Tarea> pendienteUrgente = urgente.and(Predicate.not(Tarea::estaHecha));

Function<String, String> limpiar = String::strip;
Function<String, String> normalizar = limpiar.andThen(String::toLowerCase);
normalizar.apply("  Revisar PR ");               // "revisar pr"

// Captura: solo variables efectivamente final
int limite = 3;
// limite++;                                      // con esta línea, la lambda de abajo no compila
tareas.removeIf(t -> t.intentos() > limite);
```

- **En una línea:** `Predicate<Tarea> urgente = Tarea::esUrgente; tareas.stream().filter(urgente.and(Predicate.not(Tarea::estaHecha)))`
- **Por qué importa:** Componer funciones pequeñas en lugar de escribir lambdas largas hace el código más legible y reutilizable. Entender las clases anónimas ayuda a leer código antiguo y a saber qué captura una lambda.
- **Error típico:** Intentar modificar dentro de una lambda una variable local del método: solo puede capturar variables final o efectivamente final.
- **Java 17 frente a 21:** Igual en Java 17 y 21. Las jerarquías `sealed` que acompañan a `switch` se ven en el día de POO.
- **Se conecta con:** Clases anidadas, Interfaces funcionales, Comparator

### 4. Optional<T> como Contrato de Retorno, no de Atributo

Optional fue diseñado para métodos que pueden no encontrar un valor (e.g. `buscarPorId`). Nunca debe usarse como campo de clase ni como parámetro de métodos.

**Cómo funciona por dentro.**

`Optional<T>` es una caja que puede contener un valor o estar vacía. Su propósito declarado es servir como tipo de retorno de métodos que pueden no tener resultado, para que la firma obligue al llamador a pensar en el caso vacío. No está pensado para campos, parámetros ni colecciones (`List<Optional<T>>` no tiene sentido: usa una lista sin ese elemento).

La forma idiomática es encadenar en lugar de preguntar: `map` transforma si hay valor, `filter` lo descarta si no cumple, `flatMap` encadena con otro método que también devuelve `Optional`, y se termina con `orElse`, `orElseGet`, `orElseThrow` o `ifPresentOrElse`. Ojo con `orElse(algo())`: el argumento se evalúa siempre, aunque haya valor. Si el valor alternativo es caro, usa `orElseGet(() -> algo())`.

`get()` sin comprobar es tan peligroso como un `null`. Desde Java 10, `orElseThrow()` sin argumentos hace lo mismo pero con un nombre que deja clara la intención.

*Encadenar en lugar de comprobar*

```java
// Repositorio de Spring Data: findById ya devuelve Optional
public TareaDto obtener(long id) {
    return repo.findById(id)
        .map(TareaMapper::aDto)
        .orElseThrow(() -> new TareaNoEncontradaException(id));
}

public String emailDelResponsable(long tareaId) {
    return repo.findById(tareaId)
        .flatMap(Tarea::responsable)          // responsable() devuelve Optional<Usuario>
        .map(Usuario::email)
        .filter(e -> e.endsWith("@acme.com"))
        .orElse("sin-asignar@acme.com");
}

// orElse evalúa SIEMPRE su argumento
cache.buscar(clave).orElse(consultarBaseDeDatos(clave));        // consulta aunque haya caché
cache.buscar(clave).orElseGet(() -> consultarBaseDeDatos(clave)); // solo si está vacío
```

- **En una línea:** `return optional.map(Tarea::titulo).orElse("Sin título");`
- **Por qué importa:** Comunica explícitamente en la firma del método que el valor puede faltar, obligando a tratar la ausencia de forma segura.
- **Error típico:** Llamar a `.get()` sin comprobar si está presente, provocando exactamente el mismo fallo que un NullPointerException.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** NullPointerException, Clean Code, Manejo de Errores

### 5. Las 4 Interfaces Funcionales Fundamentales del JDK

`Predicate<T>` (T -> boolean), `Function<T, R>` (T -> R), `Consumer<T>` (T -> void) y `Supplier<T>` (() -> T).

**Cómo funciona por dentro.**

Una interfaz funcional tiene un único método abstracto, y por eso se puede implementar con una lambda o una referencia a método. El paquete `java.util.function` define las cuatro básicas: `Supplier<T>` (no recibe nada, devuelve `T`), `Consumer<T>` (recibe `T`, no devuelve nada), `Function<T, R>` (transforma `T` en `R`) y `Predicate<T>` (devuelve `boolean`).

A partir de ellas hay variantes de dos argumentos (`BiFunction`, `BiConsumer`, `BiPredicate`), de mismo tipo de entrada y salida (`UnaryOperator`, `BinaryOperator`) y especializaciones primitivas (`IntPredicate`, `ToLongFunction`) que evitan el autoboxing. Muchas traen métodos por defecto para componer: `Predicate.and`, `negate`, `Function.andThen`, `compose`.

Las referencias a método (`String::length`, `System.out::println`, `TareaDto::new`) son una forma más legible de escribir lambdas que solo llaman a un método existente.

*Las cuatro interfaces y cómo se componen*

```java
Supplier<Instant> ahora = Instant::now;
Consumer<String> log = System.out::println;
Function<Tarea, TareaDto> aDto = TareaMapper::aDto;
Predicate<Tarea> pendiente = t -> !t.estaCompletada();

// Composición
Predicate<Tarea> urgente = t -> t.prioridad() >= 8;
Predicate<Tarea> pendienteYUrgente = pendiente.and(urgente);
Function<String, String> limpiar = String::strip;
Function<String, Integer> longitudLimpia = limpiar.andThen(String::length);

// Especializaciones primitivas: sin autoboxing
IntPredicate esPar = n -> n % 2 == 0;
ToIntFunction<Tarea> prioridad = Tarea::prioridad;
int suma = tareas.stream().mapToInt(prioridad).sum();

// Tu propia interfaz funcional cuando el nombre aporta significado
@FunctionalInterface
interface ReglaDeValidacion<T> {
    Optional<String> validar(T valor);   // vacío si es válido, mensaje si no
}
```

- **En una línea:** `Predicate<String> esLargo = s -> s.length() > 10;`
- **Por qué importa:** Son la base de toda la Streams API, concurrencia (CompletableFuture) y librerías modernas de Java.
- **Error típico:** Crear interfaces funcionales propias innecesarias cuando ya existe una estándar en `java.util.function`.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Expresiones Lambda, Method References, Streams


## Sesión de 90 minutos

### 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?
- **Entregable:** Pipeline con filter, map y limit(1) demostrando que solo procesa los elementos mínimos necesarios.

### 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?
- **Entregable:** Jerarquía `EstadoPago` sellada evaluada con switch exhaustivo.

### 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())`?
- **Entregable:** Refactorización de código imperativo con nulos a Optional funcional limpio.

### 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.
- **Entregable:** Checklist de katas del día registradas en la base de datos local.


## Explícalo desde cero

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

- **Qué 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.
- **Qué suele explicarse mal:** Pensar que los Streams son colecciones. Un Stream no almacena nada; es una tubería por la que circulan datos.
- **Ejemplo sencillo:** 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).
- **Señal de alerta:** Si intentas depurar un stream poniendo un `peek()` sin poner una operación terminal y te preguntas por qué no imprime nada.


## Preguntas

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

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.

**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.

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

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.

**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.

### 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?

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`.

**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.


## Reto del tutor

**Reto:** 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 está bien:** Comprueba la presencia con isPresent() antes de llamar a get(), evitando NoSuchElementException.
- **Respuesta superficial:** Decir que "está bien porque no da error".
- **Lo que falta de fondo:** 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.
- **Respuesta senior:** 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");`.


## Katas

### Kata 6.1: Procesamiento complejo de ventas con Streams

Dado un `record Venta(String cliente, double monto, String categoria)`, filtra las ventas mayores a 50€, agrúpalas por categoría y calcula la suma total por categoría usando `Collectors.groupingBy` y `Collectors.summingDouble`.


## 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ística | Java 17 | Java 21 |
| --- | --- | --- |
| 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 |


## Proyecto guiado «Pedidos»: Informes de ventas con streams

Responder preguntas de negocio sobre los pedidos sin bucles manuales.

**Tareas:**

- Calcula el total vendido por producto con `groupingBy` y `reducing`.
- Obtén los cinco clientes que más han gastado.
- Cuenta los pedidos por estado y devuelve el resultado en un `EnumMap`.
- 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.


## Fuentes oficiales

- [Javadoc de java.util.stream](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/package-summary.html)
- [Javadoc de Optional](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Optional.html)
- [JEP 441: Pattern Matching for switch](https://openjdk.org/jeps/441)
- [JEP 440: Record Patterns](https://openjdk.org/jeps/440)

