La lliçó anterior va acabar amb una observació incòmoda: cada comparador del catàleg de BiblioTech té una línia de lògica útil embolcallada en cinc de cerimònia. I aquella cerimònia no aporta res, perquè tota ella és informació que el compilador ja té: sap quina interfície esperes (ho diu el paràmetre d'Arrays.sort), sap quin mètode cal implementar (Comparator només en té un) i sap de quin tipus són els dos arguments.
Les expressions lambda, incorporades a Java 8, permeten escriure només allò que aporta informació: els paràmetres i el cos. Van ser el canvi més gran en la sintaxi del llenguatge des de la seva creació i van transformar per complet la manera d'escriure Java: ordenar col·leccions, definir callbacks, llançar tasques concurrents i —sobretot— processar dades amb l'API de Streams. Aquesta lliçó t'ensenya què són realment (no allò que gairebé tothom es pensa), tota la seva sintaxi, les seves regles de captura, la diferència crítica de this enfront de les classes anònimes, i com fer-les servir a BiblioTech per canviar el comportament d'una classe sense tocar-ne el codi.
Contingut
- De la classe anònima a la lambda, pas a pas
- Sintaxi completa i formes abreujades
- Què és realment una lambda
- Inferència de tipus i el tipus objectiu
- Captura de variables efectivament finals
- Per què una lambda no pot modificar una variable local
thisdins d'una lambda: el contrast amb 04-04- On es fan servir avui les lambdes
- Una interfície funcional pròpia de BiblioTech
- Parametritzar el comportament:
FiltreMaterial - Llegibilitat: quan una lambda ha de ser un mètode
- Errors Habituals i Consells
- Exercicis
- De la classe anònima a la lambda, pas a pas
Partim del comparador per títol de 04-04. Li llevarem cerimònia en cinc passos, comprovant a cadascun quina informació es perd: cap.
Pas 0. La classe anònima completa.
Arrays.sort(cataleg, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
});Pas 1. Fora new Comparator<Material>(). El compilador ja sap que Arrays.sort(T[], Comparator<? super T>) espera un Comparator<Material>: li ho diu el tipus de l'array. Repetir-ho és redundant.
Arrays.sort(cataleg,
public int compare(Material a, Material b) {
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
);Pas 2. Fora @Override, public i el nom del mètode. Comparator té un sol mètode abstracte: compare. No hi ha cap ambigüitat possible sobre quin estàs implementant, així que anomenar-lo no aporta res.
Arrays.sort(cataleg,
(Material a, Material b) {
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
);Pas 3. Apareix la fletxa ->. Java necessita un separador entre els paràmetres i el cos. Aquest separador és l'operador lambda.
Arrays.sort(cataleg, (Material a, Material b) -> {
return a.getTitol().compareToIgnoreCase(b.getTitol());
});Això ja és una lambda vàlida i compila.
Pas 4. Fora els tipus dels paràmetres. El compilador sap que compare rep dos Material. Escriure-ho és opcional.
Pas 5. Fora les claus i el return. Si el cos és una sola expressió, el seu valor es retorna automàticament.
De sis línies a una. I la línia final conté exactament la informació que no era deduïble: els dos paràmetres i l'operació.
| Pas | Què s'elimina | Per què es pot |
|---|---|---|
| 1 | new Comparator<Material>() |
El tipus l'imposa el paràmetre del mètode |
| 2 | @Override public int compare |
La interfície té un únic mètode abstracte |
| 3 | — | S'afegeix -> com a separador |
| 4 | Tipus dels paràmetres | S'infereixen de la signatura del mètode |
| 5 | { } i return |
Cos d'una sola expressió |
flowchart TD
A["new Comparator<Material>() { @Override public int compare(Material a, Material b) { ... } }"] --> B["El tipus l'imposa Arrays.sort: fora new Comparator"]
B --> C["La interficie te un sol metode abstracte: fora el nom i els modificadors"]
C --> D["S'afegeix la fletxa com a separador: (Material a, Material b) -> { ... }"]
D --> E["Els tipus s'infereixen: (a, b) -> { ... }"]
E --> F["Cos d'una sola expressio: (a, b) -> a.getTitol().compareToIgnoreCase(b.getTitol())"]
- Sintaxi completa i formes abreujades
La forma general és:
I admet diverses abreviatures segons el nombre de paràmetres i la forma del cos:
| Forma | Exemple | Quan es pot fer servir |
|---|---|---|
| Sense paràmetres | () -> System.out.println("Hola") |
Sempre que el mètode no rebi res. Els parèntesis són obligatoris |
| Un paràmetre amb tipus | (Material m) -> m.getTitol() |
Sempre |
| Un paràmetre sense tipus | (m) -> m.getTitol() |
Quan el tipus es pot inferir |
| Un paràmetre sense parèntesis | m -> m.getTitol() |
Només amb un paràmetre i sense tipus declarat |
| Diversos paràmetres amb tipus | (Material a, Material b) -> ... |
Sempre |
| Diversos paràmetres sense tipus | (a, b) -> ... |
Quan es poden inferir. Tot o res |
| Cos d'expressió | (a, b) -> a.getTitol().compareTo(b.getTitol()) |
Cos d'una sola expressió; en retorna el valor |
| Cos de bloc | (a, b) -> { ... return x; } |
Diverses sentències; el return és obligatori si hi ha valor de retorn |
| Bloc sense retorn | m -> { System.out.println(m); } |
Quan el mètode retorna void |
Exemples equivalents de la mateixa lambda, de més explícita a més concisa:
// 1. Tot explicit
Comparator<Material> c1 = (Material a, Material b) -> {
return a.getTitol().compareToIgnoreCase(b.getTitol());
};
// 2. Sense tipus
Comparator<Material> c2 = (a, b) -> {
return a.getTitol().compareToIgnoreCase(b.getTitol());
};
// 3. Cos d'expressio (la forma habitual)
Comparator<Material> c3 = (a, b) -> a.getTitol().compareToIgnoreCase(b.getTitol());I les regles que cal memoritzar, amb els seus errors associats:
// Amb UN parametre, els parentesis son opcionals... si no declares el tipus
m -> m.getTitol() // valid
(m) -> m.getTitol() // valid
(Material m) -> m.getTitol() // valid
// Material m -> m.getTitol() // NO VALID: amb tipus, els parentesis son obligatoris
// Sense parametres, els parentesis SEMPRE
() -> System.out.println("hola") // valid
// -> System.out.println("hola") // NO VALID
// Els tipus son TOT o RES
(Material a, Material b) -> ... // valid
(a, b) -> ... // valid
// (Material a, b) -> ... // NO VALID: barreja
// Cos de bloc: return obligatori si hi ha valor
(a, b) -> { return a.getTitol().compareTo(b.getTitol()); } // valid
// (a, b) -> { a.getTitol().compareTo(b.getTitol()); } // NO COMPILA: falta returnHi ha una variant més, disponible des de Java 11: var als paràmetres, útil quan els vols anotar.
També és tot o res, i el seu únic motiu real d'existir és poder escriure (@NotNull var a, var b) -> .... En el dia a dia es fa servir poc.
- Què és realment una lambda
Aquí convé ser precís, perquè hi ha un malentès molt estès.
Una lambda NO és un punter a funció. Una lambda ÉS una instància d'una interfície funcional.
Una interfície funcional és una interfície amb exactament un mètode abstracte. Comparator ho és (compare), Runnable ho és (run), Prestable no ho és (en té quatre).
Quan escrius:
el que obtens és un objecte d'un tipus que implementa Comparator<Material>, el mètode compare del qual executa aquell cos. Ho pots comprovar:
Comparator<Material> perTitol = (a, b) -> a.getTitol().compareTo(b.getTitol());
System.out.println(perTitol instanceof Comparator); // true
System.out.println(perTitol.getClass().getName()); // App$$Lambda$14/0x...
System.out.println(perTitol.compare(llibre1, llibre2)); // s'invoca com qualsevol metodeLes conseqüències pràctiques que sigui un objecte són importants:
- La pots desar en una variable, en un camp o en un array.
- La pots passar com a argument i retornar-la des d'un mètode.
- Cal invocar el seu mètode pel seu nom:
perTitol.compare(a, b), noperTitol(a, b). - Només funciona amb interfícies funcionals. Si la interfície té dos mètodes abstractes, no hi ha lambda possible: cal una classe anònima (04-04).
Ara bé, hi ha una diferència tècnica amb les classes anònimes que convé conèixer. El compilador no genera un fitxer .class per cada lambda: emet una instrucció invokedynamic que crea la implementació en temps d'execució. Per això el nom de classe que veus a dalt és tan estrany, i per això milers de lambdes no encareixen l'arrencada com sí que ho feien milers de classes anònimes.
El catàleg d'interfícies funcionals que porta el JDK (Function, Predicate, Consumer, Supplier...), l'anotació @FunctionalInterface i les referències a mètodes són el contingut complet de la lliçó 04-06. Aquí treballaràs amb Comparator i amb interfícies funcionals pròpies.
- Inferència de tipus i el tipus objectiu
Si a la lambda no escrius els tipus, d'on surten? Del tipus objectiu (target type): el tipus que el context espera en aquell punt.
// Context 1: assignacio a una variable declarada
Comparator<Material> c = (a, b) -> a.getTitol().compareTo(b.getTitol());
// tipus objectiu: Comparator<Material> -> a i b son Material
// Context 2: argument d'un metode
Arrays.sort(cataleg, (a, b) -> a.getTitol().compareTo(b.getTitol()));
// tipus objectiu: Comparator<? super Material> -> a i b son Material
// Context 3: valor de retorn
public Comparator<Material> criteriPerTitol() {
return (a, b) -> a.getTitol().compareTo(b.getTitol());
}I aquí arriba la conseqüència que sorprèn: la mateixa lambda pot significar coses diferents segons on la posis.
public interface ValidadorTitol {
boolean validar(String titol);
}
public interface FiltreText {
boolean accepta(String text);
}ValidadorTitol v = t -> t.length() > 3; // implementa ValidadorTitol.validar
FiltreText f = t -> t.length() > 3; // implementa FiltreText.accepta
System.out.println(v.validar("Java")); // true
System.out.println(f.accepta("Java")); // true
System.out.println(v.getClass() == f.getClass()); // false: son tipus diferentsEl text de la lambda és idèntic i els objectes són de tipus completament diferents. Això explica dues coses:
Una lambda no té tipus propi. No existeix "el tipus de t -> t.length() > 3". El seu tipus el determina el context. Per això això no compila:
// var f = t -> t.length() > 3; // NO COMPILA: no hi ha tipus objectiu del qual inferir
// Object o = t -> t.length() > 3; // NO COMPILA: Object no es una interficie funcionalUna lambda ambigua tampoc no compila. Si un mètode està sobrecarregat amb dues interfícies funcionals de signatura compatible, el compilador no pot triar:
public void processar(ValidadorTitol v) { }
public void processar(FiltreText f) { }
// processar(t -> t.length() > 3); // NO COMPILA: referencia ambigua
processar((ValidadorTitol) t -> t.length() > 3); // es desambigua amb una conversio
- Captura de variables efectivament finals
Una lambda pot llegir:
- Els seus propis paràmetres.
- Els camps de la classe embolcallant (fins i tot mutables).
- Les variables locals del mètode, si són
finalo efectivament finals.
public Material[] materialsCars(Material[] materials, double llindar, int dies) {
// 'llindar' i 'dies' son parametres que no es reassignen: efectivament finals
Comparator<Material> perMulta =
(a, b) -> Double.compare(b.calcularMulta(dies), a.calcularMulta(dies));
Material[] copia = Arrays.copyOf(materials, materials.length);
Arrays.sort(copia, perMulta);
int quants = 0;
for (Material m : copia) {
if (m.calcularMulta(dies) >= llindar) { quants++; }
}
return Arrays.copyOf(copia, quants);
}És exactament la mateixa regla que a les classes locals (04-03) i anònimes (04-04), i pel mateix motiu: les variables locals viuen a la pila i moren amb el mètode; l'objecte lambda viu al monticle i el pot sobreviure. Java copia el valor dins de l'objecte.
I la distinció crucial es manté: la restricció afecta la variable, no l'objecte.
Empleat marta = new Empleat("Marta Ruiz", "EMP-001");
StringBuilder registre = new StringBuilder();
Runnable tasca = () -> {
marta.registrarPrestec(); // PERMES: es modifica l'objecte
registre.append("prestec registrat"); // PERMES: es modifica l'objecte
// marta = new Empleat(...); // PROHIBIT: es reassigna la variable
};
- Per què una lambda no pot modificar una variable local
Aquest és l'error que tothom comet la primera setmana:
public int comptarVencuts(Material[] materials, int dies) {
int vencuts = 0;
Consumidor tasca = m -> {
if (m.calcularDiesRetard(dies) > 0) {
vencuts++; // ERROR DE COMPILACIO
}
};
// ...
}La causa, ja coneguda, és la captura per valor: la lambda desa una còpia. Si la poguessis modificar, hi hauria dos valors descoordinats amb el mateix nom. Java ho prohibeix en compilació.
Però hi ha una segona raó, específica de les lambdes i més profunda: una lambda es pot executar en un altre fil. Si dos fils incrementessin una mateixa variable de pila —cosa que a més és físicament impossible, perquè cada fil té la seva pròpia pila—, el resultat seria indefinit. La restricció tanca d'arrel tota una categoria d'errors de concurrència que estudiaràs al mòdul 8.
Les tres alternatives correctes, de millor a pitjor:
// A. Un camp de la classe (viu al heap, es legitimament mutable)
public class ComptadorVencuts {
private int vencuts; // camp, no variable local
public void processar(Material[] materials, int dies) {
Consumidor tasca = m -> {
if (m.calcularDiesRetard(dies) > 0) { vencuts++; } // PERMES
};
for (Material m : materials) { tasca.acceptar(m); }
}
public int getVencuts() { return vencuts; }
}
// B. Un bucle normal, sense lambda: gairebe sempre el mes llegible
int vencuts = 0;
for (Material m : materials) {
if (m.calcularDiesRetard(dies) > 0) { vencuts++; }
}
// C. Un array d'un element (truc antic: evita'l)
int[] vencuts = {0};
Consumidor tasca = m -> { if (m.calcularDiesRetard(dies) > 0) { vencuts[0]++; } };L'opció B mereix un comentari: acumular sobre una variable és precisament el que un bucle fa bé. No forcis una lambda on un for és més clar. I per comptar i sumar sobre col·leccions hi haurà una forma molt millor: l'API de Streams, que és la lliçó 10-04.
this dins d'una lambda: el contrast amb 04-04
this dins d'una lambda: el contrast amb 04-04Aquest apartat és el més important de la lliçó per a qui ja coneix les classes anònimes, perquè el comportament és exactament el contrari.
Dins d'una lambda,
thisés la instància de la classe que la conté. Una lambda no introdueix un àmbit nou dethis.
Es diu que la lambda té àmbit lèxic: this, els noms de variables i els de mètodes signifiquen dins d'ella el mateix que significarien just a fora.
package com.nexussoftware.bibliotech.servei;
public class Auditor {
private final String origen = "Auditor";
public void comparar() {
// --- CLASSE ANONIMA ---
Runnable ambAnonima = new Runnable() {
@Override
public void run() {
System.out.println("ANONIMA -> this.getClass() = "
+ this.getClass().getSimpleName());
System.out.println("ANONIMA -> origen extern = "
+ Auditor.this.origen);
}
};
// --- LAMBDA ---
Runnable ambLambda = () -> {
System.out.println("LAMBDA -> this.getClass() = "
+ this.getClass().getSimpleName());
System.out.println("LAMBDA -> origen = " + origen);
// System.out.println(Auditor.this.origen); // valid, pero redundant
};
ambAnonima.run();
ambLambda.run();
}
public static void main(String[] args) {
new Auditor().comparar();
}
}ANONIMA -> this.getClass() = Auditor$1 ANONIMA -> origen extern = Auditor LAMBDA -> this.getClass() = Auditor LAMBDA -> origen = Auditor
La taula comparativa que has de recordar:
| Aspecte | Classe anònima | Lambda |
|---|---|---|
this |
La instància anònima | La instància de la classe embolcallant |
this.getClass() |
Externa$1 |
Externa |
| Accedir al camp de l'externa | Externa.this.camp |
camp directament |
| Pot tenir camps propis? | Sí | No |
| Pot ensombrir noms de l'externa? | Sí (àmbit nou) | No (àmbit lèxic) |
Genera un .class? |
Sí, un per anònima | No: invokedynamic |
| Pot implementar interfícies no funcionals? | Sí | No |
Aquest "no pot ensombrir" té una conseqüència concreta: dins d'una lambda no pots declarar una variable amb el mateix nom que una del mètode embolcallant.
public void exemple() {
String titol = "Java Eficac";
Runnable r1 = () -> {
// String titol = "Altre"; // NO COMPILA: 'titol' ja esta definida
System.out.println(titol);
};
Runnable r2 = new Runnable() {
@Override public void run() {
String titol = "Altre"; // SI QUE COMPILA: ambit nou, l'ensombreix
System.out.println(titol); // imprimeix "Altre"
}
};
}És el mateix motiu del canvi de this: la lambda no crea un àmbit nou, és codi que viu al mateix àmbit lèxic. I això és la principal font de bugs en migrar codi antic d'anònimes a lambdes: si l'anònima feia servir this esperant referir-se a si mateixa, la lambda equivalent farà una cosa diferent sense donar cap error de compilació.
- On es fan servir avui les lambdes
Tres escenaris que ja pots fer servir amb el que saps, més un que arriba després.
Comparadors. El cas que has vist:
Arrays.sort(cataleg, (a, b) -> a.getTitol().compareToIgnoreCase(b.getTitol()));
Arrays.sort(cataleg, (a, b) -> Integer.compare(a.getDiesPrestec(), b.getDiesPrestec()));
Arrays.sort(cataleg, (a, b) -> Double.compare(b.calcularMulta(20), a.calcularMulta(20)));Tres criteris, tres línies. Compara-ho amb les quinze línies per criteri de 04-04.
Runnable: una tasca sense arguments ni resultat.
Aquí només l'executes directament. El seu ús real —passar-lo a un fil perquè s'executi en paral·lel— és el mòdul 8.
Callbacks propis. L'OientDevolucio de 04-04 té un sol mètode abstracte, així que és una interfície funcional i admet lambda:
// Abans (04-04): 6 linies de classe anonima
gestor.setOient(new OientDevolucio() {
@Override
public void alRetornar(Prestec prestec, double multa) {
System.out.printf("REBUT %s: %.2f EUR%n", prestec.getReferencia(), multa);
}
});
// Ara: 1 linia
gestor.setOient((prestec, multa) ->
System.out.printf("REBUT %s: %.2f EUR%n", prestec.getReferencia(), multa));I on les lambdes brillen de debò: l'API de Streams. Operacions com filtrar, transformar i agrupar col·leccions senceres en una sola expressió encadenada. És la lliçó 10-04, i no la farem servir abans.
- Una interfície funcional pròpia de BiblioTech
Definir les teves pròpies interfícies funcionals és el que converteix les lambdes en una eina de disseny. Comencem per una regla de tarifa configurable.
Avui, la tarifa d'un material està fixada a la seva classe: Llibre cobra 0,25 €/dia, sempre. Però Nexus Software vol aplicar campanyes: mitja tarifa a l'agost, tarifa doble per a materials molt demandats, tarifa zero per a becaris. Afegir un if per campanya dins de Material seria exactament l'olor de disseny que vas aprendre a evitar a 03-06.
La solució: fer que la regla sigui un paràmetre.
package com.nexussoftware.bibliotech.domini;
/**
* Regla de calcul de la tarifa diaria aplicable a un material.
*
* <p>Es una interficie funcional: te un unic metode abstracte, aixi que
* qualsevol lambda amb la forma (Material, int) -> double la implementa.</p>
*/
@FunctionalInterface
public interface ReglaTarifa {
/**
* @param material material sobre el qual es calcula
* @param diesRetard dies de retard acumulats
* @return euros per dia de retard que s'han d'aplicar
*/
double tarifaPer(Material material, int diesRetard);
}L'anotació @FunctionalInterface fa que el compilador verifiqui que hi ha exactament un mètode abstracte; si algú n'afegeix un segon, l'error apareix aquí i no als vint llocs que fan servir lambdes. El seu significat complet és 04-06.
Ara un servei que la fa servir:
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.Material;
import com.nexussoftware.bibliotech.domini.ReglaTarifa;
/** Calcula multes aplicant una regla de tarifa configurable. */
public class CalculadoraMultes {
public static final double MULTA_MAXIMA = 20.0;
private final ReglaTarifa regla;
/** La regla s'injecta: la calculadora no sap quina es. */
public CalculadoraMultes(ReglaTarifa regla) {
this.regla = (regla != null)
? regla
: (m, dies) -> m.getTarifaDiaria(); // regla per defecte, com a lambda
}
public double calcular(Material material, int diesTranscorreguts) {
int retard = material.calcularDiesRetard(diesTranscorreguts);
double tarifa = regla.tarifaPer(material, retard);
return Math.min(retard * tarifa, MULTA_MAXIMA);
}
}I les campanyes, cadascuna en una línia:
Material dvd = new Dvd("Refactoritzacio en directe", "DVD-0007", 95);
// 1. Regla estandard: la tarifa propia de cada material
CalculadoraMultes estandard = new CalculadoraMultes((m, dies) -> m.getTarifaDiaria());
// 2. Campanya d'agost: mitja tarifa
CalculadoraMultes agost = new CalculadoraMultes((m, dies) -> m.getTarifaDiaria() / 2);
// 3. Tarifa progressiva: es duplica a partir del vuite dia de retard
CalculadoraMultes progressiva = new CalculadoraMultes((m, dies) ->
dies > 7 ? m.getTarifaDiaria() * 2 : m.getTarifaDiaria());
// 4. Amnistia: sense multes
CalculadoraMultes amnistia = new CalculadoraMultes((m, dies) -> 0.0);
System.out.printf("Estandard: %.2f EUR%n", estandard.calcular(dvd, 20));
System.out.printf("Agost: %.2f EUR%n", agost.calcular(dvd, 20));
System.out.printf("Progressiva: %.2f EUR%n", progressiva.calcular(dvd, 20));
System.out.printf("Amnistia: %.2f EUR%n", amnistia.calcular(dvd, 20));El que ha passat mereix aturar-s'hi a mirar-ho: quatre polítiques de multa diferents, i ni Material ni Dvd ni CalculadoraMultes no han canviat ni una sola línia. El comportament variable es passa com si fos una dada. Això té nom —el patró Strategy— i el formalitzaràs a 12-02; aquí l'has escrit gairebé sense adonar-te'n.
- Parametritzar el comportament:
FiltreMaterial
FiltreMaterialSegon exemple, amb el problema clàssic de "dona'm els materials que compleixen X".
Sense lambdes, cada criteri nou obliga a afegir un mètode al catàleg: cercarPerTitol, cercarDisponibles, cercarPerTipus, cercarAmbMultaMajorQue... La classe creix sense fi i cada criteri nou la modifica.
package com.nexussoftware.bibliotech.domini;
/** Criteri de seleccio de materials. Interficie funcional. */
@FunctionalInterface
public interface FiltreMaterial {
/** @return true si el material compleix el criteri. */
boolean accepta(Material material);
}package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.FiltreMaterial;
import com.nexussoftware.bibliotech.domini.Material;
import java.util.Arrays;
public class Cataleg {
private final Material[] materials;
public Cataleg(Material[] materials) {
this.materials = Arrays.copyOf(materials, materials.length);
}
/**
* Un unic metode de cerca, valid per a QUALSEVOL criteri.
* El comportament arriba com a parametre.
*/
public Material[] cercar(FiltreMaterial filtre) {
Material[] resultat = new Material[materials.length];
int n = 0;
for (Material m : materials) {
if (filtre.accepta(m)) {
resultat[n++] = m;
}
}
return Arrays.copyOf(resultat, n); // el modul 5 ho fara molt millor
}
/** Compta quants compleixen el criteri, sense construir l'array. */
public int comptar(FiltreMaterial filtre) {
int n = 0;
for (Material m : materials) {
if (filtre.accepta(m)) { n++; }
}
return n;
}
}I ara, criteris il·limitats sense tocar Cataleg:
Cataleg cataleg = new Cataleg(new Material[] {
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)
});
System.out.println("Disponibles: "
+ cataleg.comptar(m -> m.estaDisponible()));
System.out.println("Nomes llibres: "
+ cataleg.comptar(m -> m instanceof Llibre));
System.out.println("Termini curt (<= 7 dies): "
+ cataleg.comptar(m -> m.getDiesPrestec() <= 7));
System.out.println("Amb 'Refactoritz' al titol:");
for (Material m : cataleg.cercar(m -> m.getTitol().contains("Refactoritz"))) {
System.out.println(" " + m.descriure());
}
// Un filtre desat en una variable i reutilitzat
FiltreMaterial cars = m -> m.calcularMulta(20) > 5.0;
System.out.println("Multa alta als 20 dies: " + cataleg.comptar(cars));Disponibles: 5 Nomes llibres: 3 Termini curt (<= 7 dies): 2 Amb 'Refactoritz' al titol: Llibre "Refactoritzacio" (ref. 978-0000000003) - Martin Fowler, 1999 DVD "Refactoritzacio en directe" (ref. DVD-0007) - 95 min Multa alta als 20 dies: 1
Aquest és el canvi de mentalitat del mòdul: el comportament és una dada. Abans passaves números i cadenes; ara passes trossos de lògica. El JDK ja porta una interfície idèntica a FiltreMaterial —es diu Predicate— i la veuràs a 04-06, juntament amb la manera de combinar filtres (and, or, negate).
- Llegibilitat: quan una lambda ha de ser un mètode
Les lambdes són concises, i la concisió mal aplicada produeix codi il·legible. Tres criteris pràctics:
Regla de les tres línies. Si el cos de la lambda passa de tres línies, extreu-lo a un mètode amb nom.
// MALAMENT: logica de negoci enterrada en una lambda llarga
Material[] critics = cataleg.cercar(m -> {
int retard = m.calcularDiesRetard(20);
if (retard == 0) { return false; }
double multa = m.calcularMulta(20);
boolean car = multa >= Material.MULTA_MAXIMA * 0.5;
boolean tipusSensible = m.getDiesPrestec() <= 3;
return car || (tipusSensible && retard > 5);
});
// BE: la condicio te nom i es pot provar per separat
Material[] critics = cataleg.cercar(m -> esCritic(m, 20));
private static boolean esCritic(Material m, int dies) {
int retard = m.calcularDiesRetard(dies);
if (retard == 0) { return false; }
boolean car = m.calcularMulta(dies) >= Material.MULTA_MAXIMA * 0.5;
boolean tipusSensible = m.getDiesPrestec() <= 3;
return car || (tipusSensible && retard > 5);
}La versió bona guanya tres coses: la condició té nom (esCritic explica la intenció millor que sis línies de booleans), és reutilitzable en altres criteris, i és provable de manera aïllada amb JUnit al mòdul 11.
Si es repeteix, posa-li nom. Una lambda que es fa servir en tres llocs hauria de ser una constant:
public static final FiltreMaterial DISPONIBLES = m -> m.estaDisponible();
public static final FiltreMaterial NOMES_LLIBRES = m -> m instanceof Llibre;
public static final Comparator<Material> PER_TITOL =
(a, b) -> a.getTitol().compareToIgnoreCase(b.getTitol());Posa bons noms als paràmetres. (a, b) està bé per a un comparador genèric; m està bé per a un material en un context obvi. Però en una lambda de dues línies, (prestec, multa) es llegeix infinitament millor que (p, x). La concisió no justifica noms críptics.
Errors Habituals i Consells
Creure que una lambda és una funció solta. És un objecte que implementa una interfície funcional. Per això s'invoca pel nom del mètode (filtre.accepta(m), no filtre(m)) i per això no la pots assignar a var ni a Object.
Intentar una lambda sobre una interfície no funcional. Prestable p = () -> true; no compila: Prestable té quatre mètodes abstractes. Només interfícies amb exactament un.
Descuidar el return en un cos de bloc. Si obres claus i el mètode retorna alguna cosa, el return és obligatori. (a, b) -> { a.compareTo(b); } no compila.
Posar punt i coma després d'un cos d'expressió. m -> m.getTitol(); dins d'una crida a un mètode és un error. Amb cos d'expressió no hi ha ; intern; amb cos de bloc, cada sentència porta el seu.
Modificar una variable local capturada. Prohibit. Fes servir un camp o un bucle. I no recorris a l'array d'un element llevat que no hi hagi alternativa.
Confondre this. En una lambda, this és la classe embolcallant; en una anònima, l'anònima. És l'error número u en convertir codi antic, i no dona error de compilació: simplement fa una altra cosa.
Barrejar tipus als paràmetres. (Material a, b) -> ... no compila. O tots amb tipus o cap.
Lambdes quilomètriques. Una lambda de vint línies dins d'una crida a un mètode és pitjor que la classe anònima que substitueix. Extreu-ne un mètode.
Consell: anota sempre @FunctionalInterface a les teves interfícies d'un sol mètode. Documenta la intenció i evita que un company hi afegeixi un segon mètode abstracte i trenqui totes les lambdes del projecte.
Consell: aprèn a llegir els errors d'inferència. Quan una lambda no compila, el missatge sol apuntar al tipus objectiu, no a la lambda. Si t'hi perds, escriu temporalment els tipus dels paràmetres: l'error es torna molt més clar.
Consell: desa les lambdes reutilitzades com a constants static final. Es crea una sola instància, el nom documenta el criteri i les traces d'error són més llegibles.
Exercicis
Exercici 1: reescriure els comparadors de 04-04
Pren el mètode mostrarOrdenat de l'exercici 1 de la lliçó 04-04 i reescriu-lo fent servir lambdes en lloc de classes anònimes. Compta les línies abans i després. Afegeix un quart criteri, "tipus", que ordeni per tipus i desempati per títol.
Exercici 2: ReglaAvis, una interfície funcional pròpia
Crea una interfície funcional ReglaAvis amb el mètode String redactar(Prestec prestec, double multa). Crea una classe ServeiAvisos amb un mètode enviar(Prestec, double, ReglaAvis) que imprimeixi el text redactat. Passa-li tres lambdes diferents: un avís formal, un de breu i un que només escrigui alguna cosa si la multa supera 5 €. Explica quin avantatge té això enfront de tres mètodes enviarFormal, enviarBreu i enviarSiAlta.
Exercici 3: captura i this
Escriu una classe Registrador amb un camp private int avisos i un mètode processar(Material[] materials, int dies) que:
- Declari una variable local
int comptadorLocal = 0i intenti incrementar-la dins d'una lambda. Comprova l'error i comenta'l. - Incrementi en el seu lloc el camp
avisosdes de la lambda. - Imprimeixi
this.getClass().getSimpleName()dins de la lambda i n'expliqui el resultat comparant-lo amb el que retornaria una classe anònima.
Solucions
Solució 1
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.*;
import java.util.Arrays;
import java.util.Comparator;
public class OrdenadorCataleg {
public static void mostrarOrdenat(Material[] materials, String criteri, int dies) {
Material[] copia = Arrays.copyOf(materials, materials.length);
Comparator<Material> comparador = switch (criteri) {
case "titol" -> (a, b) -> a.getTitol().compareToIgnoreCase(b.getTitol());
case "termini" -> (a, b) -> Integer.compare(a.getDiesPrestec(),
b.getDiesPrestec());
case "multa" -> (a, b) -> Double.compare(b.calcularMulta(dies),
a.calcularMulta(dies));
// Criteri compost: cos de bloc, perque hi ha una condicio intermedia
case "tipus" -> (a, b) -> {
int perTipus = a.getTipus().compareTo(b.getTipus());
if (perTipus != 0) {
return perTipus;
}
return a.getTitol().compareToIgnoreCase(b.getTitol());
};
default -> (a, b) -> 0;
};
Arrays.sort(copia, comparador);
System.out.println("--- Ordenat per " + criteri + " ---");
for (Material m : copia) {
System.out.printf(" %-10s %-24s termini %2d multa %5.2f EUR%n",
m.getTipus(), m.getTitol(),
m.getDiesPrestec(), m.calcularMulta(dies));
}
}
}--- Ordenat per tipus --- DVD Refactoritzacio en directe termini 3 multa 8,50 EUR Llibre Java Eficac termini 15 multa 1,25 EUR Llibre Patrons de Disseny termini 15 multa 1,25 EUR Revista Java Magazine termini 7 multa 1,30 EUR
El recompte: el switch amb classes anònimes ocupava 30 línies per a quatre criteris; amb lambdes n'ocupa 14 incloent-hi el criteri compost, que és l'únic que necessita cos de bloc. I el guany no és només de mida: el criteri d'ordenació ara es llegeix en una sola línia al costat del seu case, sense que la vista hagi de saltar per damunt de @Override public int compare(...) quatre vegades.
Fixa't en els dos estils convivint: els tres primers fan servir cos d'expressió (sense claus, sense return) i el quart fa servir cos de bloc perquè té una condició intermèdia. A 04-06 veuràs que fins i tot aquell cas es pot escriure en una línia amb Comparator.comparing(...).thenComparing(...).
Solució 2
package com.nexussoftware.bibliotech.domini;
/** Redacta el text d'un avis de devolucio. Interficie funcional. */
@FunctionalInterface
public interface ReglaAvis {
/**
* @return text a enviar, o null / cadena buida si no cal avisar
*/
String redactar(Prestec prestec, double multa);
}package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.Prestec;
import com.nexussoftware.bibliotech.domini.ReglaAvis;
/** Envia avisos aplicant la regla de redaccio que se li indiqui. */
public class ServeiAvisos {
private int enviats;
public void enviar(Prestec prestec, double multa, ReglaAvis regla) {
String text = regla.redactar(prestec, multa);
if (text == null || text.isBlank()) {
System.out.println("(sense avis per a " + prestec.getReferencia() + ")");
return;
}
enviats++; // camp: la lambda no el toca, el toca el servei
System.out.println(text);
}
public int getEnviats() { return enviats; }
}ServeiAvisos servei = new ServeiAvisos();
// 1. Avis formal
ReglaAvis formal = (p, multa) -> String.format(
"Benvolgut/uda %s:%n El material \"%s\" (ref. %s) presenta %d dies de retard.%n"
+ " Quantitat a abonar: %.2f EUR.%n Atentament, BiblioTech - Nexus Software.",
p.getNomEmpleat(), p.getTitolMaterial(), p.getReferencia(),
p.calcularDiesRetard(), multa);
// 2. Avis breu
ReglaAvis breu = (p, multa) ->
String.format("[%s] %s: %.2f EUR", p.getReferencia(), p.getTitolMaterial(), multa);
// 3. Nomes si la multa es alta: retorna null i el servei no envia res
ReglaAvis nomesAltes = (p, multa) -> multa > 5.0
? String.format("URGENT %s: %.2f EUR pendents", p.getReferencia(), multa)
: null;
Empleat marta = new Empleat("Marta Ruiz", "EMP-001");
Prestec p1 = new Prestec(
new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018), marta, 100, 20);
Prestec p2 = new Prestec(
new Dvd("Refactoritzacio en directe", "DVD-0007", 95), marta, 100, 20);
servei.enviar(p1, 1.25, breu);
servei.enviar(p1, 1.25, nomesAltes);
servei.enviar(p2, 8.50, nomesAltes);
servei.enviar(p2, 8.50, formal);
System.out.println("Avisos enviats: " + servei.getEnviats());[PR-0001] Java Eficac: 1,25 EUR (sense avis per a PR-0001) URGENT PR-0002: 8,50 EUR pendents Benvolgut/uda Marta Ruiz: El material "Refactoritzacio en directe" (ref. PR-0002) presenta 17 dies de retard. Quantitat a abonar: 8,50 EUR. Atentament, BiblioTech - Nexus Software. Avisos enviats: 3
Avantatges enfront de tres mètodes enviarFormal, enviarBreu i enviarSiAlta:
| Aspecte | Tres mètodes | Un mètode + ReglaAvis |
|---|---|---|
| Afegir un format nou | Modificar ServeiAvisos |
Escriure una lambda allà on es faci servir |
| Lògica d'enviament (comptador, control de buit) | Duplicada tres vegades | Escrita una vegada |
| Composició | Impossible combinar formats | Una regla pot embolcallar-ne una altra |
| Proves | Cal provar tres mètodes | Es prova l'enviament una vegada i les regles per separat |
| Configuració externa | Impossible | La regla es pot triar en temps d'execució |
És el principi d'obert/tancat: la classe queda oberta a extensió (regles noves) i tancada a modificació (el seu codi no canvia). El formalitzaràs a 12-02.
Solució 3
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.FiltreMaterial;
import com.nexussoftware.bibliotech.domini.Material;
public class Registrador {
private int avisos; // CAMP: viu al heap, es mutable
public void processar(Material[] materials, int dies) {
int comptadorLocal = 0; // VARIABLE LOCAL: viu a la pila
FiltreMaterial esVencut = m -> {
// comptadorLocal++; // (1) NO COMPILA:
// "local variables referenced from a lambda expression
// must be final or effectively final"
//
// La lambda desa una COPIA del valor. Si la pogues modificar,
// hi hauria dos valors diferents amb el mateix nom: el del metode
// i el de l'objecte lambda. A mes, la lambda es podria executar en
// un altre fil, que te la seva propia pila (modul 8).
if (m.calcularDiesRetard(dies) > 0) {
avisos++; // (2) SI QUE COMPILA: es un camp de la classe
return true;
}
return false;
};
for (Material m : materials) {
esVencut.accepta(m);
}
// (3) this dins de la lambda
Runnable informe = () -> System.out.printf(
"%s ha registrat %d avisos. this = %s%n",
getClass().getSimpleName(), avisos,
this.getClass().getSimpleName());
informe.run();
System.out.println("comptadorLocal continua valent " + comptadorLocal);
}
public int getAvisos() { return avisos; }
public static void main(String[] args) {
Material[] cataleg = {
new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Dvd("Refactoritzacio en directe", "DVD-0007", 95)
};
new Registrador().processar(cataleg, 20);
}
}Les tres respostes:
comptadorLocal++no compila. La lambda captura el valor per còpia perquè l'objecte pot sobreviure al mètode, i modificar la còpia produiria dos valors descoordinats. El compilador ho atura abans que el problema existeixi.avisos++sí que compila.avisosés un camp d'instància: viu al monticle juntament amb l'objecteRegistrador, i la lambda hi accedeix a través dethis, que sí que té capturat. La mutabilitat pertany a l'objecte, no a la variable capturada. (Amb diversos fils això deixaria de ser segur sense sincronització: mòdul 8.)this.getClass()retornaRegistrador. La lambda no crea un àmbit propi dethis: el seuthisés el del mètode embolcallant. Amb una classe anònima, la mateixa línia hauria imprèsRegistrador$1. I fixa't que dins de la lambdagetClass()sense prefix funciona igual quethis.getClass(), una cosa que en una anònima hauria requeritRegistrador.this.getClass().
Conclusió
Has fet el recorregut complet de la classe anònima a la lambda, llevant cerimònia pas a pas i comprovant que en cap moment no es perd informació: el tipus l'imposa el context, el nom del mètode és únic, els tipus dels paràmetres s'infereixen i el return sobra quan el cos és una expressió. De sis línies a una, i la línia que queda conté exactament el que no era deduïble.
Domines la sintaxi completa amb totes les seves formes: parèntesis obligatoris quan no hi ha paràmetres o quan es declaren els tipus, opcionals amb un únic paràmetre sense tipus; tipus tot o res; cos d'expressió sense claus ni return enfront de cos de bloc amb return obligatori. I saps què és realment una lambda: no un punter a funció, sinó una instància d'una interfície funcional —una interfície amb exactament un mètode abstracte—, un objecte de ple dret que pots desar, passar i retornar, i el mètode del qual cal invocar pel seu nom.
Entens la inferència per tipus objectiu i la seva conseqüència més cridanera: la mateixa lambda escrita paraula per paraula pot produir objectes de tipus completament diferents segons on la col·loquis, i per això no existeix "el tipus d'una lambda" ni es pot assignar a var o Object. Coneixes la regla de captura de variables efectivament finals, el seu motiu doble —la còpia per valor i la possibilitat d'executar-se en un altre fil— i les alternatives correctes quan de debò necessites acumular: un camp, o simplement un bucle. I tens molt clara la diferència que es paga més cara en migrar codi antic: en una lambda, this és la classe embolcallant, just a l'inrevés que en una classe anònima, i aquell canvi no produeix cap error de compilació.
Sobretot, has canviat de mentalitat: el comportament és una dada. CalculadoraMultes aplica quatre polítiques de tarifa diferents sense que Material ni ella mateixa canviïn ni una línia, i Cataleg respon a qualsevol criteri de cerca imaginable amb un únic mètode cercar(FiltreMaterial). Això és el que les lambdes aporten de debò: classes obertes a extensió i tancades a modificació.
Però has escrit dues interfícies funcionals pròpies —FiltreMaterial i ReglaAvis— que s'assemblen sospitosament a una cosa que hauria de ser estàndard. I ho és: el JDK porta un catàleg complet d'interfícies funcionals al paquet java.util.function, amb Predicate per filtrar, Function per transformar, Consumer per consumir i Supplier per produir, a més de formes de combinar-les (and, or, negate, andThen) que converteixen dos criteris en un. I hi ha una cosa més: 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. A la lliçó 04-06, Interfícies Funcionals i Referències a Mètodes, veuràs el catàleg sencer, l'anotació @FunctionalInterface i què comprova exactament, la composició de funcions i comparadors, i les quatre formes de referència a mètode que redueixen m -> m.getTitol() a Material::getTitol.
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
