Al final de la lliçó anterior van quedar dos caps solts. El primer: vas escriure FiltreMaterial i ReglaAvis, dues interfícies funcionals pròpies que s'assemblen sospitosament a una cosa que hauria de venir de sèrie. I així és: el JDK porta un catàleg complet d'interfícies funcionals de propòsit general al paquet java.util.function, a punt per fer servir, amb mètodes per combinar-les entre si. El segon cap: quan una lambda es limita a cridar un mètode que ja existeix —m -> m.getTitol()— fins i tot aquella línia és massa cerimònia; hi ha una manera d'escriure simplement Material::getTitol.
Aquesta lliçó tanca el cercle de la programació amb comportament en Java. Aprendràs què és formalment una interfície funcional i què comprova @FunctionalInterface; recorreràs el catàleg sencer de java.util.function sabent quan fer servir cada peça; compondràs funcions, predicats i comparadors per construir criteris complexos a partir de peces simples; i dominaràs les quatre formes de referència a mètode. En acabar, ordenar el catàleg de BiblioTech per tipus i després per títol, en ordre invers, cabrà en una sola línia llegible.
Contingut
- Definició formal d'interfície funcional
- L'anotació
@FunctionalInterface: què comprova exactament - El paquet
java.util.function: catàleg complet - Les variants primitives i l'autoboxing
- Les interfícies principals aplicades a BiblioTech
- Composició de funcions:
andThenicompose - Composició de predicats:
and,or,negate - Composició de comparadors:
comparing,thenComparing,reversed - Referències a mètodes: les quatre formes
- Quan la referència és més llegible que la lambda
- Rebre comportament com a paràmetre:
GestorPrestecs - Errors Habituals i Consells
- Exercicis
- Definició formal d'interfície funcional
Una interfície funcional és una interfície que declara exactament un mètode abstracte.
També se l'anomena SAM (Single Abstract Method). És l'única mena d'interfície que es pot implementar amb una lambda o amb una referència a mètode, perquè només amb un mètode abstracte pot el compilador saber, sense ambigüitat, què està implementant el teu codi.
L'important és la paraula abstracte: hi ha tres tipus de membres que no compten per al recompte.
@FunctionalInterface
public interface FiltreMaterial {
// 1. EL metode abstracte: compta
boolean accepta(Material material);
// 2. default: NO compta
default FiltreMaterial negat() {
return m -> !this.accepta(m);
}
// 3. static: NO compta
static FiltreMaterial tots() {
return m -> true;
}
// 4. private: NO compta (Java 9)
private boolean mai(Material m) {
return false;
}
}Aquesta interfície continua sent funcional: té un mètode abstracte, accepta. Els default, static i private són implementacions, no obligacions.
I hi ha una quarta excepció, menys coneguda i molt important a la pràctica: els mètodes públics d'Object redeclarats tampoc no compten.
@FunctionalInterface
public interface Comparador {
int comparar(Material a, Material b);
// Redeclarar equals NO trenca la funcionalitat:
// tota classe ja l'hereta d'Object
@Override boolean equals(Object o);
}És exactament el cas de java.util.Comparator, que declara compare i equals, i tot i així és funcional. Si no ho sabessis, veure-ho al codi font del JDK seria desconcertant.
// Aquestes SI que son funcionals
Runnable // run()
Comparator<T> // compare(T,T) [+ equals, que no compta]
FiltreMaterial // accepta(Material)
ReglaTarifa // tarifaPer(Material,int)
// Aquestes NO
Prestable // 4 metodes abstractes
Notificable // 2 metodes abstractes
- L'anotació
@FunctionalInterface: què comprova exactament
@FunctionalInterface: què comprova exactament@FunctionalInterface és una anotació opcional però molt recomanable. No canvia el comportament del codi: activa una comprovació en temps de compilació.
@FunctionalInterface
public interface FiltreMaterial {
boolean accepta(Material material);
boolean rebutja(Material material); // segon abstracte
}error: Unexpected @FunctionalInterface annotation FiltreMaterial is not a functional interface multiple non-overriding abstract methods found in interface FiltreMaterial
Què comprova exactament:
| Comprova | No comprova |
|---|---|
| Que sigui una interfície (no classe ni enum) | Que es faci servir realment amb lambdes |
| Que tingui exactament un mètode abstracte | Que el nom del mètode sigui bonic |
| Que no tingui zero mètodes abstractes | Res en temps d'execució |
I per què l'has de fer servir sempre a les teves interfícies d'un sol mètode, encara que sigui opcional:
- Documenta la intenció. Qui la llegeixi sap que està pensada per a lambdes.
- Protegeix el contracte. Sense l'anotació, un company pot afegir un segon mètode abstracte i l'error apareixerà als vint llocs on feies servir lambdes, amb missatges confusos. Amb ella, l'error apareix a la interfície, amb el motiu exacte.
- Costa una línia.
Compte amb la simetria: una interfície sense l'anotació però amb un sol mètode abstracte sí que admet lambdes. L'anotació no habilita res, només verifica.
RunnableiComparatorfuncionaven amb lambdes des del primer dia de Java 8.
- El paquet
java.util.function: catàleg complet
java.util.function: catàleg completJava 8 va incorporar java.util.function amb més de quaranta interfícies funcionals de propòsit general. No cal memoritzar-les: n'hi ha prou d'entendre les sis famílies i la seva lògica de noms.
| Interfície | Mètode abstracte | Rep | Retorna | Per a què serveix |
|---|---|---|---|---|
Function<T,R> |
R apply(T t) |
1 valor | 1 valor | Transformar un valor en un altre |
BiFunction<T,U,R> |
R apply(T t, U u) |
2 valors | 1 valor | Transformar dos valors en un |
Consumer<T> |
void accept(T t) |
1 valor | res | Consumir: imprimir, desar, notificar |
BiConsumer<T,U> |
void accept(T t, U u) |
2 valors | res | Consumir dos valors |
Supplier<T> |
T get() |
res | 1 valor | Produir un valor sota demanda |
Predicate<T> |
boolean test(T t) |
1 valor | boolean |
Decidir: filtrar, validar |
BiPredicate<T,U> |
boolean test(T t, U u) |
2 valors | boolean |
Decidir sobre dos valors |
UnaryOperator<T> |
T apply(T t) |
1 valor de tipus T | valor de tipus T | Transformar sense canviar de tipus |
BinaryOperator<T> |
T apply(T a, T b) |
2 valors de tipus T | valor de tipus T | Combinar dos en un del mateix tipus |
El sistema de noms és completament regular, i entendre'l val més que memoritzar la taula:
Bial davant = rep dos arguments (BiFunction,BiConsumer,BiPredicate).Operator= un cas especial deFunctionon entrada i sortida són del mateix tipus.UnaryOperator<String>és literalment unFunction<String,String>amb un nom més curt.- El verb del mètode delata la família:
applytransforma,acceptconsumeix,getprodueix,testdecideix.
Sobre els claudàtors angulars: Function<Material, String> significa "una funció que rep un Material i retorna un String". Aquí només fas servir tipus genèrics ja existents; escriure els teus propis és la lliçó 10-01.
flowchart LR
A["Supplier<T><br/>res -> T"] --> B["Function<T,R><br/>T -> R"]
B --> C["Predicate<T><br/>T -> boolean"]
B --> D["Consumer<T><br/>T -> res"]
- Les variants primitives i l'autoboxing
Al costat de les nou interfícies genèriques, el paquet inclou desenes de variants primitives: IntPredicate, ToDoubleFunction<T>, IntSupplier, DoubleConsumer, IntUnaryOperator, ToIntBiFunction<T,U>...
Per què existeixen? Per l'autoboxing, que vas estudiar a 01-04.
Els tipus genèrics no admeten primitius: no existeix Function<int, int>. Caldria escriure Function<Integer, Integer>, i llavors cada crida implica dues conversions ocultes:
// AMB autoboxing: Integer -> int -> calcul -> int -> Integer
Function<Integer, Integer> doblar = n -> n * 2;
Integer resultat = doblar.apply(21);
// 1. unboxing de 21 (Integer) a int
// 2. calcul
// 3. boxing del 42 (int) a Integer -> es crea un objecte// SENSE autoboxing: pur int
IntUnaryOperator doblarRapid = n -> n * 2;
int rapid = doblarRapid.applyAsInt(21); // zero objectes creatsEn un bucle d'un milió d'iteracions, la primera versió crea un milió d'objectes Integer que el recol·lector haurà de netejar. La segona no en crea cap.
Les regles de nomenclatura, també regulars:
| Prefix | Significat | Exemple |
|---|---|---|
Int, Long, Double al principi |
L'argument és primitiu | IntPredicate: boolean test(int) |
To + tipus |
El resultat és primitiu | ToDoubleFunction<T>: double applyAsDouble(T) |
Tipus + To + tipus |
Tots dos són primitius | IntToDoubleFunction: double applyAsDouble(int) |
Aplicat a BiblioTech:
// La multa d'un material als 20 dies: rep objecte, retorna double primitiu
ToDoubleFunction<Material> multaA20 = m -> m.calcularMulta(20);
double total = multaA20.applyAsDouble(dvd); // sense crear cap Double
// Comprovar si un termini es valid: rep int, retorna boolean
IntPredicate terminiValid = dies -> dies > 0 && dies <= 30;
System.out.println(terminiValid.test(15)); // trueConsell pràctic: en codi de negoci normal, fes servir les versions genèriques; són més llegibles. Recorre a les primitives quan treballis amb volums grans o en bucles calents. I sàpigues que l'API de Streams fa servir les primitives intensament (IntStream, mapToDouble), ho veuràs a 10-04.
- Les interfícies principals aplicades a BiblioTech
Substituïm ara les interfícies pròpies de 04-05 per les estàndard, i vegem les quatre famílies en acció.
Predicate<T>: decidir. Substitueix directament FiltreMaterial.
import java.util.function.Predicate;
Predicate<Material> disponible = m -> m.estaDisponible();
Predicate<Material> esLlibre = m -> m instanceof Llibre;
Predicate<Material> terminiCurt = m -> m.getDiesPrestec() <= 7;
System.out.println(disponible.test(javaEficac)); // true
System.out.println(esLlibre.test(dvdRefactor)); // falseFunction<T,R>: transformar.
import java.util.function.Function;
Function<Material, String> aTitol = m -> m.getTitol();
Function<Material, String> aEtiqueta = m -> m.getTipus() + " / " + m.getReferencia();
Function<Empleat, String> aInicials = e -> e.getInicials();
System.out.println(aEtiqueta.apply(javaEficac)); // Llibre / 978-0000000001
System.out.println(aInicials.apply(marta)); // M.R.Consumer<T>: consumir sense retornar res.
import java.util.function.Consumer;
Consumer<Material> imprimir = m -> System.out.println(" " + m.descriure());
Consumer<Material> prestar = m -> m.prestar();
for (Material m : cataleg) {
imprimir.accept(m);
}Supplier<T>: produir sota demanda.
import java.util.function.Supplier;
Supplier<Empleat> empleatPerDefecte = () -> new Empleat("Sense assignar", "EMP-000");
Supplier<String> marcaTemps = () -> "dia " + System.currentTimeMillis() / 86_400_000;
Empleat e = (assignat != null) ? assignat : empleatPerDefecte.get();La gràcia de Supplier és l'avaluació mandrosa: l'objecte no es crea fins que es crida get(). Si crear el valor per defecte fos costós, amb un Supplier només pagues aquell cost quan de debò cal.
BiFunction<T,U,R> i BinaryOperator<T>.
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
// Dues entrades de tipus diferents, una sortida d'un tercer tipus
BiFunction<Material, Integer, Double> multaEn = (m, dies) -> m.calcularMulta(dies);
System.out.printf("%.2f EUR%n", multaEn.apply(dvdRefactor, 20)); // 8,50 EUR
// Dues entrades i una sortida, totes del mateix tipus
BinaryOperator<Material> elMesCar = (a, b) ->
a.calcularMulta(20) >= b.calcularMulta(20) ? a : b;
System.out.println(elMesCar.apply(javaEficac, dvdRefactor).getTitol());UnaryOperator<T>: transformar sense canviar de tipus.
import java.util.function.UnaryOperator;
UnaryOperator<String> normalitzar = t -> t.trim().toUpperCase();
System.out.println(normalitzar.apply(" java eficac ")); // JAVA EFICAC
- Composició de funcions:
andThen i compose
andThen i composeAquí comença el que és veritablement potent. Les interfícies de java.util.function porten mètodes default que combinen dues funcions en una tercera.
Function n'ofereix dos, i la diferència és només en l'ordre:
| Mètode | Significat | Ordre d'execució |
|---|---|---|
f.andThen(g) |
Primer f, després g |
g(f(x)) |
f.compose(g) |
Primer g, després f |
f(g(x)) |
Function<Material, String> aTitol = m -> m.getTitol();
Function<String, String> aMajus = t -> t.toUpperCase();
Function<String, String> entreCometes = t -> "\"" + t + "\"";
// andThen: es llegeix d'esquerra a dreta, com una canonada
Function<Material, String> etiqueta = aTitol.andThen(aMajus).andThen(entreCometes);
System.out.println(etiqueta.apply(javaEficac)); // "JAVA EFICAC"
// compose: es llegeix de dreta a esquerra
Function<Material, String> mateix = entreCometes.compose(aMajus).compose(aTitol);
System.out.println(mateix.apply(javaEficac)); // "JAVA EFICAC"flowchart LR
A["Material"] -->|"aTitol"| B["Java Eficac"]
B -->|"aMajus"| C["JAVA EFICAC"]
C -->|"entreCometes"| D["\"JAVA EFICAC\""]
Fes servir andThen gairebé sempre. Es llegeix en l'ordre en què passen les coses, que és com pensa el lector. compose existeix per fidelitat a la notació matemàtica (f ∘ g), i en codi sol confondre.
Consumer també té andThen, amb la diferència que tots dos consumidors reben el valor original (cap no retorna res per encadenar):
Consumer<Material> registrar = m -> System.out.println("LOG: " + m.getReferencia());
Consumer<Material> mostrar = m -> System.out.println(" " + m.descriure());
Consumer<Material> totsDos = registrar.andThen(mostrar);
totsDos.accept(javaEficac);
- Composició de predicats:
and, or, negate
and, or, negatePredicate ofereix tres mètodes de combinació que corresponen als operadors lògics de 01-05:
| Mètode | Equival a | Descripció |
|---|---|---|
p.and(q) |
p && q |
Compleix tots dos. Fa curtcircuit: si p és fals, no avalua q |
p.or(q) |
p || q |
Compleix algun. També fa curtcircuit |
p.negate() |
!p |
No compleix |
Predicate.not(p) |
!p |
Igual que negate(), en forma estàtica (Java 11) |
Predicate.isEqual(x) |
x.equals(...) |
Predicat d'igualtat, estàtic |
Predicate<Material> disponible = m -> m.estaDisponible();
Predicate<Material> esLlibre = m -> m instanceof Llibre;
Predicate<Material> car = m -> m.calcularMulta(20) > 5.0;
// Combinacions, cadascuna en una linia
Predicate<Material> llibreDisponible = esLlibre.and(disponible);
Predicate<Material> prestat = disponible.negate();
Predicate<Material> carOPrestat = car.or(prestat);
Predicate<Material> llibreBaratLliure = esLlibre.and(car.negate()).and(disponible);Aplicat al catàleg de BiblioTech, amb el mètode comptar(Predicate<Material>):
public int comptar(Predicate<Material> criteri) {
int n = 0;
for (Material m : materials) {
if (criteri.test(m)) { n++; }
}
return n;
}System.out.println("Llibres disponibles: " + cataleg.comptar(llibreDisponible));
System.out.println("Prestats: " + cataleg.comptar(prestat));
System.out.println("Llibres barats lliures:" + cataleg.comptar(llibreBaratLliure));L'avantatge de fons: els tres predicats base (esLlibre, disponible, car) s'escriuen una vegada i d'ells surten tantes combinacions com vulguis, cadascuna amb nom propi i provable per separat. És el mateix salt que fan les funcions enfront del codi copiat, aplicat a les condicions.
Un detall sobre Predicate.not enfront de negate:
Predicate<Material> noDisponible1 = disponible.negate(); // metode d'instancia
Predicate<Material> noDisponible2 = Predicate.not(disponible); // estatic, Java 11
Predicate<Material> noDisponible3 = Predicate.not(Material::estaDisponible); // referenciaLa tercera forma és la més comuna en codi modern, i fa servir una referència a mètode: això arriba a l'apartat 9.
- Composició de comparadors:
comparing, thenComparing, reversed
comparing, thenComparing, reversedAquesta és l'aplicació que més millorarà el teu codi de BiblioTech. Recorda el comparador compost de 04-05:
// Abans: cos de bloc amb condicio intermedia
Comparator<Material> perTipusITitol = (a, b) -> {
int perTipus = a.getTipus().compareTo(b.getTipus());
if (perTipus != 0) {
return perTipus;
}
return a.getTitol().compareToIgnoreCase(b.getTitol());
};Comparator ofereix un joc de mètodes que el redueix a una línia:
| Mètode | Què fa |
|---|---|
Comparator.comparing(f) |
Crea un comparador que compara per la clau que extreu f |
Comparator.comparingInt(f) |
Igual, amb clau int (sense autoboxing). També comparingDouble, comparingLong |
c.thenComparing(f) |
Desempat: si c dona igualtat, compara per f |
c.reversed() |
Inverteix l'ordre |
Comparator.naturalOrder() |
Ordre natural del tipus (el de compareTo) |
Comparator.reverseOrder() |
Ordre natural invertit |
c.nullsFirst(c2) / nullsLast(c2) |
Col·loca els null al principi o al final |
import java.util.Comparator;
// Una linia, i es llegeix com una frase
Comparator<Material> perTipusITitol =
Comparator.comparing((Material m) -> m.getTipus())
.thenComparing(m -> m.getTitol());
// Multa mes alta primer
Comparator<Material> perMultaDesc =
Comparator.comparingDouble((Material m) -> m.calcularMulta(20)).reversed();
// Tres nivells: per termini ascendent, despres per tipus, despres per titol
Comparator<Material> complet =
Comparator.comparingInt((Material m) -> m.getDiesPrestec())
.thenComparing(m -> m.getTipus())
.thenComparing(m -> m.getTitol());
Arrays.sort(cataleg, complet);Aplicat al catàleg:
Material[] dades = {
new Dvd("Refactoritzacio en directe", "DVD-0007", 95),
new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994),
new Llibre("Refactoritzacio", "Martin Fowler", "978-0000000003", 1999)
};
Arrays.sort(dades, perTipusITitol);
for (Material m : dades) {
System.out.printf(" %-10s %s%n", m.getTipus(), m.getTitol());
}DVD Refactoritzacio en directe Llibre Java Eficac Llibre Patrons de Disseny Llibre Refactoritzacio Revista Java Magazine
Dos avisos importants.
Primer, l'ordre de reversed() importa. c1.thenComparing(c2).reversed() inverteix tot el criteri compost; c1.reversed().thenComparing(c2) inverteix només el primer. No són el mateix.
Segon, l'anotació de tipus al primer comparing. Fixa't que s'escriu (Material m) -> ... en lloc de m -> .... El motiu és que el compilador infereix el tipus de la lambda a partir del tipus objectiu, i a Comparator.comparing(...) encadenat amb .thenComparing(...) la inferència no sempre hi arriba. L'alternativa —i la més neta— és declarar el tipus de la variable o fer servir una referència a mètode:
// Amb el tipus de la variable declarat, ja no cal anotar la lambda
Comparator<Material> c1 = Comparator.comparing(Material::getTipus)
.thenComparing(Material::getTitol);Aquell Material::getTipus és just el que ve ara.
- Referències a mètodes: les quatre formes
Quan una lambda no fa res més que cridar un mètode existent, la referència a mètode expressa el mateix sense soroll. La sintaxi és Objectiu::nomMetode, sense parèntesis i sense arguments.
| Forma | Sintaxi | Lambda equivalent | Exemple |
|---|---|---|---|
| 1. Mètode estàtic | Classe::metode |
x -> Classe.metode(x) |
Integer::parseInt |
| 2. Mètode d'instància d'un objecte concret | objecte::metode |
x -> objecte.metode(x) |
rebut::imprimir |
| 3. Mètode d'instància d'un objecte arbitrari del tipus | Classe::metode |
x -> x.metode() |
Material::getTitol |
| 4. Constructor | Classe::new |
x -> new Classe(x) |
Llibre::new |
Les formes 1 i 3 s'escriuen igual (Classe::metode) i el compilador les distingeix segons que el mètode sigui static o d'instància. És la font principal de confusió, així que anem una per una.
Forma 1: mètode estàtic. L'argument de la lambda es passa al mètode.
Function<String, Integer> aEnter1 = s -> Integer.parseInt(s); // lambda
Function<String, Integer> aEnter2 = Integer::parseInt; // referencia
System.out.println(aEnter2.apply("42") + 1); // 43
// Un altre exemple del domini
BiFunction<Double, Double, Double> menor = Math::min;
System.out.println(menor.apply(8.50, 20.0)); // 8.5Forma 2: mètode d'instància d'un objecte concret. L'objecte ja està fixat; l'argument de la lambda va al mètode.
RebutConsola rebut = new RebutConsola();
Consumer<Prestec> imprimir1 = p -> rebut.imprimirRebut(p); // lambda
Consumer<Prestec> imprimir2 = rebut::imprimirRebut; // referencia
imprimir2.accept(prestec);Un detall que convé conèixer: la referència captura l'objecte en el moment de crear-se. Si després reassignes rebut a un altre objecte, imprimir2 continuarà fent servir l'original. És coherent amb la regla de captura de 04-05.
Forma 3: mètode d'instància d'un objecte arbitrari del tipus. És la més usada i la que més costa al principi. Aquí l'argument de la lambda es converteix en el receptor de la crida.
Function<Material, String> aTitol1 = m -> m.getTitol(); // lambda
Function<Material, String> aTitol2 = Material::getTitol; // referencia
// ^^^^^^^^ el parametre passa a ser 'm'
Predicate<Material> lliure1 = m -> m.estaDisponible();
Predicate<Material> lliure2 = Material::estaDisponible;
Comparator<String> alfabetic = String::compareToIgnoreCase;
// equival a: (a, b) -> a.compareToIgnoreCase(b)
// el PRIMER argument es el receptor, la resta son els parametresAquella última línia explica el mecanisme sencer: a Classe::metode sobre un mètode d'instància, el primer argument de la interfície funcional és l'objecte sobre el qual es crida, i els altres són els paràmetres del mètode.
Forma 4: constructor.
Supplier<Empleat> nouAnonim = () -> new Empleat("Sense assignar", "EMP-000");
// Amb constructor d'un argument
Function<String, StringBuilder> aBuffer = StringBuilder::new;
// Amb constructor de diversos arguments, fent servir una interficie funcional propia
@FunctionalInterface
interface CreadorLlibre {
Llibre crear(String titol, String autor, String isbn, int any);
}
CreadorLlibre creador1 = (t, a, i, y) -> new Llibre(t, a, i, y); // lambda
CreadorLlibre creador2 = Llibre::new; // referencia
Llibre nou = creador2.crear("Refactoritzacio", "Martin Fowler", "978-0000000003", 1999);
System.out.println(nou.descriure());Llibre::new tria automàticament el constructor la signatura del qual encaixa amb el mètode de la interfície funcional. Com que Llibre té dos constructors (un de quatre paràmetres i un altre de cinc), la interfície CreadorLlibre amb quatre paràmetres determina quin es fa servir.
Ara, els comparadors de l'apartat 8 amb referències:
Comparator<Material> perTitol = Comparator.comparing(Material::getTitol);
Comparator<Material> perTermini = Comparator.comparingInt(Material::getDiesPrestec);
Comparator<Material> perTipusTit = Comparator.comparing(Material::getTipus)
.thenComparing(Material::getTitol);
Comparator<Material> perTipusDesc = perTipusTit.reversed();Aquesta és la línia que es va prometre al principi de la lliçó: ordenar per tipus, desempatar per títol i invertir-ho tot, en un text que es llegeix com una frase en anglès.
- Quan la referència és més llegible que la lambda
No sempre. Aquests criteris t'estalviaran discussions de revisió de codi.
Fes-la servir quan la lambda només delega:
m -> m.getTitol() → Material::getTitol // millor
s -> Integer.parseInt(s) → Integer::parseInt // millor
p -> rebut.imprimirRebut(p) → rebut::imprimirRebut // millor
(a, b) -> a.compareTo(b) → String::compareTo // millorNo la forcis quan hi ha lògica, encara que sigui mínima:
// Hi ha una operacio: la lambda es obligatoria i a mes mes clara
m -> m.calcularMulta(20)
m -> m.getTitol().toUpperCase()
m -> !m.estaDisponible()Compte amb la pèrdua de noms de paràmetre. Compara:
// Referencia: concis, pero no diu que es compara
Arrays.sort(cataleg, Comparator.comparing(Material::getTitol));
// Lambda: diu explicitament que hi ha dos materials i com es relacionen
Arrays.sort(cataleg, (a, b) -> a.getTitol().compareToIgnoreCase(b.getTitol()));Aquí la referència guanya, perquè comparing(Material::getTitol) es llegeix com "comparant pel títol". Però en casos amb diversos arguments del mateix tipus, la lambda amb noms explícits pot ser més clara.
Sigues consistent dins d'una mateixa expressió. Barrejar estils en una cadena la fa difícil de seguir:
// MALAMENT: barreja d'estils
Comparator.comparing(Material::getTipus).thenComparing(m -> m.getTitol())
// BE
Comparator.comparing(Material::getTipus).thenComparing(Material::getTitol)
- Rebre comportament com a paràmetre:
GestorPrestecs
GestorPrestecsTanquem amb l'aplicació completa: una classe que rep comportament des de fora per decidir a qui avisar, quin text enviar i com enviar-lo, sense conèixer cap de les tres coses.
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.Prestec;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* Coordina l'enviament d'avisos de prestecs.
*
* <p>No decideix a qui s'avisa (ho diu un Predicate), ni quin text s'envia
* (ho diu una Function), ni per on s'envia (ho diu un Consumer).
* Nomes orquestra.</p>
*/
public class GestorPrestecs {
private final Prestec[] prestecs; // array provisional: les llistes arriben al modul 5
public GestorPrestecs(Prestec[] prestecs) {
this.prestecs = prestecs.clone();
}
/**
* Envia un avis per cada prestec que compleixi el criteri.
*
* @param criteri decideix QUINS prestecs s'avisen
* @param redactor decideix QUIN text s'envia
* @param canal decideix COM s'envia
* @return nombre d'avisos enviats
*/
public int avisar(Predicate<Prestec> criteri,
Function<Prestec, String> redactor,
Consumer<String> canal) {
int enviats = 0;
for (Prestec p : prestecs) {
if (criteri.test(p)) {
canal.accept(redactor.apply(p));
enviats++;
}
}
return enviats;
}
/** Compta els prestecs que compleixen un criteri. */
public int comptar(Predicate<Prestec> criteri) {
int n = 0;
for (Prestec p : prestecs) {
if (criteri.test(p)) { n++; }
}
return n;
}
}I el seu ús, component criteris a partir de peces simples:
// --- Criteris base, cadascun amb un nom que explica la seva intencio ---
Predicate<Prestec> vencut = Prestec::estaVencut;
Predicate<Prestec> retornat = Prestec::estaRetornat;
Predicate<Prestec> multaAlta = p -> p.calcularMulta() > 5.0;
// --- Criteris compostos ---
Predicate<Prestec> pendentVencut = vencut.and(retornat.negate());
Predicate<Prestec> urgent = pendentVencut.and(multaAlta);
// --- Redactors ---
Function<Prestec, String> breu = p -> String.format(
"[%s] %s - %d dies de retard, %.2f EUR",
p.getReferencia(), p.getTitolMaterial(),
p.calcularDiesRetard(), p.calcularMulta());
Function<Prestec, String> ambDestinatari = breu.andThen(t -> " " + t);
// --- Canals ---
Consumer<String> perConsola = System.out::println; // referencia forma 2
Consumer<String> ambPrefix = t -> System.out.println("AVIS> " + t);
Consumer<String> doble = perConsola.andThen(ambPrefix); // tots dos alhora
// --- Us ---
GestorPrestecs gestor = new GestorPrestecs(prestecs);
System.out.println("=== Tots els vencuts pendents ===");
int n1 = gestor.avisar(pendentVencut, ambDestinatari, perConsola);
System.out.println("=== Nomes els urgents, per dos canals ===");
int n2 = gestor.avisar(urgent, breu, doble);
System.out.printf("Enviats: %d normals, %d urgents%n", n1, n2);
System.out.println("Pendents vencuts: " + gestor.comptar(pendentVencut));=== Tots els vencuts pendents === [PR-0001] Java Eficac - 5 dies de retard, 1,25 EUR [PR-0002] Refactoritzacio en directe - 17 dies de retard, 8,50 EUR [PR-0003] Java Magazine - 13 dies de retard, 1,30 EUR === Nomes els urgents, per dos canals === [PR-0002] Refactoritzacio en directe - 17 dies de retard, 8,50 EUR AVIS> [PR-0002] Refactoritzacio en directe - 17 dies de retard, 8,50 EUR Enviats: 3 normals, 1 urgents
Repassa el que GestorPrestecs no sap: no sap què és un préstec urgent, no sap quin text s'envia, no sap si el canal és la consola, un correu o un fitxer. Només sap recórrer i coordinar. Canviar qualsevol de les tres decisions no toca el seu codi, i provar-lo al mòdul 11 serà trivial: li passes un Consumer que acumula els textos en un array i comproves el resultat, sense capturar System.out.
Recordatori: tot això es torna encara més compacte amb l'API de Streams, on
filter(criteri).map(redactor).forEach(canal)substitueix el bucle sencer. Streams és la lliçó 10-04; el pas previ, el que acabes de fer, és entendre que el comportament es passa com una dada.
Errors Habituals i Consells
Comptar malament els mètodes abstractes. Els default, static, private i els públics d'Object redeclarats no compten. Per això Comparator, que declara compare i equals, continua sent funcional.
Posar parèntesis en una referència a mètode. Material::getTitol() no compila. La referència no invoca: descriu quin mètode invocar més tard.
Confondre la forma 1 amb la forma 3. Classe::metode significa "crida l'estàtic" si el mètode és static, i "crida aquest mètode sobre el primer argument" si és d'instància. Si escrius Material::calcularMulta esperant la forma 3, recorda que calcularMulta(int) rep un paràmetre, així que la interfície funcional necessitaria dos arguments: el material i els dies.
Referència a mètode amb arguments fixos. No hi ha manera d'escriure "referència a calcularMulta amb 20". Per a això cal una lambda: m -> m.calcularMulta(20).
Invertir andThen i compose. f.andThen(g) executa f primer. f.compose(g) executa g primer. Davant del dubte, fes servir andThen.
reversed() al lloc equivocat. Inverteix tot l'acumulat fins a aquell punt de la cadena, no només l'últim criteri.
Fallades d'inferència a Comparator.comparing. Si la cadena no compila, declara el tipus de la variable (Comparator<Material> c = ...) o fes servir una referència a mètode. Gairebé sempre resol el problema.
Autoboxing invisible. Function<Integer,Integer> en un bucle de milions d'iteracions crea milions d'objectes. Fes servir IntUnaryOperator o ToDoubleFunction<T> quan el rendiment importi.
Consell: posa nom als predicats compostos. vencut.and(retornat.negate()) és correcte però opac. Assigna'l a una variable anomenada pendentVencut i el codi de negoci es llegeix sol.
Consell: reutilitza les interfícies del JDK. Abans d'escriure la teva pròpia interfície funcional, comprova si Predicate, Function, Consumer o Supplier et serveixen. Guanyes els mètodes de composició de franc i qualsevol programador Java entén la teva signatura a l'instant. Escriu-ne una de pròpia només quan el nom del domini aporti claredat (ReglaTarifa diu més que BiFunction<Material,Integer,Double>) o quan la signatura no encaixi en cap d'estàndard.
Exercicis
Exercici 1: catàleg de criteris compostos
Partint del Cataleg amb cercar(Predicate<Material>) i comptar(Predicate<Material>), defineix cinc predicats base com a constants public static final (disponible, és llibre, termini curt, multa alta als 20 dies, títol que comença per una lletra donada) i construeix amb ells, fent servir and, or i negate, com a mínim quatre criteris compostos amb nom. Mostra els resultats sobre el catàleg de BiblioTech.
Exercici 2: les quatre referències a mètode
Escriu un programa que faci servir una referència de cadascuna de les quatre formes sobre el domini de BiblioTech:
- Estàtica: convertir una cadena
"20"enintper als dies transcorreguts. - D'un objecte concret: imprimir un rebut amb
RebutConsola. - D'un objecte arbitrari: extreure el títol d'un material.
- De constructor: crear un
Empleata partir de nom i identificador.
Escriu al costat de cadascuna la seva lambda equivalent en un comentari.
Exercici 3: informe totalment parametritzat
Escriu un mètode static void informe(Material[] materials, Predicate<Material> filtre, Comparator<Material> ordre, Function<Material,String> format, Consumer<String> sortida) que filtri, ordeni i mostri. Crida'l tres vegades amb combinacions diferents: (a) només llibres, per títol, format breu, per consola; (b) tots, per multa descendent, format amb multa, per consola amb prefix; (c) els de termini curt, per tipus i títol, format complet, acumulant en un StringBuilder que s'imprimeix al final.
Solucions
Solució 1
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.*;
import java.util.function.Predicate;
public class CriterisCataleg {
// ---------- Predicats base: peces simples i reutilitzables ----------
public static final Predicate<Material> DISPONIBLE = Material::estaDisponible;
public static final Predicate<Material> ES_LLIBRE = m -> m instanceof Llibre;
public static final Predicate<Material> TERMINI_CURT = m -> m.getDiesPrestec() <= 7;
public static final Predicate<Material> MULTA_ALTA = m -> m.calcularMulta(20) > 5.0;
/** Fabrica de predicats: en retorna un de diferent segons la lletra. */
public static Predicate<Material> comencaPer(char lletra) {
return m -> !m.getTitol().isEmpty()
&& Character.toUpperCase(m.getTitol().charAt(0))
== Character.toUpperCase(lletra);
}
// ---------- Criteris compostos: es llegeixen com a frases ----------
public static final Predicate<Material> LLIBRE_DISPONIBLE =
ES_LLIBRE.and(DISPONIBLE);
public static final Predicate<Material> PRESTAT =
DISPONIBLE.negate();
public static final Predicate<Material> URGENT =
MULTA_ALTA.or(TERMINI_CURT.and(DISPONIBLE.negate()));
public static final Predicate<Material> LLIBRE_BARAT_LLIURE =
ES_LLIBRE.and(MULTA_ALTA.negate()).and(DISPONIBLE);
public static void main(String[] args) {
Material[] dades = {
new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994),
new Llibre("Refactoritzacio", "Martin Fowler", "978-0000000003", 1999),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Dvd("Refactoritzacio en directe", "DVD-0007", 95)
};
dades[0].prestar(); // Java Eficac queda prestat
dades[4].prestar(); // el DVD tambe
Cataleg cataleg = new Cataleg(dades);
System.out.println("Llibres disponibles: " + cataleg.comptar(LLIBRE_DISPONIBLE));
System.out.println("Prestats: " + cataleg.comptar(PRESTAT));
System.out.println("Urgents: " + cataleg.comptar(URGENT));
System.out.println("Llibres barats i lliures: " + cataleg.comptar(LLIBRE_BARAT_LLIURE));
System.out.println("Comencen per 'R': " + cataleg.comptar(comencaPer('R')));
System.out.println("\nLlibres disponibles que comencen per 'P':");
for (Material m : cataleg.cercar(LLIBRE_DISPONIBLE.and(comencaPer('P')))) {
System.out.println(" " + m.descriure());
}
}
}Llibres disponibles: 2 Prestats: 2 Urgents: 2 Llibres barats i lliures: 2 Comencen per 'R': 2 Llibres disponibles que comencen per 'P': Llibre "Patrons de Disseny" (ref. 978-0000000002) - Erich Gamma, 1994
Tres detalls de disseny. Primer, DISPONIBLE fa servir una referència a mètode d'objecte arbitrari (Material::estaDisponible), la forma 3: el material que es passi a test serà el receptor de la crida. Segon, comencaPer(char) no és una constant sinó una fàbrica de predicats: un mètode que retorna comportament, una cosa que només té sentit des que les funcions són valors. Tercer, els criteris compostos es llegeixen com a frases del negoci (LLIBRE_BARAT_LLIURE) i cadascun es pot provar per separat amb JUnit sense tocar el catàleg.
Solució 2
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.presentacio.RebutConsola;
import java.util.function.*;
public class ReferenciesDemo {
/** Interficie funcional propia per al constructor de dos arguments. */
@FunctionalInterface
interface CreadorEmpleat {
Empleat crear(String nom, String identificador);
}
public static void main(String[] args) {
// ---- FORMA 1: metode ESTATIC ----
// Lambda equivalent: s -> Integer.parseInt(s)
Function<String, Integer> aEnter = Integer::parseInt;
int dies = aEnter.apply("20");
System.out.println("1. Dies transcorreguts llegits: " + dies);
// ---- FORMA 4: CONSTRUCTOR (es fa servir abans perque cal l'empleat) ----
// Lambda equivalent: (n, id) -> new Empleat(n, id)
CreadorEmpleat crearEmpleat = Empleat::new;
Empleat marta = crearEmpleat.crear("Marta Ruiz", "EMP-001");
System.out.println("4. Empleat creat: " + marta.getNom()
+ " (" + marta.getInicials() + ")");
Llibre javaEficac = new Llibre("Java Eficac", "Joshua Bloch",
"978-0000000001", 2018);
Prestec prestec = new Prestec(javaEficac, marta, 100);
// ---- FORMA 3: metode d'instancia d'un OBJECTE ARBITRARI del tipus ----
// Lambda equivalent: m -> m.getTitol()
// L'argument que es passi a apply sera el RECEPTOR de getTitol()
Function<Material, String> aTitol = Material::getTitol;
System.out.println("3. Titol extret: " + aTitol.apply(javaEficac));
// ---- FORMA 2: metode d'instancia d'un OBJECTE CONCRET ----
// Lambda equivalent: p -> rebut.imprimirRebut(p)
// L'objecte 'rebut' queda capturat; l'argument va al metode
RebutConsola rebut = new RebutConsola();
Consumer<Prestec> imprimir = rebut::imprimirRebut;
prestec.registrarDevolucio(dies);
System.out.println("2. Rebut impres mitjancant referencia a objecte concret:");
imprimir.accept(prestec);
}
}1. Dies transcorreguts llegits: 20 4. Empleat creat: Marta Ruiz (M.R.) 3. Titol extret: Java Eficac 2. Rebut impres mitjancant referencia a objecte concret: ======================================== REBUT DE DEVOLUCIO - BIBLIOTECH Referencia: PR-0001 Material: Java Eficac Empleat: Marta Ruiz Retard: 5 dies Multa: 1,25 EUR Gravetat: LLEU ========================================
La clau per distingir la forma 2 de la 3 és què hi ha a l'esquerra de ::: si és una variable (rebut), l'objecte està fixat i l'argument va al mètode; si és un nom de tipus (Material), l'argument es converteix en l'objecte sobre el qual es crida.
Solució 3
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.*;
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 InformeParametritzat {
/**
* Filtra, ordena, formata i lliura. Les quatre decisions son parametres:
* aquest metode no en coneix cap.
*/
public static void informe(Material[] materials,
Predicate<Material> filtre,
Comparator<Material> ordre,
Function<Material, String> format,
Consumer<String> sortida) {
// 1. Filtrar (sense llistes: array de mida maxima i retall final)
Material[] seleccio = new Material[materials.length];
int n = 0;
for (Material m : materials) {
if (filtre.test(m)) { seleccio[n++] = m; }
}
seleccio = Arrays.copyOf(seleccio, n);
// 2. Ordenar
Arrays.sort(seleccio, ordre);
// 3. Formatar i lliurar
for (Material m : seleccio) {
sortida.accept(format.apply(m));
}
}
public static void main(String[] args) {
Material[] cataleg = {
new Dvd("Refactoritzacio en directe", "DVD-0007", 95),
new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994),
new Llibre("Refactoritzacio", "Martin Fowler", "978-0000000003", 1999)
};
// ---------- (a) Nomes llibres, per titol, format breu, per consola ----------
System.out.println("=== (a) Llibres per titol ===");
informe(cataleg,
m -> m instanceof Llibre,
Comparator.comparing(Material::getTitol),
Material::getTitol, // referencia forma 3
System.out::println); // referencia forma 2
// ---------- (b) Tots, per multa descendent, amb prefix ----------
System.out.println("=== (b) Tots per multa descendent ===");
informe(cataleg,
m -> true,
Comparator.comparingDouble((Material m) -> m.calcularMulta(20)).reversed(),
m -> String.format("%-24s %6.2f EUR", m.getTitol(), m.calcularMulta(20)),
t -> System.out.println(" > " + t));
// ---------- (c) Termini curt, per tipus i titol, acumulant ----------
System.out.println("=== (c) Termini curt, acumulat ===");
StringBuilder acumulador = new StringBuilder();
informe(cataleg,
m -> m.getDiesPrestec() <= 7,
Comparator.comparing(Material::getTipus)
.thenComparing(Material::getTitol),
m -> String.format("%s | %s | termini %d dies | tarifa %.2f EUR%n",
m.getTipus(), m.getTitol(),
m.getDiesPrestec(), m.getTarifaDiaria()),
acumulador::append); // referencia forma 2
System.out.print(acumulador);
}
}=== (a) Llibres per titol === Java Eficac Patrons de Disseny Refactoritzacio === (b) Tots per multa descendent === > Refactoritzacio en directe 8,50 EUR > Java Magazine 1,30 EUR > Java Eficac 1,25 EUR > Patrons de Disseny 1,25 EUR > Refactoritzacio 1,25 EUR === (c) Termini curt, acumulat === DVD | Refactoritzacio en directe | termini 3 dies | tarifa 0,50 EUR Revista | Java Magazine | termini 7 dies | tarifa 0,10 EUR
El que és notable del cas (c): la sortida no va a la consola, sinó a un StringBuilder, i el canvi ha costat una referència a mètode (acumulador::append). El mètode informe no ha canviat ni en podria notar la diferència. Aquesta és exactament la propietat que farà trivials les proves del mòdul 11: substitueixes el Consumer per un que acumula i comproves el text, sense capturar System.out.
I observa l'anotació de tipus a (b): (Material m) -> m.calcularMulta(20) la porta perquè comparingDouble seguida de .reversed() no sempre infereix el tipus des de l'argument. A (a) i (c) no cal, perquè les referències a mètode (Material::getTitol) porten el tipus amb elles.
Conclusió
Ja tens el vocabulari complet de la programació amb comportament en Java. Saps que una interfície funcional és la que declara exactament un mètode abstracte, i que no compten per a aquell recompte els default, els static, els private ni els mètodes públics d'Object redeclarats —raó per la qual Comparator, que declara compare i equals, continua sent funcional—. Fas servir @FunctionalInterface sempre a les teves interfícies d'un mètode, sabent que no habilita res sinó que verifica, i que el seu valor és que l'error salti a la interfície i no als vint llocs que la fan servir amb lambdes.
Coneixes el catàleg de java.util.function i, millor que la llista, la seva lògica: Function transforma, Consumer consumeix, Supplier produeix, Predicate decideix; el prefix Bi afegeix un segon argument i Operator és la Function l'entrada i la sortida de la qual són del mateix tipus. I entens per què existeixen les desenes de variants primitives: evitar l'autoboxing, que en un bucle d'un milió d'iteracions significa un milió d'objectes que el recol·lector haurà de netejar.
Domines la composició, que és on aquestes peces deixen de ser curiositats i es converteixen en disseny: andThen i compose per encadenar funcions; and, or i negate per construir criteris de negoci complexos a partir de predicats simples amb nom propi; i Comparator.comparing().thenComparing().reversed(), que va convertir aquell comparador compost de sis línies amb condició intermèdia en una línia que es llegeix com una frase. I domines les quatre formes de referència a mètode amb el criteri per distingir-les: el que hi ha a l'esquerra de :: —una variable o un nom de tipus— determina si l'objecte està fixat o si l'argument passa a ser el receptor.
Sobretot, has escrit codi que rep comportament com a paràmetre: un GestorPrestecs que no sap què és un préstec urgent, ni quin text s'envia, ni per quin canal, i que tanmateix fa exactament el que se li demana a cada crida. Canviar qualsevol de les tres decisions no toca el seu codi, i provar-lo serà trivial. Recorda que tot això es torna encara més compacte amb l'API de Streams, on filter().map().forEach() substitueix el bucle sencer: és la lliçó 10-04, i hi arribaràs amb el concepte ja entès.
Queda un últim assumpte pendent del mòdul, i s'arrossega des del mòdul 3. classificarGravetat continua retornant les cadenes "LLEU" i "GREU", comparades amb equals per tot el projecte; Prestec.Incidencia desa la seva gravetat com un String que valida a mà al constructor; i res no impedeix que algú escrigui "greu" en minúscules, o "GRAVISSIM", i el compilador no dirà ni una paraula fins que la fallada aparegui en producció. A més, has escrit a mà equals, hashCode i toString a cada classe de dades, tres vegades les mateixes vint línies. A la lliçó 04-07, Enumeracions i Registres, tanques el mòdul amb les dues eines que resolen totes dues coses: els enum, que converteixen aquelles cadenes en un conjunt tancat de valors amb seguretat de tipus, exhaustivitat al switch i fins i tot comportament propi per constant; i els record, que generen sols el constructor, els accessors i els tres mètodes d'Object que tant et va costar escriure a 03-09.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
