Escudo Real Oviedo La Pizarra de Almada
Java 8+ • Primera División Edition

LIBRO DE TÁCTICA:
JAVA STREAMS & OPTIONAL

"Ganar, ganar y volver a ganar: El Manual de Guille Almada para Java 8+"

Java Version License Build

🌟 LA PIZARRA DE LAS ESTRELLAS 🌟

🎖️ ELIMINATORIAS: DOMINAR EL TARTIERE

campaign

1. Charla Técnica: Salir a Ganar

El Plan de Juego

1.1. El Plan de Juego (Objetivo del manual)

Escuchadme bien, equipo. Miradme a los ojos. El objetivo de este manual no es que aprendáis a "picar código" de cualquier manera. El objetivo es que aprendáis a competir. Aquí vamos a presentar, con la pizarra clara y el cuchillo entre los dientes, cómo se usa la API de Streams en Java. No es solo una librería; es nuestra nueva formación táctica para dominar la programación funcional.

A lo largo de estas páginas, vamos a analizar a nuestros jugadores estrella: los Streams y la clase Optional. No nos vamos a quedar en la teoría; vamos a bajar al barro con ejemplos prácticos, jugadas ensayadas (casos de uso) y las leyes del vestuario (buenas prácticas) que separan a los campeones de los que se quedan en el banquillo. Aquí buscamos legibilidad, limpieza y, sobre todo, calidad en el juego.

Este manual va dirigido a los canteranos: estudiantes que ya saben darle patadas al balón (conocimientos básicos de Java) pero que quieren dar el salto al primer equipo y dominar el enfoque funcional que cambió nuestra historia a partir de Java 8.

"Porque aquí, programadores, el esfuerzo no se negocia. Partido a partido. Stream a stream."

1.2. El Sistema de Juego: Del "Patadón" al Fútbol Moderno

Escuchad bien, porque aquí es donde se decide el partido. La programación funcional es nuestra nueva filosofía de juego. Se basa en un movimiento fluido del balón (las funciones) y en algo sagrado: evitar los cambios de estado y los efectos secundarios.

En nuestro equipo, no queremos jugadores que se vuelvan locos y dejen su posición desprotegida (cambios de estado impredecibles). Queremos que, si un balón entra en una jugada, salga transformado en gol, sin que el resto del campo se convierta en un caos.

Tradicionalmente, Java jugaba al "patadón y tentetieso": la Programación Imperativa. Cada jugador era un objeto pesado que cargaba con toda la responsabilidad, y el entrenador tenía que gritar paso a paso cada movimiento. Era un fútbol lento, de mucho contacto y código farragoso.

Pero a partir de Java 8, hemos fichado el talento. El lenguaje ha incorporado elementos del fútbol total:

  • Expresiones Lambda: Instrucciones rápidas, como un gesto desde la banda. Menos palabras, más acción.
  • Interfaces Funcionales: El contrato que define el rol de cada jugador en una jugada específica.

2. Conceptos Tácticos: Del Barro al Césped

2.1. El Cómo contra el Qué (Imperativo vs. Funcional)

2.1.1 El Fútbol de "La Vieja Guardia"

La programación imperativa es el fútbol de antes, el de los bigotes y el barro. Se basa en darle al jugador una secuencia de instrucciones machaconas: "Corre 10 metros, frena, mira a la banda, pon el centro con la pierna izquierda".

2.1.2 El Fútbol Total

La programación funcional es nuestra pizarra moderna. Aquí no nos obsesionamos con el movimiento de cada músculo, sino con la misión. Priorizamos las funciones como el corazón del equipo.

TÁCTICA IMPERATIVA TÁCTICA FUNCIONAL
Instrucciones paso a paso: Cómo hay que jugar. Objetivo claro: Qué resultado queremos obtener.
Usa bucles y marcas al hombre: Control explícito. Usa funciones y toques sutiles: Transforma los datos.
Estado variable: El marcador y los jugadores cambian. Inmutabilidad: Evitamos líos y efectos secundarios.
Código largo: Mucho sudor y repetición. Código conciso: Calidad y limpieza en el pase.

El Equilibrio del Míster

En este equipo, ambos sistemas conviven. La programación imperativa sigue siendo nuestra defensa central para la lógica general. Pero cuando el balón llega al centro del campo y hay que manejar colecciones, la programación funcional es nuestra delantera estrella. Partido a partido, función a función.

2.2. Clases Anónimas: Los "Temporeros" del Código

En el fútbol, a veces necesitas a un jugador para una misión muy específica: tirar un penalti concreto o cubrir una baja solo durante cinco minutos. No le haces un contrato de cinco años, ni le pones su nombre en la marquesina del estadio. Simplemente lo sacas al campo, hace su trabajo y se va.

