Ejercicios: Programación Funcional y Streams
Proyecto Temático: StreamFlix Analytics Engine
¡Atención equipo de desarrollo!
Habéis sido reclutados por StreamFlix, la plataforma de streaming de vídeo de más rápido crecimiento (ficticia, por supuesto). Nuestro backend está sufriendo. El código heredado está lleno de bucles for anidados que ralentizan el análisis de datos de nuestros millones de usuarios.
Vuestra misión es modernizar el núcleo de análisis de datos ("Analytics Engine"). Necesitamos procesar catálogos de películas, logs de visualizaciones y preferencias de usuarios de forma eficiente, paralela y limpia. Vamos a sustituir el viejo código "spaghetti" por pipelines de Streams elegantes y robustas. ¡Manos a la obra!
Ejercicios de Consolidación
1) El Bug del "Stream Fantasma" | Nivel: Fácil
Objetivo
Identificar errores comunes en el ciclo de vida de un Stream (operaciones terminales y evaluación perezosa).
Setup Inicial
Un becario ha intentado filtrar las películas para adultos (clasificación "+18") para el modo "Kids", pero el sistema reporta que no se está filtrando nada o el código no hace nada.
import java.util.List;
import java.util.stream.Stream;
public class DebugStream {
public static void main(String[] args) {
List<String> peliculas = List.of("Toy Story", "Saw (+18)", "Frozen", "Deadpool (+18)");
System.out.println("Iniciando filtrado...");
// CÓDIGO CON BUG
Stream<String> streamFiltrado = peliculas.stream()
.filter(p -> {
System.out.println("Filtrando: " + p);
return !p.contains("(+18)");
});
System.out.println("Filtrado terminado.");
}
}
Tarea a realizar
- Ejecuta mentalmente (o en tu IDE) el código. ¿Se imprime "Filtrando: Toy Story"? ¿Por qué?
- Corrige el código para que realmente se ejecute el filtrado e imprima la lista resultante segura para niños.
Aplicación en el Mundo Real
En entornos de producción, un error común es definir complejas tuberías de procesamiento de datos que nunca se ejecutan porque falta la "chispa" de la operación terminal, consumiendo memoria sin producir resultados.
2) Refactorización: Del Bucle a la Lambda | Nivel: Fácil
Objetivo
Transformar código imperativo tradicional en código funcional declarativo usando forEach y Lambdas.
Setup Inicial
Tenemos una lista de usuarios a los que hay que enviar un correo de "Bienvenida a StreamFlix".
List<String> nuevosUsuarios = List.of("ana@gmail.com", "beto@hotmail.com", "carla@outlook.com");
// Código Imperativo (Legacy)
for (String email : nuevosUsuarios) {
System.out.println("Enviando email a: " + email);
}
Tarea a realizar
- Reescribe el bucle anterior utilizando el método
forEachde la interfazIterable(disponible en la lista). - Utiliza primero una expresión lambda estándar.
- Refactorízalo después para usar una Referencia a Método (
::).
Aplicación en el Mundo Real
Reducir la verbosidad de los bucles simples mejora la legibilidad. Las referencias a métodos son muy comunes en frameworks como Spring Boot o en callbacks de interfaces gráficas.
3) Pipeline Básica: Filtrado y Conteo | Nivel: Fácil
Objetivo
Crear un Stream desde cero, aplicar un predicado (filter) y obtener un resultado escalar (count).
Setup Inicial
Se te proporciona un array de duraciones de visualización (en minutos) de una sesión de usuario.
int[] visualizaciones = {120, 45, 15, 95, 10, 200, 5};
Tarea a realizar
StreamFlix considera una "visualización efectiva" aquella que dura más de 30 minutos.
1. Convierte el array primitivo en un Stream.
2. Filtra las visualizaciones que no sean efectivas.
3. Cuenta cuántas visualizaciones efectivas hubo.
Aplicación en el Mundo Real
El cálculo de KPIs (Key Performance Indicators) como "usuarios activos" o "retención" se basa enteramente en filtrar logs masivos de eventos bajo ciertos criterios.
4) Transformación de Datos: Mapeando el Catálogo | Nivel: Fácil
Objetivo
Usar map para transformar objetos complejos en datos simples necesarios para el frontend.
Setup Inicial
Usa el siguiente record (introducido en Java 16, estándar en Java 21) para representar una película.
record Pelicula(String titulo, String director, int anio) {}
List<Pelicula> catalogo = List.of(
new Pelicula("Inception", "Nolan", 2010),
new Pelicula("Avatar", "Cameron", 2009),
new Pelicula("Interstellar", "Nolan", 2014)
);
Tarea a realizar
El equipo de Marketing necesita una lista simple de Strings con el formato: "TITULO (Año)".
1. Crea un Stream a partir del catálogo.
2. Usa map para transformar cada objeto Pelicula en un String con el formato requerido (título en mayúsculas).
3. Recolecta el resultado en una nueva lista.
Aplicación en el Mundo Real
Patrón DTO (Data Transfer Object): A menudo la base de datos devuelve objetos con 50 campos, pero la API REST solo debe enviar 3 al frontend. map es la herramienta perfecta para esta conversión.
5) Seguridad ante Nulls: El Buscador Robusto | Nivel: Fácil
Objetivo
Utilizar Optional para manejar búsquedas que pueden no dar resultados, evitando NullPointerException.
Setup Inicial
Un método simulado busca subtítulos en una base de datos externa.
import java.util.Optional;
import java.util.Random;
public class SubtitleService {
// Simula una búsqueda que a veces falla
public static Optional<String> buscarSubtitulo(String idioma) {
if ("ES".equals(idioma)) {
return Optional.of("subtitulos_es.srt");
} else {
return Optional.empty();
}
}
}
Tarea a realizar
- Llama a
buscarSubtitulo("EN")(inglés). - Usa
orElsepara obtener "default.srt" si no se encuentra el subtítulo. - Llama a
buscarSubtitulo("ES")y usaifPresentpara imprimir "Archivo encontrado: ..." solo si existe. - Intenta usar
orElseThrowsimulando una búsqueda crítica que no puede fallar (ej: idioma del sistema).
Aplicación en el Mundo Real
Evitar el "billion dollar mistake" (referencias nulas) es crucial en sistemas distribuidos donde los servicios pueden fallar o no devolver datos.
Ejercicios de Refuerzo
6) El Top 3: Ordenación y Límites | Nivel: Medio
Objetivo
Dominar sorted con comparadores personalizados y limit para paginación o rankings.
Setup Inicial
record Serie(String titulo, double rating) {}
List<Serie> series = List.of(
new Serie("Breaking Bad", 9.5),
new Serie("Friends", 8.9),
new Serie("Game of Thrones", 9.3),
new Serie("The Office", 9.0),
new Serie("Chernobyl", 9.4)
);
Tarea a realizar
StreamFlix quiere mostrar en la portada "Las 3 series mejor valoradas".
1. Ordena las series por rating de forma descendente (de mayor a menor). Pista: Usa Comparator.comparingDouble(...).reversed().
2. Limita el resultado a 3 elementos.
3. Mapea el resultado para obtener solo los nombres de las series.
Aplicación en el Mundo Real
Generar rankings ("Top Ventas", "Lo más visto") es una de las consultas más frecuentes en cualquier base de datos o sistema de caché.
7) Aplanando Géneros: flatMap al rescate | Nivel: Medio
Objetivo
Entender flatMap para procesar estructuras anidadas (listas dentro de listas) y distinct para eliminar duplicados.
Setup Inicial
Cada película tiene múltiples géneros.
record Pelicula(String titulo, List<String> generos) {}
List<Pelicula> biblioteca = List.of(
new Pelicula("Matrix", List.of("Ciencia Ficción", "Acción")),
new Pelicula("John Wick", List.of("Acción", "Thriller")),
new Pelicula("Interstellar", List.of("Ciencia Ficción", "Drama"))
);
Tarea a realizar
Queremos obtener una lista de todos los géneros únicos disponibles en la biblioteca para llenar el filtro de búsqueda del menú principal.
1. Si usas map, obtendrás una lista de listas (Stream<List<String>>). Eso no nos sirve.
2. Usa flatMap para convertir cada lista de géneros en un flujo de strings individuales.
3. Elimina los duplicados.
4. Ordena alfabéticamente el resultado.
Aplicación en el Mundo Real
Procesamiento de etiquetas (tags), categorías de productos o permisos de usuarios donde un usuario tiene múltiples roles y queremos la lista plana de todos los permisos posibles.
8) Estadísticas de Visualización: IntStream | Nivel: Medio
Objetivo
Trabajar con streams primitivos (IntStream) para obtener estadísticas matemáticas (suma, promedio, max, min) de forma eficiente.
Setup Inicial
Imagina una lista de objetos Sesion que contiene los minutos vistos por un usuario en la última semana.
record Sesion(int minutos) {}
List<Sesion> historial = List.of(
new Sesion(15), new Sesion(120), new Sesion(45), new Sesion(90), new Sesion(15)
);
Tarea a realizar
El departamento de Data Science quiere un resumen rápido.
1. Mapea el stream a un IntStream usando mapToInt.
2. Utiliza el método summaryStatistics() para obtener de una sola vez: el total de minutos, el promedio, el máximo y el mínimo.
3. Imprime los resultados.
Aplicación en el Mundo Real
En Big Data, calcular estadísticas básicas sobre millones de registros numéricos es el primer paso de cualquier análisis exploratorio. IntStream evita el coste de "boxing/unboxing" (convertir int a Integer), siendo mucho más rápido y eficiente en memoria.
9) El Algoritmo de Búsqueda: Matching | Nivel: Medio
Objetivo
Usar operaciones de cortocircuito (anyMatch, allMatch) para lógica de validación rápida.
Setup Inicial
record Contenido(String titulo, boolean esOriginal, boolean tieneSubtitulos) {}
List<Contenido> parrilla = List.of(
new Contenido("Stranger Things", true, true),
new Contenido("Casa de Papel", true, true),
new Contenido("Pelicula Indie Antigua", false, false)
);
Tarea a realizar
Implementa las siguientes validaciones para el panel de control de calidad:
1. ¿Hay algún contenido que no tenga subtítulos? (anyMatch). Si es así, imprimir una alerta "Alerta: Contenido sin accesibilidad detectado".
2. ¿Son todos los contenidos originales de StreamFlix? (allMatch).
3. ¿Ningún contenido es original? (noneMatch).
Aplicación en el Mundo Real
Validaciones de reglas de negocio antes de procesar lotes de datos (ej: "Asegurarse de que ninguna transacción sea negativa antes de procesar la nómina").
Ejercicios de Ampliación
10) El Informe Ejecutivo: Agrupación Avanzada | Nivel: Alto
Objetivo
Dominar Collectors.groupingBy para crear mapas complejos y agregaciones por grupos.
Setup Inicial
record Visualizacion(String usuario, String genero, int minutos) {}
List<Visualizacion> logs = List.of(
new Visualizacion("User1", "Accion", 120),
new Visualizacion("User2", "Comedia", 50),
new Visualizacion("User1", "Comedia", 30),
new Visualizacion("User3", "Accion", 100),
new Visualizacion("User2", "Accion", 200)
);
Tarea a realizar
Genera un informe que agrupe los datos por Género.
1. Nivel 1: Obtén un Map<String, List<Visualizacion>> donde la clave es el género.
2. Nivel 2 (Avanzado): No queremos la lista de objetos, queremos la suma total de minutos vistos por género. El resultado debe ser Map<String, Integer>.
* Pista: Usa la sobrecarga de groupingBy(clasificador, collector). El segundo colector debe ser summingInt.
Aplicación en el Mundo Real
Este es el núcleo de las consultas "GROUP BY" en SQL. Hacerlo en memoria con Java permite procesar datos provenientes de múltiples fuentes (APIs, archivos) que no están en una base de datos relacional.
11) Motor de Recomendación: Pipeline Compleja | Nivel: Alto
Objetivo
Encadenar múltiples operaciones (filtrado, mapeo, ordenación, colección) para implementar una lógica de negocio no trivial.
Setup Inicial
Tenemos una lista de candidatos a películas para recomendar a un usuario específico.
record Peli(String titulo, String genero, double rating, boolean vistaPreviamente) {}
List<Peli> candidatos = List.of(
new Peli("Matrix", "Sci-Fi", 9.0, true),
new Peli("Dune", "Sci-Fi", 9.2, false),
new Peli("Minions", "Infantil", 6.5, false),
new Peli("Blade Runner", "Sci-Fi", 8.8, false),
new Peli("Sharknado", "Sci-Fi", 3.0, false)
);
Tarea a realizar
Implementa el algoritmo "Sci-Fi Lovers":
1. Filtra las películas que el usuario ya ha visto (no recomendar repetidas).
2. Filtra solo las del género "Sci-Fi".
3. Filtra las que tengan un rating inferior a 7.0 (queremos calidad).
4. Ordena las restantes por rating (mejor a peor).
5. Obtén solo los títulos.
6. Devuelve el primer resultado (findFirst) o "No hay recomendaciones" si la lista está vacía.
Aplicación en el Mundo Real
Los algoritmos de recomendación de Netflix, Amazon o Spotify funcionan así: una serie de filtros en embudo para reducir millones de ítems a una lista pequeña y relevante.
12) Reto de Refactorización y Peer Review | Nivel: Alto
Objetivo
Analizar código legado (imperativo, mutable y complejo), refactorizarlo a funcional y realizar una revisión crítica.
Tarea a realizar
Parte 1: Análisis Tienes el siguiente método legado que procesa transacciones de pago de suscripciones. Analízalo: ¿Qué hace? ¿Es fácil de leer? ¿Es seguro para multihilo (fíjate en las variables externas)?
// Código Legado horrible
public List<String> procesarPagos(List<Pago> pagos) {
List<String> recibos = new ArrayList<>();
double total = 0; // Estado mutable externo! Peligroso en paralelo
for (Pago p : pagos) {
if (p.getEstado().equals("APROBADO")) {
if (p.getMonto() > 10) {
double impuesto = p.getMonto() * 0.21;
total += p.getMonto(); // Efecto secundario
recibos.add("Recibo-" + p.getId() + ": " + (p.getMonto() + impuesto));
}
}
}
System.out.println("Total procesado: " + total); // Side effect
return recibos;
}
Parte 2: Refactorización (Tu Misión)
Reescribe este método usando Streams.
* No debes usar variables externas para el total (el cálculo del total debería ser una operación separada o usar una reducción compleja, para este ejercicio, céntrate en devolver la lista de recibos).
* Usa filter, map y collect/toList.
Parte 3: Peer Review (Simulado) Imagina que eres el "Senior Developer". Escribe 3 comentarios que le harías al autor del código original explicando por qué la versión Stream es mejor (habla de inmutabilidad, testabilidad y paralelismo).
Aplicación en el Mundo Real
El mantenimiento de software consume el 80% del presupuesto de IT. Convertir código imperativo propenso a errores en pipelines funcionales reduce la deuda técnica.