Al final de la lección anterior quedaron dos cabos sueltos. El primero: escribiste FiltroMaterial y ReglaAviso, dos interfaces funcionales propias que se parecen sospechosamente a algo que debería venir de serie. Y así es: el JDK trae un catálogo completo de interfaces funcionales de propósito general en el paquete java.util.function, listas para usar, con métodos para combinarlas entre sí. El segundo cabo: cuando una lambda se limita a llamar a un método que ya existe —m -> m.getTitulo()— hasta esa línea es demasiada ceremonia; existe una forma de escribir simplemente Material::getTitulo.
Esta lección cierra el círculo de la programación con comportamiento en Java. Aprenderás qué es formalmente una interfaz funcional y qué comprueba @FunctionalInterface; recorrerás el catálogo entero de java.util.function sabiendo cuándo usar cada pieza; compondrás funciones, predicados y comparadores para construir criterios complejos a partir de piezas simples; y dominarás las cuatro formas de referencia a método. Al terminar, ordenar el catálogo de BiblioTech por tipo y luego por título, en orden inverso, cabrá en una sola línea legible.
Contenido
- Definición formal de interfaz funcional
- La anotación
@FunctionalInterface: qué comprueba exactamente - El paquete
java.util.function: catálogo completo - Las variantes primitivas y el autoboxing
- Las interfaces principales aplicadas a BiblioTech
- Composición de funciones:
andThenycompose - Composición de predicados:
and,or,negate - Composición de comparadores:
comparing,thenComparing,reversed - Referencias a métodos: las cuatro formas
- Cuándo la referencia es más legible que la lambda
- Recibir comportamiento como parámetro:
GestorPrestamos - Errores Comunes y Consejos
- Ejercicios
- Definición formal de interfaz funcional
Una interfaz funcional es una interfaz que declara exactamente un método abstracto.
Se la llama también SAM (Single Abstract Method). Es la única clase de interfaz que puede implementarse con una lambda o con una referencia a método, porque solo con un método abstracto puede el compilador saber, sin ambigüedad, qué está implementando tu código.
Lo importante es la palabra abstracto: hay tres tipos de miembros que no cuentan para el recuento.
@FunctionalInterface
public interface FiltroMaterial {
// 1. EL metodo abstracto: cuenta
boolean acepta(Material material);
// 2. default: NO cuenta
default FiltroMaterial negado() {
return m -> !this.acepta(m);
}
// 3. static: NO cuenta
static FiltroMaterial todos() {
return m -> true;
}
// 4. private: NO cuenta (Java 9)
private boolean nunca(Material m) {
return false;
}
}Esta interfaz sigue siendo funcional: tiene un método abstracto, acepta. Los default, static y private son implementaciones, no obligaciones.
Y hay una cuarta excepción, menos conocida y muy importante en la práctica: los métodos públicos de Object redeclarados tampoco cuentan.
@FunctionalInterface
public interface Comparador {
int comparar(Material a, Material b);
// Redeclarar equals NO rompe la funcionalidad:
// toda clase ya lo hereda de Object
@Override boolean equals(Object o);
}Es exactamente el caso de java.util.Comparator, que declara compare y equals, y aun así es funcional. Si no lo supieras, verlo en el código fuente del JDK sería desconcertante.
// Estas SI son funcionales
Runnable // run()
Comparator<T> // compare(T,T) [+ equals, que no cuenta]
FiltroMaterial // acepta(Material)
ReglaTarifa // tarifaPara(Material,int)
// Estas NO
Prestable // 4 metodos abstractos
Notificable // 2 metodos abstractos
- La anotación
@FunctionalInterface: qué comprueba exactamente
@FunctionalInterface: qué comprueba exactamente@FunctionalInterface es una anotación opcional pero muy recomendable. No cambia el comportamiento del código: activa una comprobación en tiempo de compilación.
@FunctionalInterface
public interface FiltroMaterial {
boolean acepta(Material material);
boolean rechaza(Material material); // segundo abstracto
}error: Unexpected @FunctionalInterface annotation FiltroMaterial is not a functional interface multiple non-overriding abstract methods found in interface FiltroMaterial
Qué comprueba exactamente:
| Comprueba | No comprueba |
|---|---|
| Que sea una interfaz (no clase ni enum) | Que se use realmente con lambdas |
| Que tenga exactamente un método abstracto | Que el nombre del método sea bonito |
| Que no tenga cero métodos abstractos | Nada en tiempo de ejecución |
Y por qué debes usarla siempre en tus interfaces de un solo método, aunque sea opcional:
- Documenta la intención. Quien la lea sabe que está pensada para lambdas.
- Protege el contrato. Sin la anotación, un compañero puede añadir un segundo método abstracto y el error aparecerá en los veinte sitios donde usabas lambdas, con mensajes confusos. Con ella, el error aparece en la interfaz, con el motivo exacto.
- Cuesta una línea.
Ojo a la simetría: una interfaz sin la anotación pero con un solo método abstracto sí admite lambdas. La anotación no habilita nada, solo verifica.
RunnableyComparatorfuncionaban con lambdas desde el primer día de Java 8.
- El paquete
java.util.function: catálogo completo
java.util.function: catálogo completoJava 8 incorporó java.util.function con más de cuarenta interfaces funcionales de propósito general. No hace falta memorizarlas: basta con entender las seis familias y su lógica de nombres.
| Interfaz | Método abstracto | Recibe | Devuelve | Para qué sirve |
|---|---|---|---|---|
Function<T,R> |
R apply(T t) |
1 valor | 1 valor | Transformar un valor en otro |
BiFunction<T,U,R> |
R apply(T t, U u) |
2 valores | 1 valor | Transformar dos valores en uno |
Consumer<T> |
void accept(T t) |
1 valor | nada | Consumir: imprimir, guardar, notificar |
BiConsumer<T,U> |
void accept(T t, U u) |
2 valores | nada | Consumir dos valores |
Supplier<T> |
T get() |
nada | 1 valor | Producir un valor bajo demanda |
Predicate<T> |
boolean test(T t) |
1 valor | boolean |
Decidir: filtrar, validar |
BiPredicate<T,U> |
boolean test(T t, U u) |
2 valores | boolean |
Decidir sobre dos valores |
UnaryOperator<T> |
T apply(T t) |
1 valor de tipo T | valor de tipo T | Transformar sin cambiar de tipo |
BinaryOperator<T> |
T apply(T a, T b) |
2 valores de tipo T | valor de tipo T | Combinar dos en uno del mismo tipo |
El sistema de nombres es completamente regular, y entenderlo vale más que memorizar la tabla:
Bidelante = recibe dos argumentos (BiFunction,BiConsumer,BiPredicate).Operator= un caso especial deFunctiondonde entrada y salida son del mismo tipo.UnaryOperator<String>es literalmente unFunction<String,String>con nombre más corto.- El verbo del método delata la familia:
applytransforma,acceptconsume,getproduce,testdecide.
Sobre los corchetes angulares: Function<Material, String> significa "una función que recibe un Material y devuelve un String". Aquí solo usas tipos genéricos ya existentes; escribir los tuyos propios es la lección 10-01.
flowchart LR
A["Supplier<T><br/>nada -> T"] --> B["Function<T,R><br/>T -> R"]
B --> C["Predicate<T><br/>T -> boolean"]
B --> D["Consumer<T><br/>T -> nada"]
- Las variantes primitivas y el autoboxing
Junto a las nueve interfaces genéricas, el paquete incluye decenas de variantes primitivas: IntPredicate, ToDoubleFunction<T>, IntSupplier, DoubleConsumer, IntUnaryOperator, ToIntBiFunction<T,U>...
¿Por qué existen? Por el autoboxing, que estudiaste en 01-04.
Los tipos genéricos no admiten primitivos: no existe Function<int, int>. Habría que escribir Function<Integer, Integer>, y entonces cada llamada implica dos conversiones ocultas:
// CON autoboxing: Integer -> int -> calculo -> int -> Integer
Function<Integer, Integer> doblar = n -> n * 2;
Integer resultado = doblar.apply(21);
// 1. unboxing de 21 (Integer) a int
// 2. calculo
// 3. boxing del 42 (int) a Integer -> se crea un objeto// SIN autoboxing: puro int
IntUnaryOperator doblarRapido = n -> n * 2;
int rapido = doblarRapido.applyAsInt(21); // cero objetos creadosEn un bucle de un millón de iteraciones, la primera versión crea un millón de objetos Integer que el recolector tendrá que limpiar. La segunda no crea ninguno.
Las reglas de nomenclatura, también regulares:
| Prefijo | Significado | Ejemplo |
|---|---|---|
Int, Long, Double al principio |
El argumento es primitivo | IntPredicate: boolean test(int) |
To + tipo |
El resultado es primitivo | ToDoubleFunction<T>: double applyAsDouble(T) |
Tipo + To + tipo |
Ambos son primitivos | IntToDoubleFunction: double applyAsDouble(int) |
Aplicado a BiblioTech:
// La multa de un material a 20 dias: recibe objeto, devuelve double primitivo
ToDoubleFunction<Material> multaA20 = m -> m.calcularMulta(20);
double total = multaA20.applyAsDouble(dvd); // sin crear ningun Double
// Comprobar si un plazo es valido: recibe int, devuelve boolean
IntPredicate plazoValido = dias -> dias > 0 && dias <= 30;
System.out.println(plazoValido.test(15)); // trueConsejo práctico: en código de negocio normal, usa las versiones genéricas; son más legibles. Recurre a las primitivas cuando trabajes con volúmenes grandes o en bucles calientes. Y sabe que la API de Streams usa las primitivas intensamente (IntStream, mapToDouble), lo verás en 10-04.
- Las interfaces principales aplicadas a BiblioTech
Sustituyamos ahora las interfaces propias de 04-05 por las estándar, y veamos las cuatro familias en acción.
Predicate<T>: decidir. Sustituye directamente a FiltroMaterial.
import java.util.function.Predicate;
Predicate<Material> disponible = m -> m.estaDisponible();
Predicate<Material> esLibro = m -> m instanceof Libro;
Predicate<Material> plazoCorto = m -> m.getDiasPrestamo() <= 7;
System.out.println(disponible.test(javaEfectivo)); // true
System.out.println(esLibro.test(dvdRefactor)); // falseFunction<T,R>: transformar.
import java.util.function.Function;
Function<Material, String> aTitulo = m -> m.getTitulo();
Function<Material, String> aEtiqueta = m -> m.getTipo() + " / " + m.getReferencia();
Function<Empleado, String> aIniciales = e -> e.getIniciales();
System.out.println(aEtiqueta.apply(javaEfectivo)); // Libro / 978-0000000001
System.out.println(aIniciales.apply(marta)); // M.R.Consumer<T>: consumir sin devolver nada.
import java.util.function.Consumer;
Consumer<Material> imprimir = m -> System.out.println(" " + m.describir());
Consumer<Material> prestar = m -> m.prestar();
for (Material m : catalogo) {
imprimir.accept(m);
}Supplier<T>: producir bajo demanda.
import java.util.function.Supplier;
Supplier<Empleado> empleadoPorDefecto = () -> new Empleado("Sin asignar", "EMP-000");
Supplier<String> marcaTiempo = () -> "dia " + System.currentTimeMillis() / 86_400_000;
Empleado e = (asignado != null) ? asignado : empleadoPorDefecto.get();La gracia de Supplier es la evaluación perezosa: el objeto no se crea hasta que se llama a get(). Si crear el valor por defecto fuera costoso, con un Supplier solo pagas ese coste cuando de verdad hace falta.
BiFunction<T,U,R> y BinaryOperator<T>.
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
// Dos entradas de tipos distintos, una salida de un tercer tipo
BiFunction<Material, Integer, Double> multaEn = (m, dias) -> m.calcularMulta(dias);
System.out.printf("%.2f EUR%n", multaEn.apply(dvdRefactor, 20)); // 8,50 EUR
// Dos entradas y una salida, todas del mismo tipo
BinaryOperator<Material> elMasCaro = (a, b) ->
a.calcularMulta(20) >= b.calcularMulta(20) ? a : b;
System.out.println(elMasCaro.apply(javaEfectivo, dvdRefactor).getTitulo());UnaryOperator<T>: transformar sin cambiar de tipo.
import java.util.function.UnaryOperator;
UnaryOperator<String> normalizar = t -> t.trim().toUpperCase();
System.out.println(normalizar.apply(" java efectivo ")); // JAVA EFECTIVO
- Composición de funciones:
andThen y compose
andThen y composeAquí empieza lo verdaderamente potente. Las interfaces de java.util.function traen métodos default que combinan dos funciones en una tercera.
Function ofrece dos, y la diferencia está solo en el orden:
| Método | Significado | Orden de ejecución |
|---|---|---|
f.andThen(g) |
Primero f, luego g |
g(f(x)) |
f.compose(g) |
Primero g, luego f |
f(g(x)) |
Function<Material, String> aTitulo = m -> m.getTitulo();
Function<String, String> aMayus = t -> t.toUpperCase();
Function<String, String> entreComillas = t -> "\"" + t + "\"";
// andThen: se lee de izquierda a derecha, como una tuberia
Function<Material, String> etiqueta = aTitulo.andThen(aMayus).andThen(entreComillas);
System.out.println(etiqueta.apply(javaEfectivo)); // "JAVA EFECTIVO"
// compose: se lee de derecha a izquierda
Function<Material, String> mismo = entreComillas.compose(aMayus).compose(aTitulo);
System.out.println(mismo.apply(javaEfectivo)); // "JAVA EFECTIVO"flowchart LR
A["Material"] -->|"aTitulo"| B["Java Efectivo"]
B -->|"aMayus"| C["JAVA EFECTIVO"]
C -->|"entreComillas"| D["\"JAVA EFECTIVO\""]
Usa andThen casi siempre. Se lee en el orden en que ocurren las cosas, que es como piensa el lector. compose existe por fidelidad a la notación matemática (f ∘ g), y en código suele confundir.
Consumer también tiene andThen, con la diferencia de que ambos consumidores reciben el valor original (ninguno devuelve nada que encadenar):
Consumer<Material> registrar = m -> System.out.println("LOG: " + m.getReferencia());
Consumer<Material> mostrar = m -> System.out.println(" " + m.describir());
Consumer<Material> ambos = registrar.andThen(mostrar);
ambos.accept(javaEfectivo);
- Composición de predicados:
and, or, negate
and, or, negatePredicate ofrece tres métodos de combinación que corresponden a los operadores lógicos de 01-05:
| Método | Equivale a | Descripción |
|---|---|---|
p.and(q) |
p && q |
Cumple ambos. Cortocircuita: si p es falso, no evalúa q |
p.or(q) |
p || q |
Cumple alguno. Cortocircuita también |
p.negate() |
!p |
No cumple |
Predicate.not(p) |
!p |
Igual que negate(), en forma estática (Java 11) |
Predicate.isEqual(x) |
x.equals(...) |
Predicado de igualdad, estático |
Predicate<Material> disponible = m -> m.estaDisponible();
Predicate<Material> esLibro = m -> m instanceof Libro;
Predicate<Material> caro = m -> m.calcularMulta(20) > 5.0;
// Combinaciones, cada una en una linea
Predicate<Material> libroDisponible = esLibro.and(disponible);
Predicate<Material> prestado = disponible.negate();
Predicate<Material> caroOPrestado = caro.or(prestado);
Predicate<Material> libroBaratoLibre = esLibro.and(caro.negate()).and(disponible);Aplicado al catálogo de BiblioTech, con el método contar(Predicate<Material>):
public int contar(Predicate<Material> criterio) {
int n = 0;
for (Material m : materiales) {
if (criterio.test(m)) { n++; }
}
return n;
}System.out.println("Libros disponibles: " + catalogo.contar(libroDisponible));
System.out.println("Prestados: " + catalogo.contar(prestado));
System.out.println("Libros baratos libres:" + catalogo.contar(libroBaratoLibre));La ventaja de fondo: los tres predicados base (esLibro, disponible, caro) se escriben una vez y de ellos salen tantas combinaciones como quieras, cada una con nombre propio y probable por separado. Es el mismo salto que dan las funciones frente al código copiado, aplicado a las condiciones.
Un detalle sobre Predicate.not frente a negate:
Predicate<Material> noDisponible1 = disponible.negate(); // metodo de instancia
Predicate<Material> noDisponible2 = Predicate.not(disponible); // estatico, Java 11
Predicate<Material> noDisponible3 = Predicate.not(Material::estaDisponible); // referenciaLa tercera forma es la más común en código moderno, y usa una referencia a método: eso llega en el apartado 9.
- Composición de comparadores:
comparing, thenComparing, reversed
comparing, thenComparing, reversedEsta es la aplicación que más va a mejorar tu código de BiblioTech. Recuerda el comparador compuesto de 04-05:
// Antes: cuerpo de bloque con condicion intermedia
Comparator<Material> porTipoYTitulo = (a, b) -> {
int porTipo = a.getTipo().compareTo(b.getTipo());
if (porTipo != 0) {
return porTipo;
}
return a.getTitulo().compareToIgnoreCase(b.getTitulo());
};Comparator ofrece un juego de métodos que lo reduce a una línea:
| Método | Qué hace |
|---|---|
Comparator.comparing(f) |
Crea un comparador que compara por la clave que extrae f |
Comparator.comparingInt(f) |
Igual, con clave int (sin autoboxing). También comparingDouble, comparingLong |
c.thenComparing(f) |
Desempate: si c da igualdad, compara por f |
c.reversed() |
Invierte el orden |
Comparator.naturalOrder() |
Orden natural del tipo (el de compareTo) |
Comparator.reverseOrder() |
Orden natural invertido |
c.nullsFirst(c2) / nullsLast(c2) |
Coloca los null al principio o al final |
import java.util.Comparator;
// Una linea, y se lee como una frase
Comparator<Material> porTipoYTitulo =
Comparator.comparing((Material m) -> m.getTipo())
.thenComparing(m -> m.getTitulo());
// Mayor multa primero
Comparator<Material> porMultaDesc =
Comparator.comparingDouble((Material m) -> m.calcularMulta(20)).reversed();
// Tres niveles: por plazo ascendente, luego por tipo, luego por titulo
Comparator<Material> completo =
Comparator.comparingInt((Material m) -> m.getDiasPrestamo())
.thenComparing(m -> m.getTipo())
.thenComparing(m -> m.getTitulo());
Arrays.sort(catalogo, completo);Aplicado al catálogo:
Material[] datos = {
new Dvd("Refactorizacion en vivo", "DVD-0007", 95),
new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Libro("Patrones de Diseno", "Erich Gamma", "978-0000000002", 1994),
new Libro("Refactorizacion", "Martin Fowler", "978-0000000003", 1999)
};
Arrays.sort(datos, porTipoYTitulo);
for (Material m : datos) {
System.out.printf(" %-10s %s%n", m.getTipo(), m.getTitulo());
}DVD Refactorizacion en vivo Libro Java Efectivo Libro Patrones de Diseno Libro Refactorizacion Revista Java Magazine
Dos avisos importantes.
Primero, el orden de reversed() importa. c1.thenComparing(c2).reversed() invierte todo el criterio compuesto; c1.reversed().thenComparing(c2) invierte solo el primero. No son lo mismo.
Segundo, la anotación de tipo en el primer comparing. Fíjate en que se escribe (Material m) -> ... en vez de m -> .... El motivo es que el compilador infiere el tipo de la lambda a partir del tipo objetivo, y en Comparator.comparing(...) encadenado con .thenComparing(...) la inferencia no siempre llega. La alternativa —y la más limpia— es declarar el tipo de la variable o usar una referencia a método:
// Con el tipo de la variable declarado, ya no hace falta anotar la lambda
Comparator<Material> c1 = Comparator.comparing(Material::getTipo)
.thenComparing(Material::getTitulo);Eso Material::getTipo es justo lo que viene ahora.
- Referencias a métodos: las cuatro formas
Cuando una lambda no hace nada más que llamar a un método existente, la referencia a método expresa lo mismo sin ruido. La sintaxis es Objetivo::nombreMetodo, sin paréntesis y sin argumentos.
| Forma | Sintaxis | Lambda equivalente | Ejemplo |
|---|---|---|---|
| 1. Método estático | Clase::metodo |
x -> Clase.metodo(x) |
Integer::parseInt |
| 2. Método de instancia de un objeto concreto | objeto::metodo |
x -> objeto.metodo(x) |
recibo::imprimir |
| 3. Método de instancia de un objeto arbitrario del tipo | Clase::metodo |
x -> x.metodo() |
Material::getTitulo |
| 4. Constructor | Clase::new |
x -> new Clase(x) |
Libro::new |
Las formas 1 y 3 se escriben igual (Clase::metodo) y el compilador las distingue según el método sea static o de instancia. Es la fuente principal de confusión, así que vamos una por una.
Forma 1: método estático. El argumento de la lambda se pasa al método.
Function<String, Integer> aEntero1 = s -> Integer.parseInt(s); // lambda
Function<String, Integer> aEntero2 = Integer::parseInt; // referencia
System.out.println(aEntero2.apply("42") + 1); // 43
// Otro ejemplo del dominio
BiFunction<Double, Double, Double> menor = Math::min;
System.out.println(menor.apply(8.50, 20.0)); // 8.5Forma 2: método de instancia de un objeto concreto. El objeto ya está fijado; el argumento de la lambda va al método.
ReciboConsola recibo = new ReciboConsola();
Consumer<Prestamo> imprimir1 = p -> recibo.imprimirRecibo(p); // lambda
Consumer<Prestamo> imprimir2 = recibo::imprimirRecibo; // referencia
imprimir2.accept(prestamo);Un detalle que conviene conocer: la referencia captura el objeto en el momento de crearse. Si después reasignas recibo a otro objeto, imprimir2 seguirá usando el original. Es coherente con la regla de captura de 04-05.
Forma 3: método de instancia de un objeto arbitrario del tipo. Es la más usada y la que más cuesta al principio. Aquí el argumento de la lambda se convierte en el receptor de la llamada.
Function<Material, String> aTitulo1 = m -> m.getTitulo(); // lambda
Function<Material, String> aTitulo2 = Material::getTitulo; // referencia
// ^^^^^^^^ el parametro pasa a ser 'm'
Predicate<Material> libre1 = m -> m.estaDisponible();
Predicate<Material> libre2 = Material::estaDisponible;
Comparator<String> alfabetico = String::compareToIgnoreCase;
// equivale a: (a, b) -> a.compareToIgnoreCase(b)
// el PRIMER argumento es el receptor, el resto son los parametrosEsa última línea explica el mecanismo entero: en Clase::metodo sobre un método de instancia, el primer argumento de la interfaz funcional es el objeto sobre el que se llama, y los demás son los parámetros del método.
Forma 4: constructor.
Supplier<Empleado> nuevoAnonimo = () -> new Empleado("Sin asignar", "EMP-000");
// Con constructor de un argumento
Function<String, StringBuilder> aBuffer = StringBuilder::new;
// Con constructor de varios argumentos, usando una interfaz funcional propia
@FunctionalInterface
interface CreadorLibro {
Libro crear(String titulo, String autor, String isbn, int anio);
}
CreadorLibro creador1 = (t, a, i, y) -> new Libro(t, a, i, y); // lambda
CreadorLibro creador2 = Libro::new; // referencia
Libro nuevo = creador2.crear("Refactorizacion", "Martin Fowler", "978-0000000003", 1999);
System.out.println(nuevo.describir());Libro::new elige automáticamente el constructor cuya firma encaja con el método de la interfaz funcional. Como Libro tiene dos constructores (uno de cuatro parámetros y otro de cinco), la interfaz CreadorLibro con cuatro parámetros determina cuál se usa.
Ahora, los comparadores del apartado 8 con referencias:
Comparator<Material> porTitulo = Comparator.comparing(Material::getTitulo);
Comparator<Material> porPlazo = Comparator.comparingInt(Material::getDiasPrestamo);
Comparator<Material> porTipoTit = Comparator.comparing(Material::getTipo)
.thenComparing(Material::getTitulo);
Comparator<Material> porTipoDesc = porTipoTit.reversed();Esa es la línea que se prometió al principio de la lección: ordenar por tipo, desempatar por título e invertirlo todo, en un texto que se lee como una frase en inglés.
- Cuándo la referencia es más legible que la lambda
No siempre. Estos criterios te ahorrarán discusiones de revisión de código.
Úsala cuando la lambda solo delega:
m -> m.getTitulo() → Material::getTitulo // mejor
s -> Integer.parseInt(s) → Integer::parseInt // mejor
p -> recibo.imprimirRecibo(p) → recibo::imprimirRecibo // mejor
(a, b) -> a.compareTo(b) → String::compareTo // mejorNo la fuerces cuando hay lógica, aunque sea mínima:
// Hay una operacion: la lambda es obligatoria y ademas mas clara
m -> m.calcularMulta(20)
m -> m.getTitulo().toUpperCase()
m -> !m.estaDisponible()Cuidado con la pérdida de nombres de parámetro. Compara:
// Referencia: conciso, pero no dice que se compara
Arrays.sort(catalogo, Comparator.comparing(Material::getTitulo));
// Lambda: dice explicitamente que hay dos materiales y como se relacionan
Arrays.sort(catalogo, (a, b) -> a.getTitulo().compareToIgnoreCase(b.getTitulo()));Aquí la referencia gana, porque comparing(Material::getTitulo) se lee como "comparando por el título". Pero en casos con varios argumentos del mismo tipo, la lambda con nombres explícitos puede ser más clara.
Sé consistente dentro de una misma expresión. Mezclar estilos en una cadena la vuelve difícil de seguir:
// MAL: mezcla de estilos
Comparator.comparing(Material::getTipo).thenComparing(m -> m.getTitulo())
// BIEN
Comparator.comparing(Material::getTipo).thenComparing(Material::getTitulo)
- Recibir comportamiento como parámetro:
GestorPrestamos
GestorPrestamosCerremos con la aplicación completa: una clase que recibe comportamiento desde fuera para decidir a quién avisar, qué texto enviar y cómo enviarlo, sin conocer ninguna de las tres cosas.
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.Prestamo;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* Coordina el envio de avisos de prestamos.
*
* <p>No decide a quien se avisa (lo dice un Predicate), ni que texto se
* envia (lo dice una Function), ni por donde se envia (lo dice un Consumer).
* Solo orquesta.</p>
*/
public class GestorPrestamos {
private final Prestamo[] prestamos; // array provisional: colecciones en el modulo 5
public GestorPrestamos(Prestamo[] prestamos) {
this.prestamos = prestamos.clone();
}
/**
* Envia un aviso por cada prestamo que cumpla el criterio.
*
* @param criterio decide QUE prestamos se avisan
* @param redactor decide QUE texto se envia
* @param canal decide COMO se envia
* @return numero de avisos enviados
*/
public int avisar(Predicate<Prestamo> criterio,
Function<Prestamo, String> redactor,
Consumer<String> canal) {
int enviados = 0;
for (Prestamo p : prestamos) {
if (criterio.test(p)) {
canal.accept(redactor.apply(p));
enviados++;
}
}
return enviados;
}
/** Cuenta los prestamos que cumplen un criterio. */
public int contar(Predicate<Prestamo> criterio) {
int n = 0;
for (Prestamo p : prestamos) {
if (criterio.test(p)) { n++; }
}
return n;
}
}Y su uso, componiendo criterios a partir de piezas simples:
// --- Criterios base, cada uno con un nombre que explica su intencion ---
Predicate<Prestamo> vencido = Prestamo::estaVencido;
Predicate<Prestamo> devuelto = Prestamo::estaDevuelto;
Predicate<Prestamo> multaAlta = p -> p.calcularMulta() > 5.0;
// --- Criterios compuestos ---
Predicate<Prestamo> pendienteVencido = vencido.and(devuelto.negate());
Predicate<Prestamo> urgente = pendienteVencido.and(multaAlta);
// --- Redactores ---
Function<Prestamo, String> breve = p -> String.format(
"[%s] %s - %d dias de retraso, %.2f EUR",
p.getReferencia(), p.getTituloMaterial(),
p.calcularDiasRetraso(), p.calcularMulta());
Function<Prestamo, String> conDestinatario = breve.andThen(t -> " " + t);
// --- Canales ---
Consumer<String> porConsola = System.out::println; // referencia forma 2
Consumer<String> conPrefijo = t -> System.out.println("AVISO> " + t);
Consumer<String> doble = porConsola.andThen(conPrefijo); // los dos a la vez
// --- Uso ---
GestorPrestamos gestor = new GestorPrestamos(prestamos);
System.out.println("=== Todos los vencidos pendientes ===");
int n1 = gestor.avisar(pendienteVencido, conDestinatario, porConsola);
System.out.println("=== Solo los urgentes, por dos canales ===");
int n2 = gestor.avisar(urgente, breve, doble);
System.out.printf("Enviados: %d normales, %d urgentes%n", n1, n2);
System.out.println("Pendientes vencidos: " + gestor.contar(pendienteVencido));=== Todos los vencidos pendientes === [PR-0001] Java Efectivo - 5 dias de retraso, 1,25 EUR [PR-0002] Refactorizacion en vivo - 17 dias de retraso, 8,50 EUR [PR-0003] Java Magazine - 13 dias de retraso, 1,30 EUR === Solo los urgentes, por dos canales === [PR-0002] Refactorizacion en vivo - 17 dias de retraso, 8,50 EUR AVISO> [PR-0002] Refactorizacion en vivo - 17 dias de retraso, 8,50 EUR Enviados: 3 normales, 1 urgentes
Repasa lo que GestorPrestamos no sabe: no sabe qué es un préstamo urgente, no sabe qué texto se envía, no sabe si el canal es la consola, un correo o un fichero. Solo sabe recorrer y coordinar. Cambiar cualquiera de las tres decisiones no toca su código, y probarlo en el módulo 11 será trivial: le pasas un Consumer que acumula los textos en un array y compruebas el resultado, sin capturar System.out.
Recordatorio: todo esto se vuelve aún más compacto con la API de Streams, donde
filter(criterio).map(redactor).forEach(canal)sustituye al bucle entero. Streams es la lección 10-04; el paso previo, el que acabas de dar, es entender que el comportamiento se pasa como un dato.
Errores Comunes y Consejos
Contar mal los métodos abstractos. Los default, static, private y los públicos de Object redeclarados no cuentan. Por eso Comparator, que declara compare y equals, sigue siendo funcional.
Poner paréntesis en una referencia a método. Material::getTitulo() no compila. La referencia no invoca: describe qué método invocar más tarde.
Confundir la forma 1 con la forma 3. Clase::metodo significa "llama al estático" si el método es static, y "llama a este método sobre el primer argumento" si es de instancia. Si escribes Material::calcularMulta esperando la forma 3, recuerda que calcularMulta(int) recibe un parámetro, así que la interfaz funcional necesitaría dos argumentos: el material y los días.
Referencia a método con argumentos fijos. No existe forma de escribir "referencia a calcularMulta con 20". Para eso hace falta una lambda: m -> m.calcularMulta(20).
Invertir andThen y compose. f.andThen(g) ejecuta f primero. f.compose(g) ejecuta g primero. Ante la duda, usa andThen.
reversed() en el sitio equivocado. Invierte todo lo acumulado hasta ese punto de la cadena, no solo el último criterio.
Fallos de inferencia en Comparator.comparing. Si la cadena no compila, declara el tipo de la variable (Comparator<Material> c = ...) o usa una referencia a método. Casi siempre resuelve el problema.
Autoboxing invisible. Function<Integer,Integer> en un bucle de millones de iteraciones crea millones de objetos. Usa IntUnaryOperator o ToDoubleFunction<T> cuando el rendimiento importe.
Consejo: nombra los predicados compuestos. vencido.and(devuelto.negate()) es correcto pero opaco. Asígnalo a una variable llamada pendienteVencido y el código de negocio se lee solo.
Consejo: reutiliza las interfaces del JDK. Antes de escribir tu propia interfaz funcional, comprueba si Predicate, Function, Consumer o Supplier te sirven. Ganas los métodos de composición gratis y cualquier programador Java entiende tu firma al instante. Escribe una propia solo cuando el nombre del dominio aporte claridad (ReglaTarifa dice más que BiFunction<Material,Integer,Double>) o cuando la firma no encaje en ninguna estándar.
Ejercicios
Ejercicio 1: catálogo de criterios compuestos
Partiendo del Catalogo con buscar(Predicate<Material>) y contar(Predicate<Material>), define cinco predicados base como constantes public static final (disponible, es libro, plazo corto, multa alta a 20 días, título que empieza por una letra dada) y construye con ellos, usando and, or y negate, al menos cuatro criterios compuestos con nombre. Muestra los resultados sobre el catálogo de BiblioTech.
Ejercicio 2: las cuatro referencias a método
Escribe un programa que use una referencia de cada una de las cuatro formas sobre el dominio de BiblioTech:
- Estática: convertir una cadena
"20"enintpara los días transcurridos. - De un objeto concreto: imprimir un recibo con
ReciboConsola. - De un objeto arbitrario: extraer el título de un material.
- De constructor: crear un
Empleadoa partir de nombre e identificador.
Escribe al lado de cada una su lambda equivalente en un comentario.
Ejercicio 3: informe totalmente parametrizado
Escribe un método static void informe(Material[] materiales, Predicate<Material> filtro, Comparator<Material> orden, Function<Material,String> formato, Consumer<String> salida) que filtre, ordene y muestre. Llámalo tres veces con combinaciones distintas: (a) solo libros, por título, formato breve, por consola; (b) todos, por multa descendente, formato con multa, por consola con prefijo; (c) los de plazo corto, por tipo y título, formato completo, acumulando en un StringBuilder que se imprime al final.
Soluciones
Solución 1
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.*;
import java.util.function.Predicate;
public class CriteriosCatalogo {
// ---------- Predicados base: piezas simples y reutilizables ----------
public static final Predicate<Material> DISPONIBLE = Material::estaDisponible;
public static final Predicate<Material> ES_LIBRO = m -> m instanceof Libro;
public static final Predicate<Material> PLAZO_CORTO = m -> m.getDiasPrestamo() <= 7;
public static final Predicate<Material> MULTA_ALTA = m -> m.calcularMulta(20) > 5.0;
/** Fabrica de predicados: devuelve uno distinto segun la letra. */
public static Predicate<Material> empiezaPor(char letra) {
return m -> !m.getTitulo().isEmpty()
&& Character.toUpperCase(m.getTitulo().charAt(0))
== Character.toUpperCase(letra);
}
// ---------- Criterios compuestos: se leen como frases ----------
public static final Predicate<Material> LIBRO_DISPONIBLE =
ES_LIBRO.and(DISPONIBLE);
public static final Predicate<Material> PRESTADO =
DISPONIBLE.negate();
public static final Predicate<Material> URGENTE =
MULTA_ALTA.or(PLAZO_CORTO.and(DISPONIBLE.negate()));
public static final Predicate<Material> LIBRO_BARATO_LIBRE =
ES_LIBRO.and(MULTA_ALTA.negate()).and(DISPONIBLE);
public static void main(String[] args) {
Material[] datos = {
new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018),
new Libro("Patrones de Diseno", "Erich Gamma", "978-0000000002", 1994),
new Libro("Refactorizacion", "Martin Fowler", "978-0000000003", 1999),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Dvd("Refactorizacion en vivo", "DVD-0007", 95)
};
datos[0].prestar(); // Java Efectivo queda prestado
datos[4].prestar(); // el DVD tambien
Catalogo catalogo = new Catalogo(datos);
System.out.println("Libros disponibles: " + catalogo.contar(LIBRO_DISPONIBLE));
System.out.println("Prestados: " + catalogo.contar(PRESTADO));
System.out.println("Urgentes: " + catalogo.contar(URGENTE));
System.out.println("Libros baratos y libres: " + catalogo.contar(LIBRO_BARATO_LIBRE));
System.out.println("Empiezan por 'R': " + catalogo.contar(empiezaPor('R')));
System.out.println("\nLibros disponibles que empiezan por 'P':");
for (Material m : catalogo.buscar(LIBRO_DISPONIBLE.and(empiezaPor('P')))) {
System.out.println(" " + m.describir());
}
}
}Libros disponibles: 2 Prestados: 2 Urgentes: 2 Libros baratos y libres: 2 Empiezan por 'R': 2 Libros disponibles que empiezan por 'P': Libro "Patrones de Diseno" (ref. 978-0000000002) - Erich Gamma, 1994
Tres detalles de diseño. Primero, DISPONIBLE usa una referencia a método de objeto arbitrario (Material::estaDisponible), la forma 3: el material que se pase a test será el receptor de la llamada. Segundo, empiezaPor(char) no es una constante sino una fábrica de predicados: un método que devuelve comportamiento, algo que solo tiene sentido desde que las funciones son valores. Tercero, los criterios compuestos se leen como frases del negocio (LIBRO_BARATO_LIBRE) y cada uno se puede probar por separado en JUnit sin tocar el catálogo.
Solución 2
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.dominio.*;
import com.nexussoftware.bibliotech.presentacion.ReciboConsola;
import java.util.function.*;
public class ReferenciasDemo {
/** Interfaz funcional propia para el constructor de dos argumentos. */
@FunctionalInterface
interface CreadorEmpleado {
Empleado crear(String nombre, String identificador);
}
public static void main(String[] args) {
// ---- FORMA 1: metodo ESTATICO ----
// Lambda equivalente: s -> Integer.parseInt(s)
Function<String, Integer> aEntero = Integer::parseInt;
int dias = aEntero.apply("20");
System.out.println("1. Dias transcurridos leidos: " + dias);
// ---- FORMA 4: CONSTRUCTOR (se usa antes porque hace falta el empleado) ----
// Lambda equivalente: (n, id) -> new Empleado(n, id)
CreadorEmpleado crearEmpleado = Empleado::new;
Empleado marta = crearEmpleado.crear("Marta Ruiz", "EMP-001");
System.out.println("4. Empleado creado: " + marta.getNombre()
+ " (" + marta.getIniciales() + ")");
Libro javaEfectivo = new Libro("Java Efectivo", "Joshua Bloch",
"978-0000000001", 2018);
Prestamo prestamo = new Prestamo(javaEfectivo, marta, 100);
// ---- FORMA 3: metodo de instancia de un OBJETO ARBITRARIO del tipo ----
// Lambda equivalente: m -> m.getTitulo()
// El argumento que se pase a apply sera el RECEPTOR de getTitulo()
Function<Material, String> aTitulo = Material::getTitulo;
System.out.println("3. Titulo extraido: " + aTitulo.apply(javaEfectivo));
// ---- FORMA 2: metodo de instancia de un OBJETO CONCRETO ----
// Lambda equivalente: p -> recibo.imprimirRecibo(p)
// El objeto 'recibo' queda capturado; el argumento va al metodo
ReciboConsola recibo = new ReciboConsola();
Consumer<Prestamo> imprimir = recibo::imprimirRecibo;
prestamo.registrarDevolucion(dias);
System.out.println("2. Recibo impreso mediante referencia a objeto concreto:");
imprimir.accept(prestamo);
}
}1. Dias transcurridos leidos: 20 4. Empleado creado: Marta Ruiz (M.R.) 3. Titulo extraido: Java Efectivo 2. Recibo impreso mediante referencia a objeto concreto: ======================================== RECIBO DE DEVOLUCION - BIBLIOTECH Referencia: PR-0001 Material: Java Efectivo Empleado: Marta Ruiz Retraso: 5 dias Multa: 1,25 EUR Gravedad: LEVE ========================================
La clave para distinguir la forma 2 de la 3 está en qué hay a la izquierda de ::: si es una variable (recibo), el objeto está fijado y el argumento va al método; si es un nombre de tipo (Material), el argumento se convierte en el objeto sobre el que se llama.
Solución 3
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.*;
import java.util.Arrays;
import java.util.Comparator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
public class InformeParametrizado {
/**
* Filtra, ordena, formatea y entrega. Las cuatro decisiones son parametros:
* este metodo no conoce ninguna de ellas.
*/
public static void informe(Material[] materiales,
Predicate<Material> filtro,
Comparator<Material> orden,
Function<Material, String> formato,
Consumer<String> salida) {
// 1. Filtrar (sin colecciones: array de tamano maximo y recorte final)
Material[] seleccion = new Material[materiales.length];
int n = 0;
for (Material m : materiales) {
if (filtro.test(m)) { seleccion[n++] = m; }
}
seleccion = Arrays.copyOf(seleccion, n);
// 2. Ordenar
Arrays.sort(seleccion, orden);
// 3. Formatear y entregar
for (Material m : seleccion) {
salida.accept(formato.apply(m));
}
}
public static void main(String[] args) {
Material[] catalogo = {
new Dvd("Refactorizacion en vivo", "DVD-0007", 95),
new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Libro("Patrones de Diseno", "Erich Gamma", "978-0000000002", 1994),
new Libro("Refactorizacion", "Martin Fowler", "978-0000000003", 1999)
};
// ---------- (a) Solo libros, por titulo, formato breve, por consola ----------
System.out.println("=== (a) Libros por titulo ===");
informe(catalogo,
m -> m instanceof Libro,
Comparator.comparing(Material::getTitulo),
Material::getTitulo, // referencia forma 3
System.out::println); // referencia forma 2
// ---------- (b) Todos, por multa descendente, con prefijo ----------
System.out.println("=== (b) Todos por multa descendente ===");
informe(catalogo,
m -> true,
Comparator.comparingDouble((Material m) -> m.calcularMulta(20)).reversed(),
m -> String.format("%-24s %6.2f EUR", m.getTitulo(), m.calcularMulta(20)),
t -> System.out.println(" > " + t));
// ---------- (c) Plazo corto, por tipo y titulo, acumulando ----------
System.out.println("=== (c) Plazo corto, acumulado ===");
StringBuilder acumulador = new StringBuilder();
informe(catalogo,
m -> m.getDiasPrestamo() <= 7,
Comparator.comparing(Material::getTipo)
.thenComparing(Material::getTitulo),
m -> String.format("%s | %s | plazo %d dias | tarifa %.2f EUR%n",
m.getTipo(), m.getTitulo(),
m.getDiasPrestamo(), m.getTarifaDiaria()),
acumulador::append); // referencia forma 2
System.out.print(acumulador);
}
}=== (a) Libros por titulo === Java Efectivo Patrones de Diseno Refactorizacion === (b) Todos por multa descendente === > Refactorizacion en vivo 8,50 EUR > Java Magazine 1,30 EUR > Java Efectivo 1,25 EUR > Patrones de Diseno 1,25 EUR > Refactorizacion 1,25 EUR === (c) Plazo corto, acumulado === DVD | Refactorizacion en vivo | plazo 3 dias | tarifa 0,50 EUR Revista | Java Magazine | plazo 7 dias | tarifa 0,10 EUR
Lo notable del caso (c): la salida no va a la consola, sino a un StringBuilder, y el cambio ha costado una referencia a método (acumulador::append). El método informe no ha cambiado ni podría notar la diferencia. Esa es exactamente la propiedad que hará triviales las pruebas del módulo 11: sustituyes el Consumer por uno que acumula y compruebas el texto, sin capturar System.out.
Y observa la anotación de tipo en (b): (Material m) -> m.calcularMulta(20) la lleva porque comparingDouble seguida de .reversed() no siempre infiere el tipo desde el argumento. En (a) y (c) no hace falta, porque las referencias a método (Material::getTitulo) llevan el tipo consigo.
Conclusión
Ya tienes el vocabulario completo de la programación con comportamiento en Java. Sabes que una interfaz funcional es la que declara exactamente un método abstracto, y que no cuentan para ese recuento los default, los static, los private ni los métodos públicos de Object redeclarados —razón por la que Comparator, que declara compare y equals, sigue siendo funcional—. Usas @FunctionalInterface siempre en tus interfaces de un método, sabiendo que no habilita nada sino que verifica, y que su valor está en que el error salte en la interfaz y no en los veinte sitios que la usan con lambdas.
Conoces el catálogo de java.util.function y, mejor que la lista, su lógica: Function transforma, Consumer consume, Supplier produce, Predicate decide; el prefijo Bi añade un segundo argumento y Operator es la Function cuya entrada y salida son del mismo tipo. Y entiendes por qué existen las decenas de variantes primitivas: evitar el autoboxing, que en un bucle de un millón de iteraciones significa un millón de objetos que el recolector tendrá que limpiar.
Dominas la composición, que es donde estas piezas dejan de ser curiosidades y se convierten en diseño: andThen y compose para encadenar funciones; and, or y negate para construir criterios de negocio complejos a partir de predicados simples con nombre propio; y Comparator.comparing().thenComparing().reversed(), que convirtió aquel comparador compuesto de seis líneas con condición intermedia en una línea que se lee como una frase. Y dominas las cuatro formas de referencia a método con el criterio para distinguirlas: lo que hay a la izquierda de :: —una variable o un nombre de tipo— determina si el objeto está fijado o si el argumento pasa a ser el receptor.
Sobre todo, has escrito código que recibe comportamiento como parámetro: un GestorPrestamos que no sabe qué es un préstamo urgente, ni qué texto se envía, ni por qué canal, y que sin embargo hace exactamente lo que se le pide en cada llamada. Cambiar cualquiera de las tres decisiones no toca su código, y probarlo será trivial. Recuerda que todo esto se vuelve aún más compacto con la API de Streams, donde filter().map().forEach() sustituye al bucle entero: es la lección 10-04, y llegarás a ella con el concepto ya entendido.
Queda un último asunto pendiente del módulo, y arrastra desde el módulo 3. clasificarGravedad sigue devolviendo las cadenas "LEVE" y "GRAVE", comparadas con equals por todo el proyecto; Prestamo.Incidencia guarda su gravedad como un String que valida a mano en el constructor; y nada impide que alguien escriba "grave" en minúsculas, o "GRAVISIMO", y el compilador no dirá una palabra hasta que el fallo aparezca en producción. Además, has escrito a mano equals, hashCode y toString en cada clase de datos, tres veces las mismas veinte líneas. En la lección 04-07, Enumeraciones y Registros, cierras el módulo con las dos herramientas que resuelven ambas cosas: los enum, que convierten esas cadenas en un conjunto cerrado de valores con seguridad de tipos, exhaustividad en el switch y hasta comportamiento propio por constante; y los record, que generan solos el constructor, los accesores y los tres métodos de Object que tanto te costó escribir en 03-09.
Curso de Programación en Java
Módulo 1: Introducción a Java
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