Jugada.java
public interface Jugada {
    void ejecutar();
}
Entrenamiento.java
Jugada faltaDirecta = new Jugada() {
    @Override
    public void ejecutar() {
        System.out.println("Balón por encima de la barrera y a la escuadra.");
    }
};

2.3. Expresiones Lambda: El Grito desde la Banda

Escuchadme bien: en el fútbol moderno no hay tiempo para redactar un contrato cada vez que quieres que un jugador presione. Necesitamos instrucciones eléctricas. Las expresiones lambda son precisamente eso: una forma de definir una jugada (un bloque de código) de manera fulminante, sin tener que escribir toda la burocracia de un método tradicional.

Clase Anónima

"Yo, jugador, prometo por la presente golpear el esférico..." (Demasiado texto)

Lambda

(balón) -> chutar(); (Corto, directo, efectivo)

📋 Anatomía de la Jugada (Sintaxis)

1. Sin implicados (Sin parámetros):

() -> System.out.println("¡Pita el árbitro!");

2. Con un solo protagonista (Un parámetro):

jugador -> jugador.correr();

3. Jugada combinada (Más de un parámetro):

(pase, remate) -> pase + remate;

4. Plan táctico completo (Más de una sentencia):

(balón) -> {
    mirarPorteria();
    armarPierna();
    return marcarGol();
};

2.4. Interfaces Funcionales: Los Especialistas de la Plantilla

En el sistema de Almada, la especialización es sagrada. Una interfaz funcional es exactamente eso: un contrato que define una sola tarea específica (un único método abstracto).

El "Equipo de Gala" (Interfaces Predefinidas)

Predicate<T>

El Ojeador / El Árbitro

Analizar a un jugador y decir true o false

jugador -> jugador.getEdad() < 20
Function<T, R>

El Enlace / El Creador

Recibir un balón y devolver una asistencia

crack -> crack.getSueldo()
Consumer<T>

El Rematador / El Finalizador

Recibir el balón y terminar la jugada

titular -> System.out.println(titular)
Supplier<T>

El Canterano / El Utillero

No recibe nada, pero siempre tiene un balón nuevo

() -> new Jugador("Canterano")
Arenga.java
@FunctionalInterface
public interface Arenga {
    void darGrito(String mensaje);
}

// Uso con Lambda
Arenga almada = mensaje -> System.out.println("ALMADA GRITA: " + mensaje);
almada.darGrito("¡PARTIDO A PARTIDO!");

3. La API de Streams: El Balón en Movimiento

3.1. ¿Qué es un Stream? (El Flujo de Juego)

Escuchadme bien: la API Stream de Java es nuestra nueva forma de jugar al fútbol total. Es la herramienta para manejar plantillas de datos enormes de forma clara, ordenada y, sobre todo, con una eficiencia que asfixia al rival. Olvidaos de los bucles for tradicionales; eso es fútbol de los años 50, correr por correr.

Operaciones Intermedias

Son los pases en el centro del campo. Preparan la jugada (filtrar, ordenar, transformar).

Operación Terminal

¡Es el pitido final o el gol! Sin esta operación, la jugada no sirve de nada.

⚽ CONCEPTO CLAVE

Un Stream NO es una colección. No guarda a los jugadores, no es un almacén. Es el flujo de los elementos mientras se procesan. Solo existen mientras el balón está en movimiento.

4. Operaciones Intermedias: El Trabajo en el Centro del Campo

4.1. .filter() - El Filtro del Antidoping

Solo pasan los que cumplen la condición. Los demás, al banquillo.

List<String> plantilla = Arrays.asList("Cazorla", "Costas", "Agudín", "Viñas");

plantilla.stream()
    .filter(jugador -> jugador.length() > 6)
    .forEach(System.out::println);
// Resultado: Cazorla, Agudín

4.2. .map() - El Cambio de Posición

Transformamos a los jugadores. Entra un dato, sale otro distinto.

plantilla.stream()
    .map(String::toUpperCase)
    .forEach(System.out::println);
// Resultado: CAZORLA, COSTAS, AGUDÍN, VIÑAS

4.3. .sorted() - La Tabla de Clasificación

Ordenamos la fila antes de salir al campo.

plantilla.stream()
    .sorted()
    .forEach(System.out::println);
// Resultado: Agudín, Cazorla, Costas, Viñas

4.4. .distinct() - Sin Cromos Repetidos

Aquí no queremos duplicados. Solo valores únicos.

List<String> cromos = Arrays.asList("Cazorla", "Cazorla", "Agudín");

