Skip to content

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
  1. Ejecuta mentalmente (o en tu IDE) el código. ¿Se imprime "Filtrando: Toy Story"? ¿Por qué?
  2. 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
  1. Reescribe el bucle anterior utilizando el método forEach de la interfaz Iterable (disponible en la lista).
  2. Utiliza primero una expresión lambda estándar.
  3. 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
  1. Llama a buscarSubtitulo("EN") (inglés).
  2. Usa orElse para obtener "default.srt" si no se encuentra el subtítulo.
  3. Llama a buscarSubtitulo("ES") y usa ifPresent para imprimir "Archivo encontrado: ..." solo si existe.
  4. Intenta usar orElseThrow simulando 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.