La lliçó anterior va acabar amb una molèstia legítima: Cataleg.PerTitol és una classe completa —declaració, @Override, cos, nom i un lloc a l'espai de noms— per a una sola línia de lògica que es fa servir exactament en un lloc. Quan el codi que vols escriure cap en una línia i la cerimònia per embolcallar-lo n'ocupa cinc, alguna cosa sobra.
Les classes anònimes eliminen aquella cerimònia. Permeten declarar una implementació i instanciar-la a la mateixa expressió, al punt exacte on cal i sense posar-li nom. Van ser durant quinze anys el mecanisme estàndard de Java per passar comportament a un mètode: cada Comparator, cada listener d'interfície gràfica, cada Runnable s'escrivia així. Avui les lambdes han ocupat bona part d'aquell terreny, però les classes anònimes continuen sent l'única opció en uns quants escenaris concrets, i sobretot continuen apareixent a tot el codi Java existent: llegir-les amb desimboltura no és opcional. A més, entendre-les a fons és la millor preparació possible per a la lliçó 04-05, perquè una lambda no és res més que una classe anònima a la qual s'ha llevat tot el que era prescindible.
Contingut
- Què és una classe anònima
- La sintaxi, dissecada
- Què pot i què no pot fer
- El bloc d'inicialització d'instància
- Mètodes propis: existeixen, però gairebé no serveixen
- Captura de variables efectivament finals
thisdins d'una classe anònima- Ús clàssic 1: un
Comparatorper al catàleg - Ús clàssic 2: una crida de retorn
- Ús clàssic 3: estendre puntualment una classe
- Quan continua sent l'opció correcta i quan fer servir una lambda
- El "double brace initialization" i per què evitar-lo
- Pes al bytecode i llegibilitat
- Taula de decisió final
- Errors Habituals i Consells
- Exercicis
- Què és una classe anònima
Una classe anònima és una classe sense nom que es declara i s'instancia en una sola expressió. Pot ser:
- Una implementació d'una interfície, o
- Una subclasse d'una classe (concreta o abstracta).
Compara les dues maneres d'obtenir un comparador per títol:
// --- FORMA 1: classe imbricada amb nom (04-03) ---
public static class PerTitol implements Comparator<Material> {
@Override
public int compare(Material a, Material b) {
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
}
// ... i en un altre lloc:
Arrays.sort(cataleg, new Cataleg.PerTitol());// --- FORMA 2: classe anonima, aqui mateix ---
Arrays.sort(cataleg, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
});La segona forma no declara cap classe amb nom, no ocupa un lloc a l'espai de noms de Cataleg i posa la lògica exactament on es fa servir. Qui llegeix Arrays.sort(...) veu a l'instant per quin criteri s'ordena, sense anar a buscar un altre fitxer.
L'expressió new Comparator<Material>() { ... } fa tres coses alhora:
- Declara una classe nova que implementa
Comparator<Material>. - La instancia.
- Retorna aquella instància com a valor de l'expressió.
- La sintaxi, dissecada
Aquesta és la part que convé mirar amb lupa, perquè té una estructura peculiar:
Comparator<Material> perTitol = new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
};Peça per peça:
| Fragment | Què és |
|---|---|
Comparator<Material> perTitol = |
Declaració normal. El tipus és la interfície, no la classe anònima (que no té nom) |
new |
Es crearà un objecte |
Comparator<Material> |
El tipus que s'implementa (si és interfície) o s'estén (si és classe) |
() |
Arguments del constructor. Buits si és una interfície; si estens una classe, aquí van els seus |
{ ... } |
Cos de la classe anònima: els seus mètodes i camps |
; |
Punt i coma final. El més oblidat de Java |
Aquest punt i coma final és la font d'error número u amb classes anònimes. La raó és que tota la construcció és una expressió dins d'una sentència, no una declaració de classe. Pensa-hi així: si escrius int x = 5;, el ; és evident. Aquí passa el mateix; el que despista és que enmig hi ha una clau de tancament, i l'ull la llegeix com el final d'una classe.
// Amb el punt i coma final (correcte):
Comparator<Material> c = new Comparator<Material>() { ... };
// Sense (error de compilacio):
Comparator<Material> c = new Comparator<Material>() { ... }Quan l'anònima va com a argument d'un mètode, el parèntesi de tancament apareix després de la clau, cosa que resulta xocant al principi:
Arrays.sort(cataleg, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
}); // clau, parentesi, punt i comaAquest }); és la signatura visual d'una classe anònima en codi Java. Aprèn a reconèixer-lo.
- Què pot i què no pot fer
| Pot | No pot |
|---|---|
| Implementar una interfície | Implementar diverses interfícies |
| Estendre una classe (concreta o abstracta) | Estendre una classe i implementar interfícies alhora |
| Tenir camps propis | Tenir un constructor propi |
| Tenir un bloc d'inicialització d'instància | Ser abstract, static o final |
| Declarar mètodes nous | Que aquells mètodes es cridin des de fora (apartat 5) |
| Accedir als membres de la classe externa | Ser reutilitzada: és d'un sol ús |
| Capturar variables efectivament finals | Modificar aquelles variables |
La limitació central és no tenir constructor. I la raó és senzilla: un constructor porta el nom de la seva classe, i aquesta classe no té nom. No hi ha manera d'escriure'l.
Comparator<Material> c = new Comparator<Material>() {
// Comparator() { } // IMPOSSIBLE: la classe no te nom
// AnonimaSenseNom() { } // IMPOSSIBLE: no es diu aixi
@Override public int compare(Material a, Material b) { return 0; }
};I si estens una classe que sí que té constructor? Llavors li pots passar arguments als parèntesis del new, que es reenvien al constructor de la superclasse:
// Esten Empleat fent servir el seu constructor de dos arguments
Empleat becari = new Empleat("Nuria Vidal", "EMP-003") {
@Override
public boolean potPrendrePrestat() {
return false; // els becaris en practiques no prenen materials en prestec
}
};Aquí ("Nuria Vidal", "EMP-003") no és el constructor de l'anònima: és la invocació del constructor d'Empleat. L'anònima rep un constructor implícit que es limita a reenviar-los.
- El bloc d'inicialització d'instància
Si no hi ha constructor, com inicialitzes un camp amb lògica? Amb un bloc d'inicialització d'instància: un bloc { ... } solt al cos de la classe, que s'executa en construir l'objecte, just després del constructor de la superclasse.
Comparator<Material> perTitolAmbLog = new Comparator<Material>() {
private int comparacions; // camp propi
private final String etiqueta; // camp final
{ // BLOC D'INICIALITZACIO D'INSTANCIA (fa de constructor)
etiqueta = "cmp-" + System.nanoTime();
comparacions = 0;
System.out.println("Comparador creat: " + etiqueta);
}
@Override
public int compare(Material a, Material b) {
comparacions++;
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
};Aquest mecanisme no és exclusiu de les anònimes —qualsevol classe el pot tenir—, però aquí és l'única via per executar codi d'inicialització. L'ordre complet, ampliant el que vas veure a 03-04, és:
flowchart TD
A["new Anonima(...)"] --> B["constructor de la superclasse"]
B --> C["inicialitzadors de camps, en ordre d'escriptura"]
C --> D["bloc d'inicialitzacio d'instancia"]
D --> E["cos del constructor implicit (buit)"]
E --> F["objecte a punt"]
A la pràctica, si la teva classe anònima necessita un bloc d'inicialització complex, considera-ho un senyal: probablement mereix ser una classe amb nom.
- Mètodes propis: existeixen, però gairebé no serveixen
Una classe anònima pot declarar mètodes que no són a la interfície o a la classe base. Compilen. Però hi ha un parany:
Comparator<Material> comparador = new Comparator<Material>() {
private int comparacions;
@Override
public int compare(Material a, Material b) {
comparacions++;
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
/** Metode propi: NO es a Comparator. */
public int getComparacions() {
return comparacions;
}
};
Arrays.sort(cataleg, comparador);
// comparador.getComparacions(); // NO COMPILAerror: cannot find symbol symbol: method getComparacions() location: variable comparador of type Comparator<Material>
El motiu és la regla de polimorfisme de 03-06: el tipus declarat decideix què pots cridar. La variable és de tipus Comparator<Material>, i Comparator no té getComparacions(). I no la pots declarar del tipus real, perquè el tipus real no té nom.
Hi ha un escletxa, que serveix per entendre el mecanisme encara que no la facis servir: invocar el mètode sobre la mateixa expressió, abans que el tipus es perdi.
System.out.println(
new Object() {
int comptarLletres(String s) { return s.length(); }
}.comptarLletres("Java Eficac")
); // 11Funciona perquè el compilador coneix el tipus anònim mentre dura l'expressió. Tan bon punt l'assignes a una variable, el tipus es generalitza i el mètode esdevé inabastable.
Conclusió pràctica: els mètodes propis d'una anònima només són útils dins d'ella mateixa, com a auxiliars privats del mètode que sí que forma part del contracte. Si necessites exposar mètodes nous, necessites una classe amb nom.
- Captura de variables efectivament finals
S'aplica exactament la mateixa regla que a les classes locals (04-03): una classe anònima pot llegir variables locals i paràmetres del mètode que l'envolta, sempre que siguin final o efectivament finals.
public Material[] ordenarPerMulta(Material[] materials, int diesTranscorreguts) {
Material[] copia = Arrays.copyOf(materials, materials.length);
Arrays.sort(copia, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
// 'diesTranscorreguts' es un parametre capturat: no es reassigna
return Double.compare(b.calcularMulta(diesTranscorreguts),
a.calcularMulta(diesTranscorreguts));
}
});
return copia;
}Si en qualsevol punt del mètode escrivissis diesTranscorreguts = 30;, la variable deixaria de ser efectivament final i l'error apareixeria dins de la classe anònima:
La raó és la que vas estudiar a 04-03: la variable local viu a la pila i mor amb el mètode; l'objecte anònim viu al monticle i el pot sobreviure. Java copia el valor dins de l'objecte, i permetre'n la modificació produiria dues còpies descoordinades.
I una nota important que evita confusions: la restricció afecta la variable, no l'objecte. Pots capturar una referència i modificar l'objecte a què apunta:
Empleat marta = new Empleat("Marta Ruiz", "EMP-001");
Runnable tasca = new Runnable() {
@Override
public void run() {
marta.registrarPrestec(); // PERMES: es modifica l'OBJECTE
// marta = altreEmpleat; // PROHIBIT: es reassignaria la VARIABLE
}
};Runnable és la interfície de les tasques executables; el seu ús real amb fils és el mòdul 8.
this dins d'una classe anònima
this dins d'una classe anònimaAquest és el punt que provoca més errors subtils del mòdul, i mereix atenció completa.
Dins d'una classe anònima, this es refereix a la mateixa classe anònima, no a la classe que l'envolta.
package com.nexussoftware.bibliotech.servei;
public class GestorPrestecs {
private final String nomGestor = "Gestor central";
public void demostrarThis() {
Runnable tasca = new Runnable() {
private final String nomGestor = "Tasca anonima";
@Override
public void run() {
System.out.println("this.nomGestor = " + this.nomGestor);
System.out.println("GestorPrestecs.this.nom = "
+ GestorPrestecs.this.nomGestor);
System.out.println("this.getClass() = " + this.getClass().getName());
System.out.println("GestorPrestecs.this.class = "
+ GestorPrestecs.this.getClass().getName());
}
};
tasca.run();
}
}this.nomGestor = Tasca anonima GestorPrestecs.this.nom = Gestor central this.getClass() = com.nexussoftware.bibliotech.servei.GestorPrestecs$1 GestorPrestecs.this.class = com.nexussoftware.bibliotech.servei.GestorPrestecs
Les dues últimes línies ho demostren sense cap dubte: this és un objecte de la classe GestorPrestecs$1, la classe anònima que el compilador ha generat (04-03, apartat 9).
Per arribar a la instància externa cal escriure ClasseExterna.this, la mateixa sintaxi de les classes internes.
Per què importa tant? Perquè l'error típic és silenciós:
public class Cataleg {
private String ultimCriteri;
public void ordenarPerTitol(Material[] materials) {
Arrays.sort(materials, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
// Aixo SI que funciona: no hi ha ambiguitat, el camp nomes existeix a fora
ultimCriteri = "titol";
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
});
}
}Aquest accés funciona perquè no hi ha cap ultimCriteri a l'anònima. Però tan bon punt l'anònima declara un camp amb el mateix nom —o crides un mètode registrar() que existeix a totes dues—, la resolució canvia de destinació sense avisar i el programa fa una cosa diferent de la que esperaves. La regla de resolució, igual que a 04-03: variable local → membre de l'anònima → membre de l'externa.
Desa aquesta taula, perquè a 04-05 la compararàs amb el comportament de les lambdes i veuràs que és just el contrari:
| Expressió | Dins d'una classe anònima |
|---|---|
this |
La instància anònima |
this.camp |
Camp de l'anònima |
Externa.this |
La instància de la classe embolcallant |
this.getClass() |
Externa$1 |
Nom solt sense this |
El més proper: local, després anònima, després externa |
- Ús clàssic 1: un
Comparator per al catàleg
Comparator per al catàlegL'ús més freqüent. Aquí tens el Cataleg de 04-03 reescrit amb anònimes, més un tercer criteri compost:
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.Material;
import java.util.Arrays;
import java.util.Comparator;
public class Cataleg {
private final Material[] materials;
public Cataleg(Material[] materials) {
this.materials = Arrays.copyOf(materials, materials.length);
}
private Material[] copia() {
return Arrays.copyOf(materials, materials.length);
}
/** Ordre alfabetic per titol. */
public Material[] ordenatPerTitol() {
Material[] c = copia();
Arrays.sort(c, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
});
return c;
}
/** Multa mes alta primer. El parametre es captura: es efectivament final. */
public Material[] ordenatPerMulta(int diesTranscorreguts) {
Material[] c = copia();
Arrays.sort(c, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return Double.compare(b.calcularMulta(diesTranscorreguts),
a.calcularMulta(diesTranscorreguts));
}
});
return c;
}
/** Criteri compost: primer per tipus, i dins de cada tipus per titol. */
public Material[] ordenatPerTipusITitol() {
Material[] c = copia();
Arrays.sort(c, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
int perTipus = a.getTipus().compareTo(b.getTipus());
if (perTipus != 0) {
return perTipus; // criteri principal
}
return a.getTitol().compareToIgnoreCase(b.getTitol()); // desempat
}
});
return c;
}
}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)
};
Cataleg cataleg = new Cataleg(dades);
System.out.println("--- Per tipus i titol ---");
for (Material m : cataleg.ordenatPerTipusITitol()) {
System.out.printf(" %-10s %s%n", m.getTipus(), m.getTitol());
}--- Per tipus i titol --- DVD Refactoritzacio en directe Llibre Java Eficac Llibre Patrons de Disseny Llibre Refactoritzacio Revista Java Magazine
L'ordenatPerTipusITitol és un bon exemple d'anònima que continua justificant-se: té lògica de diverses línies amb una condició intermèdia. Ordenar així amb un sol compare és exactament el que Comparator.comparing().thenComparing() resoldrà de manera més elegant a 04-06.
- Ús clàssic 2: una crida de retorn
Una crida de retorn (callback) és un objecte amb comportament que passes a un altre perquè l'invoqui quan passi alguna cosa. És el mecanisme que permet que una classe de servei notifiqui sense saber a qui.
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.Prestec;
/** S'avisa aquest oient cada vegada que es registra una devolucio. */
public interface OientDevolucio {
void alRetornar(Prestec prestec, double multa);
}package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.Prestec;
public class GestorPrestecs {
private OientDevolucio oient;
/** Registra qui vol ser avisat. */
public void setOient(OientDevolucio oient) {
this.oient = oient;
}
public double processarDevolucio(Prestec prestec, int diesTranscorreguts) {
double multa = prestec.registrarDevolucio(diesTranscorreguts);
if (oient != null) {
oient.alRetornar(prestec, multa); // callback
}
return multa;
}
}I ara l'ús, amb dos oients diferents definits al punt exacte on es decideix què fer:
GestorPrestecs gestor = new GestorPrestecs();
// Oient 1: rebut per consola
gestor.setOient(new OientDevolucio() {
@Override
public void alRetornar(Prestec prestec, double multa) {
System.out.printf("REBUT %s: %s retornat per %s. Multa %.2f EUR%n",
prestec.getReferencia(), prestec.getTitolMaterial(),
prestec.getNomEmpleat(), multa);
}
});
gestor.processarDevolucio(p1, 20);
// Oient 2: nomes alerta si la multa supera un llindar
final double llindar = 5.0;
gestor.setOient(new OientDevolucio() {
@Override
public void alRetornar(Prestec prestec, double multa) {
if (multa > llindar) { // captura de 'llindar'
System.out.printf("ALERTA: multa alta (%.2f EUR) a %s%n",
multa, prestec.getReferencia());
}
}
});
gestor.processarDevolucio(p2, 40);REBUT PR-0001: Java Eficac retornat per Marta Ruiz. Multa 1,25 EUR ALERTA: multa alta (8,50 EUR) a PR-0002
Fixa't en el que s'ha aconseguit: GestorPrestecs no sap res de rebuts ni d'alertes. Només coneix la interfície OientDevolucio i crida el seu mètode. És la inversió de dependències de 04-01 en acció, i és la base del patró Observer que formalitzaràs a 12-02.
- Ús clàssic 3: estendre puntualment una classe
Les anònimes també serveixen per crear una variant puntual d'una classe existent, típicament en proves:
// Un empleat amb un limit diferent, nomes per a aquesta prova
Empleat directiu = new Empleat("Nuria Vidal", "EMP-004") {
@Override
public boolean potPrendrePrestat() {
return true; // sense limit de prestecs simultanis
}
};
for (int i = 0; i < 6; i++) {
directiu.registrarPrestec();
}
System.out.println(directiu.getNom() + " ha pres 6 materials sense avis.");I amb classes abstractes, que no es poden instanciar (04-02) però sí que es poden estendre anònimament:
// Un material de prova, sense necessitat de crear una classe nova
Material materialProva = new Material("Manual intern", "INT-0001", true) {
@Override public String getTipus() { return "Intern"; }
@Override public int getDiesPrestec() { return 5; }
@Override public double getTarifaDiaria() { return 0.05; }
};
System.out.println(materialProva.descriure());
System.out.printf("Multa als 20 dies: %.2f EUR%n", materialProva.calcularMulta(20));Aquest ús —crear al vol una implementació mínima d'una classe abstracta per provar la lògica de la base— és molt habitual al mòdul 11 amb JUnit. I observa el detall: new Material(...) està prohibit, però new Material(...) { ... } sí que és legal, perquè no instancia Material, sinó una subclasse anònima que sí que és concreta.
- Quan continua sent l'opció correcta i quan fer servir una lambda
Amb Java 8 van arribar les lambdes, que expressen el mateix en molt menys espai:
// Classe anonima
Arrays.sort(c, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
});
// Lambda equivalent (04-05)
Arrays.sort(c, (a, b) -> a.getTitol().compareToIgnoreCase(b.getTitol()));Però la lambda no substitueix la classe anònima en tots els casos. La regla és taxativa:
Una lambda només pot implementar una interfície amb un únic mètode abstracte (una interfície funcional, 04-06). Per a tota la resta, continua fent falta una classe anònima.
| Situació | Fes servir |
|---|---|
| Interfície amb un sol mètode abstracte i cos breu | Lambda |
| Interfície amb dos o més mètodes abstractes | Classe anònima |
| Estendre una classe (concreta o abstracta) | Classe anònima |
| Necessites camps propis amb estat entre crides | Classe anònima |
| Necessites un bloc d'inicialització | Classe anònima |
Necessites que this sigui l'objecte implementador |
Classe anònima |
| Cos de més de ~5 línies o amb diverses branques | Classe anònima o mètode amb nom |
| Es fa servir en més d'un lloc | Classe amb nom |
Exemple d'anònima insubstituïble, amb estat propi entre crides:
/** Comparador que compta quantes vegades se'l crida. Una lambda NO pot fer aixo. */
Comparator<Material> instrumentat = new Comparator<Material>() {
private int crides; // ESTAT propi: impossible en una lambda
@Override
public int compare(Material a, Material b) {
crides++;
if (crides % 5 == 0) {
System.out.println(" [" + crides + " comparacions]");
}
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
};La sintaxi completa de la lambda, les seves formes abreujades i les seves regles són el contingut de la lliçó 04-05. Aquí n'hi ha prou de saber que existeix i quan no et serveix.
- El "double brace initialization" i per què evitar-lo
Existeix un truc molt difós que combina una classe anònima amb un bloc d'inicialització per inicialitzar objectes "en línia". S'anomena double brace initialization per les dues claus consecutives:
// NO FACIS AIXO
Empleat marta = new Empleat("Marta Ruiz", "EMP-001") {{
registrarPrestec();
registrarPrestec();
}};Es llegeix així: la primera clau obre la classe anònima, la segona obre un bloc d'inicialització d'instància. Sembla elegant i compacte. És una mala pràctica reconeguda, per quatre raons concretes:
- Crea una classe extra per cada ús. Cada
{{ }}genera un fitxer.classmés. Un mètode que es cridi mil vegades amb aquesta sintaxi dins d'un bucle genera una classe, sí, però mil instàncies d'una subclasse innecessària. - L'objecte ja no és de la classe que et penses.
marta.getClass()retornaApp$1, noEmpleat. I com que elsequalsben escrits comparen ambgetClass()(03-09),marta.equals(altreEmpleat)retornafalseencara que tinguin el mateix identificador. És un bug real i difícil de trobar. - Reté la instància externa. És una classe interna no estàtica: arrossega la referència oculta
this$0amb tot el que això implica (04-03), inclosa la fuita de memòria si l'objecte es desa en una memòria cau. - Trenca la serialització. Una classe anònima no serialitza bé (mòdul 7).
Les alternatives correctes ja les tens a la mà des del mòdul 3:
// Opcio A: simplement escriu les linies
Empleat marta = new Empleat("Marta Ruiz", "EMP-001");
marta.registrarPrestec();
marta.registrarPrestec();
// Opcio B: un constructor que rebi l'estat inicial
Empleat marta2 = new Empleat("Marta Ruiz", "EMP-001", 2);
// Opcio C: un metode de fabrica amb nom expressiu
Empleat marta3 = Empleat.ambPrestecs("Marta Ruiz", "EMP-001", 2);
- Pes al bytecode i llegibilitat
Dos costos que convé tenir presents.
Una classe per cada anònima. El compilador genera Externa$1.class, Externa$2.class, Externa$3.class... numerant per ordre d'aparició. Conseqüències:
- Més fitxers al
.jari més temps de càrrega de classes en arrencar. - Traces d'error críptiques:
at com.nexussoftware.bibliotech.servei.Cataleg$2.compare(Cataleg.java:41). El nom no diu res; cal anar a la línia 41 per saber de quin comparador es parla. - Renumeració: si afegeixes una anònima abans d'una altra, totes les següents canvien de número. En depuració això despista.
Les lambdes ho mitiguen, perquè no generen una classe per lambda: fan servir la instrucció invokedynamic i creen la implementació en temps d'execució. És un dels seus avantatges menys coneguts i és la raó que les lambdes no encareixin l'arrencada com sí que ho feien milers d'anònimes.
Llegibilitat. Una anònima de tres línies millora la lectura, perquè posa la lògica on es fa servir. Una anònima de trenta línies destrossa la lectura del mètode que la conté: el flux principal queda enterrat sota una declaració de classe. La regla informal més útil:
Si la classe anònima no cap sencera a la pantalla juntament amb el codi que l'envolta, treu-la a una classe amb nom.
- Taula de decisió final
Aquest és el resum del mòdul fins aquí. Desa'l: respon la pregunta "quina forma de classe faig servir?" en gairebé qualsevol situació real.
| Forma | Fes-la servir quan | Evita-la quan |
|---|---|---|
| Classe de primer nivell | El tipus té entitat pròpia, es fa servir des de diversos llocs i forma part del domini | És un auxiliar de quinze línies que només fa servir una classe |
| Imbricada estàtica | És un auxiliar lligat conceptualment a l'externa: nodes, builders, agrupacions de dades | Necessita l'estat de la instància externa |
| Interna (no estàtica) | De debò necessites accedir a l'estat de la instància externa | Gairebé sempre. Risc de fuita de memòria |
| Local | Un tipus amb diversos camps i mètodes que només existeix dins d'un algorisme | Cap en una lambda o el mètode ja és llarg |
| Anònima | Implementació d'un sol ús que necessita estat, diversos mètodes abstractes o estendre una classe | La interfície és funcional i el cos és breu |
| Lambda (04-05) | Interfície funcional, cos breu, sense estat propi | Necessites this, estat o estendre una classe |
flowchart TD
A["Necessito una implementacio"] --> B{"Es fa servir en mes d'un lloc?"}
B -- "Si" --> C["Classe amb nom: primer nivell o imbricada estatica"]
B -- "No" --> D{"Estenc una classe o la interficie te diversos metodes abstractes?"}
D -- "Si" --> E["Classe anonima"]
D -- "No" --> F{"Necessito estat propi o que this sigui l'implementador?"}
F -- "Si" --> E
F -- "No" --> G{"El cos cap en poques linies?"}
G -- "Si" --> H["Lambda"]
G -- "No" --> I["Metode amb nom i referencia a metode"]
Errors Habituals i Consells
Descuidar el punt i coma final. }; tanca una sentència, no una classe. És l'error número u, i el missatge del compilador (';' expected) apunta a la línia següent, cosa que despista.
Intentar escriure un constructor. No existeix. Fes servir un bloc d'inicialització d'instància, o passa els arguments al constructor de la superclasse als parèntesis del new.
Cridar des de fora un mètode propi de l'anònima. No compila: la variable té el tipus de la interfície o la superclasse. Si necessites exposar mètodes nous, necessites nom.
Creure que this és la classe externa. És l'error conceptual més car. Dins de l'anònima, this és l'anònima. Fes servir Externa.this. I prepara't perquè a 04-05 això sigui just a l'inrevés amb les lambdes.
Modificar una variable local capturada. Prohibit: ha de ser efectivament final. Però sí que pots modificar l'objecte a què apunta una referència capturada.
Fer servir double brace initialization. Canvia la classe real de l'objecte, trenca equals amb getClass(), reté la instància externa i genera classes de més.
Anònimes gegants. Una anònima de trenta línies dins d'una crida a un mètode és un mètode il·legible. Treu-la.
Consell: posa sempre @Override. Igual que en herència (03-05), detecta a l'instant una signatura mal escrita. Sense ell, un compare(Material a, Object b) mal teclejat es converteix en un mètode nou i el compilador es queixa molt més tard i pitjor.
Consell: posa nom a la variable si vas a reutilitzar l'anònima. Si el mateix comparador es fa servir en tres mètodes, assigna'l a un camp private static final Comparator<Material> PER_TITOL = new Comparator<>() { ... };. Es crea una sola instància i el nom documenta el criteri.
Consell: el diamant <> funciona des de Java 9. new Comparator<>() { ... } és vàlid i estalvia repetir el tipus. Abans de Java 9 calia escriure new Comparator<Material>() { ... }.
Exercicis
Exercici 1: tres comparadors anònims
Escriu un mètode static void mostrarOrdenat(Material[] materials, String criteri, int dies) que ordeni una còpia de l'array amb Arrays.sort fent servir una classe anònima diferent segons el criteri rebut: "titol" (alfabètic ascendent), "termini" (termini més curt primer) i "multa" (multa més alta primer, per als dies indicats). Fes servir switch de fletxes (02-03). Prova'ls tots tres amb el catàleg de BiblioTech.
Exercici 2: callback amb estat
Amplia el GestorPrestecs de l'apartat 9. Escriu una classe anònima OientDevolucio que acumuli el nombre de devolucions i el total de multes en camps propis, i imprimeixi un resum cada tres devolucions. Explica per què aquesta anònima no podria ser una lambda.
Exercici 3: this en una anònima
Escriu una classe Auditor amb un camp private String origen = "Auditor" i un mètode auditar() que creï una classe anònima Runnable amb el seu propi camp origen = "Tasca". Dins de run(), imprimeix: el camp de l'anònima, el de la classe externa, el nom de classe de this i el nom de classe d'Auditor.this. Predigues la sortida abans d'executar-la.
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);
// switch de fletxes (02-03) que retorna un Comparator diferent a cada branca
Comparator<Material> comparador = switch (criteri) {
case "titol" -> new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitol().compareToIgnoreCase(b.getTitol());
}
};
case "termini" -> new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
// Integer.compare evita el desbordament de 'a - b'
return Integer.compare(a.getDiesPrestec(), b.getDiesPrestec());
}
};
case "multa" -> new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
// b abans que a: ordre DESCENDENT
// 'dies' es un parametre capturat, efectivament final
return Double.compare(b.calcularMulta(dies), a.calcularMulta(dies));
}
};
default -> new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return 0; // sense ordre: deixa l'array tal com esta
}
};
};
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));
}
}
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)
};
mostrarOrdenat(cataleg, "titol", 20);
mostrarOrdenat(cataleg, "termini", 20);
mostrarOrdenat(cataleg, "multa", 20);
}
}--- Ordenat per titol --- Llibre Java Eficac termini 15 multa 1,25 EUR Revista Java Magazine termini 7 multa 1,30 EUR Llibre Patrons de Disseny termini 15 multa 1,25 EUR DVD Refactoritzacio en directe termini 3 multa 8,50 EUR --- Ordenat per termini --- DVD Refactoritzacio en directe termini 3 multa 8,50 EUR Revista Java Magazine termini 7 multa 1,30 EUR Llibre Java Eficac termini 15 multa 1,25 EUR Llibre Patrons de Disseny termini 15 multa 1,25 EUR --- Ordenat per multa --- DVD Refactoritzacio en directe termini 3 multa 8,50 EUR Revista Java Magazine termini 7 multa 1,30 EUR Llibre Java Eficac termini 15 multa 1,25 EUR Llibre Patrons de Disseny termini 15 multa 1,25 EUR
Dues notes tècniques. Primera: Integer.compare(a, b) i Double.compare(a, b) en lloc de restes manuals; la resta d'enters pot desbordar i la de double obliga a convertir el resultat a int, perdent precisió. Segona: l'ordre entre els dos llibres de termini 15 es manté tal com estaven a l'array, perquè Arrays.sort sobre objectes és estable. Aquest detall importarà molt a 05-09.
I una observació que anticipa la lliçó següent: els quatre cossos tenen exactament una línia de lògica útil, embolcallada cadascuna en cinc línies de cerimònia. A 04-05 aquest mètode sencer es quedarà en quatre línies.
Solució 2
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.servei.*;
public class SessioDevolucions {
public static void main(String[] args) {
GestorPrestecs gestor = new GestorPrestecs();
/*
* Aquesta classe anonima NO pot ser una lambda perque mante ESTAT
* propi entre crides (els camps 'devolucions' i 'totalMultes').
* Una lambda es una implementacio sense camps: nomes pot llegir
* variables capturades, que a mes han de ser efectivament finals.
*/
gestor.setOient(new OientDevolucio() {
private int devolucions;
private double totalMultes;
{ // bloc d'inicialitzacio: fa de constructor
System.out.println("Sessio de devolucions iniciada.");
}
@Override
public void alRetornar(Prestec prestec, double multa) {
devolucions++;
totalMultes += multa;
System.out.printf(" %s retornat: %.2f EUR%n",
prestec.getReferencia(), multa);
if (devolucions % 3 == 0) {
System.out.printf(" >>> RESUM: %d devolucions, %.2f EUR acumulats%n",
devolucions, totalMultes);
}
}
});
Empleat marta = new Empleat("Marta Ruiz", "EMP-001");
Empleat diego = new Empleat("Diego Alonso", "EMP-002");
Material[] materials = {
new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994),
new Dvd("Refactoritzacio en directe", "DVD-0007", 95),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual")
};
int[] diesPerPrestec = { 20, 16, 25, 30 };
for (int i = 0; i < materials.length; i++) {
Empleat qui = (i % 2 == 0) ? marta : diego;
Prestec p = new Prestec(materials[i], qui, 100);
gestor.processarDevolucio(p, diesPerPrestec[i]);
}
}
}Sessio de devolucions iniciada. PR-0001 retornat: 1,25 EUR PR-0002 retornat: 0,25 EUR PR-0003 retornat: 11,00 EUR >>> RESUM: 3 devolucions, 12,50 EUR acumulats PR-0004 retornat: 2,30 EUR
Per què no pot ser una lambda: una lambda no té camps. El seu cos només pot fer servir els seus paràmetres, els membres de la classe embolcallant i variables locals capturades, i aquestes últimes han de ser efectivament finals, és a dir, no es poden incrementar. Aquí devolucions i totalMultes canvien a cada invocació i el seu valor ha de persistir entre crides: això exigeix estat a l'objecte, i estat a l'objecte exigeix una classe. A més, aquesta anònima fa servir un bloc d'inicialització, una cosa que tampoc no existeix en una lambda. És el cas de manual on la classe anònima continua sent l'eina correcta en Java 17.
Solució 3
package com.nexussoftware.bibliotech.servei;
public class Auditor {
private String origen = "Auditor";
public void auditar() {
Runnable tasca = new Runnable() {
private String origen = "Tasca";
@Override
public void run() {
System.out.println("1. origen (sense this) = " + origen);
System.out.println("2. this.origen = " + this.origen);
System.out.println("3. Auditor.this.origen = " + Auditor.this.origen);
System.out.println("4. this.getClass() = " + this.getClass().getName());
System.out.println("5. Auditor.this.getClass = "
+ Auditor.this.getClass().getName());
}
};
tasca.run();
}
public static void main(String[] args) {
new Auditor().auditar();
}
}1. origen (sense this) = Tasca 2. this.origen = Tasca 3. Auditor.this.origen = Auditor 4. this.getClass() = com.nexussoftware.bibliotech.servei.Auditor$1 5. Auditor.this.getClass = com.nexussoftware.bibliotech.servei.Auditor
Anàlisi línia a línia:
origensense prefix retorna"Tasca". La resolució va de dins cap a fora: no hi ha cap variable local amb aquest nom, així que guanya el camp de l'anònima i el d'Auditorqueda ensombrit. Aquest és exactament el mecanisme pel qual un canvi innocent —afegir un camp a l'anònima— redirigeix silenciosament accessos que abans anaven a la classe externa.this.origenés el mateix que la línia 1, i aquí queda explícit:thisés l'anònima.Auditor.this.origenés l'única manera d'arribar al camp de la classe embolcallant.this.getClass()retornaAuditor$1, la classe que el compilador va generar (04-03, apartat 9). L'1indica que és la primera anònima d'Auditor; si n'afegeixes una altra abans, aquesta passarà a serAuditor$2.Auditor.this.getClass()retornaAuditor, confirmant que són dos objectes diferents.
Repeteix aquest mateix exercici en acabar 04-05 substituint l'anònima per una lambda: les cinc línies canvien de resultat, perquè en una lambda this és la classe externa. Aquesta diferència és la font número u d'errors en migrar codi antic d'anònimes a lambdes.
Conclusió
Ja saps declarar i instanciar una implementació en una sola expressió i sense posar-li nom. Domines la sintaxi dissecada —el tipus que s'implementa o s'estén, els parèntesis que van al constructor de la superclasse, el cos entre claus i aquell punt i coma final que tanca una sentència, no una classe— i reconeixes el }); com la signatura visual d'una anònima en qualsevol codi Java que obris.
Coneixes els seus límits i el motiu de cadascun: no pot tenir constructor perquè un constructor porta el nom de la seva classe i aquesta no en té, i per això existeix el bloc d'inicialització d'instància; pot declarar mètodes propis, però són inabastables des de fora perquè la variable té el tipus de la interfície i el tipus real no es pot anomenar; i captura variables efectivament finals, amb la mateixa regla i el mateix motiu que les classes locals. I tens molt present el parany que es paga més car: dins d'una classe anònima, this és l'anònima, no la classe embolcallant; per arribar a aquesta cal escriure Externa.this.
Has vist els tres usos que la van convertir en el cavall de batalla de Java durant quinze anys: el Comparator que ordena el catàleg de BiblioTech per títol, per termini o per multa; la crida de retorn que permet a GestorPrestecs avisar sense saber a qui, amb dos oients diferents definits allà on es decideix què fer; i l'extensió puntual d'una classe, inclosa la d'una classe abstracta, que fa legal new Material(...) { ... } allà on new Material(...) està prohibit. Saps per què el double brace initialization és una mala pràctica —canvia la classe real de l'objecte i trenca equals, reté la instància externa i genera classes de més— i quin cost té cada anònima al bytecode: un .class numerat, traces críptiques i renumeració en reordenar.
I sobretot te'n portes la taula de decisió: classe de primer nivell quan el tipus té entitat, imbricada estàtica per a auxiliars, local per a tipus d'un sol algorisme, anònima quan cal estat, diversos mètodes abstractes o estendre una classe, i lambda quan res d'això no cal.
Perquè aquella última fila és el que ve ara. Repassa els comparadors que has escrit en aquesta lliçó: cadascun té una línia de lògica embolcallada en cinc de cerimònia. El nom de la interfície ja el coneix el compilador, perquè ho diu el paràmetre d'Arrays.sort. El nom del mètode també, perquè Comparator només en té un. Els tipus d'a i b també. Tot això sobra. A la lliçó 04-05, Expressions Lambda, trauràs la cerimònia peça a peça fins a deixar només el que aporta informació, aprendràs la sintaxi completa amb totes les seves formes abreujades, entendràs què és realment una lambda —no un punter a funció, sinó la implementació d'una interfície funcional—, veuràs per què this es comporta a l'inrevés que aquí, i escriuràs les teves pròpies interfícies funcionals de BiblioTech per canviar el comportament d'una classe sense tocar-ne el codi.
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