cromos.stream()
    .distinct()
    .forEach(System.out::println);
// Resultado: Cazorla, Agudín

4.5. .limit() y .skip() - El 11 Inicial y los Descartes

Controlamos quién entra en el flujo.

List<String> cantera = Arrays.asList("J1", "J2", "J3", "J4", "J5");

cantera.stream()
    .skip(2)
    .limit(2)
    .forEach(System.out::println);
// Resultado: J3, J4

4.6. .flatMap() - El Repechaje de Cedidos

Imagina que tienes varias listas: los cedidos en el Ibiza, los del filial y los del primer equipo. Si usas map(), te sale una lista de listas (un desastre administrativo). Con flatMap(), unificas a todos en una sola convocatoria. Aplanas la jerarquía.

List<List<String>> grupos = Arrays.asList(
    Arrays.asList("Cazorla", "Costas"),      // Veteranos
    Arrays.asList("Hassan", "Vidal"),        // Offensive
    Arrays.asList("Brugman", "Dubravka")     // Refuerzos
);

// Sin flatMap: Lista dentro de Lista. Un lío.
// Con flatMap: Una sola familia, el Real Oviedo unido.

grupos.stream()
    .flatMap(grupo -> grupo.stream()) // Desempaquetamos cada sublist en el flujo general
    .forEach(System.out::println);
// Resultado: Cazorla, Costas, Hassan, Vidal, Brugman, Dubravka...

4.7. .peek() - El Ojo del VAR

Esta operación es para el analista táctico. Sirve para espiar qué pasa en el flujo sin parar el partido. Es perfecta para depurar y ver por qué se filtra un jugador que no debía. No modifica el balón, solo lo observa.

List<String> rivales = Arrays.asList("Sporting", "Gijón", "Sporting");

rivales.stream()
    .filter(rival -> rival.length() > 6)
    .peek(rival -> System.out.println("VAR revisando a: " + rival)) // Espiamos
    .map(String::toUpperCase)
    .forEach(System.out::println);

5. Operaciones Terminales: El Remate a Puerta

Aquí se acaba la charla técnica. Las operaciones terminales disparan la acción. Una vez ejecutadas, el Stream se agota: el balón sale fuera de banda y el partido termina.

5.1. .forEach()

Instrucciones Individuales

plantilla.stream()
  .forEach(jugador -> println(jugador));

5.2. .collect()

El Autobús del Equipo

List<String> convocados = 
  plantilla.stream()
  .collect(Collectors.toList());

5.3. .reduce()

El Marcador Final

int total = goles.stream()
  .reduce(0, Integer::sum);

5.4. .count()

El Acta del Árbitro

long num = plantilla.stream()
  .count();

5.5. Matchers (VAR)

anyMatch, allMatch, noneMatch

boolean hay = plantilla.stream()
  .anyMatch(jugador -> jugador.equals("Cazorla"));

5.6. .findFirst()

El MVP (devuelve Optional)

Optional<String> primero = 
  plantilla.stream().findFirst();

5.7. .collect(Collectors.groupingBy()) - Los Vestuarios

El partido ha terminado y toca clasificar. No queremos una lista interminable, queremos un Mapa donde la clave sea la posición y el valor la lista de jugadores. Esto organiza el vestuario: Defensas a la izquierda, Delanteros a la derecha.

class JugadorCarbayón {
    String nombre;
    String posicion; // DEF, MED, DEL
    public JugadorCarbayón(String n, String p) { nombre = n; posicion = p; }
    public String getPosicion() { return posicion; }
    public String getNombre() { return nombre; }
}

List<JugadorCarbayón> plantilla = Arrays.asList(
    new JugadorCarbayón("Costas", "DEF"),
    new JugadorCarbayón("Cazorla", "MED"),
    new JugadorCarbayón("Hassan", "DEL"),
    new JugadorCarbayón("Vidal", "DEL")
);

// Agrupamos por posición. ¡Cada uno a su taquilla!
Map<String, List<JugadorCarbayón>> vestuarios = plantilla.stream()
    .collect(Collectors.groupingBy(JugadorCarbayón::getPosicion));

// Si pedimos vestuarios.get("DEL"), tenemos a Hassan y a Vidal.
System.out.println("Delanteros: " + vestuarios.get("DEL"));

5.8. .collect(Collectors.joining()) - La Pantalla Gigante

Tienes la lista de goleadores y quieres ponerla en el marcador del Carlos Tartiere separada por comas. joining concatena strings sin bucles feos.

List<String> goleadores = Arrays.asList("Hassan", "Cazorla", "Costas");

String marcador = goleadores.stream()
    .collect(Collectors.joining(" ⚽ ", "Goles del Oviedo: ", " ¡Gloria!"));

// Resultado: "Goles del Oviedo: Hassan ⚽ Cazorla ⚽ Costas ¡Gloria!"

6. La Clase Optional: El Seguro de Vida del Míster

6.1. El Fantasma de los Valores Nulos

Escuchadme bien: el error más estúpido en el fútbol es dar un pase al hueco esperando que esté tu delantero y descubrir que se ha quedado en el vestuario. En Java, eso se llama NullPointerException (NPE).

⚠️ EL ENEMIGO NÚMERO 1

Cuando intentas llamar a un método de un objeto que es null, el programa "peta", el partido se suspende y nos vamos a casa con cara de tontos.

6.2. ¿Qué es el Optional? El Informe Médico

El Optional es una clase que introdujo Java 8 y funciona como un contenedor, una caja. En lugar de pasarte al jugador y rezar para que no sea null, te paso una "caja de disponibilidad".

Contiene un valor

El jugador está listo para jugar.

Está vacío

El jugador está en la enfermería.

6.3. Creación de Objetos Optional (Fichajes)

1. Optional.of(jugador)

El fichaje estrella. Estás 100% seguro de que está ahí. Si es null, explota.

Optional<String> estrella = Optional.of("Cazorla");

2. Optional.ofNullable(jugador) ⭐

El jugador es duda. Si está, bien; si es null, la caja queda vacía. LA MÁS SEGURA.

Optional<String> quizas = Optional.ofNullable(duda);

3. Optional.empty()

El puesto está vacante. Creamos una caja vacía a propósito.

Optional<String> vacante = Optional.empty();

6.4. Métodos Principales: El Plan de Emergencia

A. Comprobar disponibilidad

isPresent()

if (delantero.isPresent()) { ... }

isEmpty()

if (delantero.isEmpty()) { ... }

B. El Plan B (orElse y orElseGet)

Si el titular no está, sacamos al reserva.

Optional<String> delantero = mercado.buscar("Viñas");

// Si no está Viñas, juega el "Canterano"
String titular = delantero.orElse("Canterano");

// orElseGet (Lazy): Solo se busca si Viñas falla
String titularPro = delantero.orElseGet(() -> "Canterano de la cantera");

C. El "Todo o Nada" (orElseThrow)

Si el jugador no está y no tenemos reserva, se acaba el partido.

String portero = listaPorteros.findAny()
    .orElseThrow(() -> new RuntimeException("¡No tenemos portero!"));

D. La Jugada Maestra (ifPresentOrElse)

Controlamos los dos escenarios de forma limpia.

delantero.ifPresentOrElse(
    jugador -> System.out.println("¡Gol de " + jugador + "!"),
    () -> System.out.println("Nadie remató el centro...")
);

🏆 SESIÓN DE ENTRENAMIENTO: DEL BARRO AL GOL

Ejercicio Completo 1: "La Convocatoria de Primera"

Objetivo: Filtrar a los jugadores que están en forma, ordenarlos por calidad y elegir a nuestros tres capitanes.

Escenario: Tienes una lista de jugadores con su nombre, nivel de energía (0-100) y su puntuación de "coraje" (0-10). Solo pueden ir convocados los que tengan más de 70 de energía.

ConvocatoriaPrimera.java
import java.util.*;
import java.util.stream.Collectors;

class Jugador {
    String nombre;
    int energia;
    int coraje;

    Jugador(String n, int e, int c) { nombre = n; energia = e; coraje = c; }
    public int getCoraje() { return coraje; }
    public String getNombre() { return nombre; }
    public int getEnergia() { return energia; }
}

public class ChampionsLeague {
    public static void main(String[] args) {
        List<Jugador> plantilla = Arrays.asList(
            new Jugador("Cazorla", 85, 10),
            new Jugador("Hassan", 90, 9),
            new Jugador("Viñas", 60, 8),
            new Jugador("Escandell", 95, 10),
            new Jugador("Vidal", 75, 7)
        );

        List<String> convocados = plantilla.stream()
            .filter(jugador -> jugador.getEnergia() > 70)
            .sorted(Comparator.comparing(Jugador::getCoraje).reversed())
            .limit(3)
            .map(Jugador::getNombre)
            .collect(Collectors.toList());

        System.out.println("Nuestros tres guerreros: " + convocados);
    }
}

Ejercicio Completo 2: "El Fichaje de Invierno con Optional"

Objetivo: Buscar un fichaje en el mercado y, si no lo encontramos, tirar de la cantera.

Escenario: Buscamos a un delantero que cueste menos de 40 millones. Si el mercado nos devuelve un null, Optional debe salvarnos la vida.

MercadoFichajes.java
import java.util.Optional;

public class MercadoFichajes {
    public static void main(String[] args) {

        // Simulamos una búsqueda que podría no devolver nada
        String fichajeBuscado = null;

        // EL PLAN DE EMERGENCIA DEL MÍSTER
        String delanteroFinal = Optional.ofNullable(fichajeBuscado)
            .filter(nombre -> nombre.equals("Lewandowski"))
            .map(String::toUpperCase)
            .orElse("CANTERANO DE LA CASA");

        System.out.println("Delantero elegido: " + delanteroFinal);

        // JUGADA CON VAR
        Optional.ofNullable(fichajeBuscado)
            .ifPresentOrElse(
                fichaje -> System.out.println("¡Habemus crack!"),
                () -> System.out.println("Tiramos de cantera.")
            );

    }
}

Ejercicio Completo 3: "El Once Ideal por Estadísticas (Grouping + FlatMap)"

Objetivo: Unificar titulares y suplentes, quedarnos con los más goleadores y analizar dónde tenemos más pólvora.

Escenario: Aplanamos varias listas con flatMap, filtramos a los que tienen más de 5 goles y agrupamos por demarcación para ver qué línea es la más letal.

TacticaAlmada.java
import java.util.*;
import java.util.stream.*;

class Crack {
    String nombre;
    String demarcacion; // MED, DEL
    int goles;

    public Crack(String n, String d, int g) { nombre = n; demarcacion = d; goles = g; }
    public String getDemarcacion() { return demarcacion; }
    public int getGoles() { return goles; }
    public String getNombre() { return nombre; }
}

public class TacticaAlmada {
    public static void main(String[] args) {

        List<Crack> titulares = Arrays.asList(
            new Crack("Cazorla", "MED", 8),
            new Crack("Hassan", "DEL", 12)
        );

        List<Crack> suplentes = Arrays.asList(
            new Crack("Ilic", "MED", 2),
            new Crack("Sibo", "DEL", 6)
        );

        // PASO 1: Juntamos todas las listas
        // PASO 2: Filtramos los que meten más de 5 goles
        // PASO 3: Agrupamos por demarcación

        Map<String, List<String>> armasPorPosicion =
            Stream.of(titulares, suplentes)
                .flatMap(List::stream)
                .filter(j -> j.getGoles() > 5)
                .collect(Collectors.groupingBy(
                    Crack::getDemarcacion,
                    Collectors.mapping(Crack::getNombre, Collectors.toList())
                ));

        System.out.println("Nuestras armas ofensivas: " + armasPorPosicion);

    }
}

🏃‍♂️ LOS 15 NIVELES DEL TARTIERE

Nivel Canterano (Básicos)

  1. 1. El Saludo: Dada una lista de nombres de jugadores, imprímelos todos usando .forEach().
  2. 2. El Antidoping: Dada una lista de edades, filtra solo las que sean mayores de 18.
  3. 3. Dorsales: Convierte una lista de Strings de números ("1", "5", "10") en Integers usando .map().
  4. 4. ¿Hay alguien?: Comprueba si hay algún jugador que se llame "Cazorla" usando anyMatch.
  5. 5. Caja Vacía: Crea un Optional vacío y haz que imprima "No hay fichajes" usando orElse.

Nivel Primer Equipo (Intermedios)

  1. 6. Pichichi: Dada una lista de goles, usa reduce para el total de la temporada.
  2. 7. Sin Repes: Lista con nombres duplicados. Usa distinct para quedarte con los únicos.
  3. 8. El Capi: Busca al primer jugador mayor de 30 años usando findFirst y devuelve Optional.
  4. 9. Limpieza: Filtra nombres que empiecen por "C", conviértelos a mayúsculas y guárdalos.
  5. 10. Tallas: Dada una lista de jugadores, obtén una lista con las longitudes de sus nombres.

Nivel Primera División (Avanzados)

  1. 11. Estadísticas: Usa count para saber cuántos han marcado más de 20 goles.
  2. 12. La Pizarra: Agrupa a los jugadores por posición usando Collectors.groupingBy.
  3. 13. Fichaje Relámpago: Busca un jugador en Optional. Si existe y su sueldo >1M, aplica descuento del 10% (map) o lanza excepción (orElseThrow).
  4. 14. Scouting: Lista de Clubes, cada uno con Jugadores. Usa flatMap para obtener TODOS los jugadores.
  5. 15. Presión Alta: Filtrado complejo sobre 1 millón de datos usando parallelStream.

JAVA STREAMS