En tancar el mòdul 9 va quedar un deute declarat amb totes les lletres: ClientCataleg, ClientMetadades i EnriquidorCataleg repeteixen la mateixa estructura —fer la petició, comprovar el codi, analitzar el cos, traduir l'error— canviant únicament el tipus del resultat. I no tenies manera d'escriure "això és un client d'alguna cosa que retorna coses de tipus T" sense duplicar la classe sencera.
També va quedar un deute més vell. Des del mòdul 5 portes escrivint List<Material>, Map<String, Llibre>, Optional<Fitxa>, CompletableFuture<HttpResponse<String>>, i cada vegada que apareixien aquests <...> et vam dir el mateix: "és el tipus dels elements, la teoria és a 10-01". Aquesta és 10-01.
Els genèrics són la resposta de Java a una pregunta molt concreta: com escric codi que funcioni amb qualsevol tipus, sense renunciar que el compilador comprovi els tipus per mi? Abans de Java 5 la resposta era "fes servir Object i fes conversions", i el preu era que els errors de tipus apareixien en execució, en producció, amb un ClassCastException que ningú no va veure venir. Amb genèrics, aquests mateixos errors apareixen en compilació, a la teva pantalla, abans que ningú no els pateixi.
Aquesta lliçó explica què significa exactament aquest <T>, com escriure les teves pròpies classes i interfícies genèriques, com acotar els tipus acceptats, com funcionen els comodins i el principi PECS, i —molt important— què NO pots fer amb genèrics i per què, perquè l'esborrat de tipus deixa un conjunt de limitacions que sorprenen tothom la primera vegada.
En acabar, BiblioTech tindrà un Repositori<T extends Identificable> que generalitza d'una tacada CatalegConcurrent i RegistrePrestecsSegur, i un Resultat<T> genèric que substitueix el Resultat del mòdul 6.
Contingut
- El problema: contenidors d'
ObjectiClassCastException - La mateixa escena amb genèrics
- Classes genèriques pròpies:
Capsa<T> - Convencions de noms per als paràmetres de tipus
- Instanciació i l'operador diamant
<> - Interfícies genèriques pròpies
- Mètodes genèrics i inferència
- Límits:
<T extends Comparable<T>> - Límits múltiples amb
& - Invariància: per què
List<String>no és unList<Object> - El contrast amb els arrays covariants
- Comodins:
<?>,<? extends X>i<? super X> - PECS: Producer Extends, Consumer Super
- Esborrat de tipus: què fa realment el compilador
- Les conseqüències pràctiques de l'esborrat
- Mètodes pont (bridge methods)
- Tipus crus (raw types) i per què existeixen
@SuppressWarnings("unchecked")i quan és legítim- El testimoni de tipus:
Class<T> - Genèrics i excepcions
- BiblioTech:
Repositori<T extends Identificable> - BiblioTech:
Resultat<T>genèric - Errors Comuns i Consells
- Exercicis
- El problema: contenidors d'
Object i ClassCastException
Object i ClassCastExceptionImagina't que ets a Java 1.4 i vols una llista de llibres. L'única llista que existeix guarda Object:
import java.util.ArrayList;
import java.util.List;
public class CatalegAntic {
public static void main(String[] args) {
List cataleg = new ArrayList(); // sense generics: guarda Object
cataleg.add(new Llibre("Java Eficac", "978-0000000001"));
cataleg.add(new Llibre("Patrons de Disseny", "978-0000000002"));
cataleg.add("Refactoritzacio"); // ERROR! pero el compilador calla
for (int i = 0; i < cataleg.size(); i++) {
Llibre llibre = (Llibre) cataleg.get(i); // conversio obligatoria
System.out.println(llibre.getTitol());
}
}
}Aquest codi compila sense ni un sol error. I en executar-lo:
Java Eficac
Patrons de Disseny
Exception in thread "main" java.lang.ClassCastException:
class java.lang.String cannot be cast to class Llibre
at CatalegAntic.main(CatalegAntic.java:14)Fixa't en els tres mals, perquè són exactament els tres que resolen els genèrics:
| Mal | Descripció | Cost |
|---|---|---|
| No hi ha comprovació | cataleg.add("Refactoritzacio") és vàlid per al compilador |
L'error viatja fins a producció |
| Conversions per tot arreu | Cada get() obliga a un (Llibre) |
Soroll i oportunitat d'equivocar-se |
| La fallada és lluny de la causa | L'add erroni és a la línia 10; l'excepció salta a la 14 |
Depuració llarga |
I hi ha un quart mal, més subtil: l'API no documenta res. Si veus un mètode List carregarCataleg(), no saps què conté la llista. Has de llegir la implementació o refiar-te del nom.
- La mateixa escena amb genèrics
import java.util.ArrayList;
import java.util.List;
public class CatalegModern {
public static void main(String[] args) {
List<Llibre> cataleg = new ArrayList<>();
cataleg.add(new Llibre("Java Eficac", "978-0000000001"));
cataleg.add(new Llibre("Patrons de Disseny", "978-0000000002"));
// cataleg.add("Refactoritzacio"); // NO COMPILA
for (Llibre llibre : cataleg) { // sense conversio
System.out.println(llibre.getTitol());
}
}
}Si descomentes la línia del String, el compilador diu:
error: no suitable method found for add(String)
cataleg.add("Refactoritzacio");
^
method Collection.add(Llibre) is not applicable
(argument mismatch; String cannot be converted to Llibre)L'error s'ha mogut de les 3 de la matinada en producció a les 10 del matí al teu editor. Aquest és tot el valor dels genèrics, i és enorme.
Amb la llista genèrica, a més:
- Desapareixen les conversions. El compilador sap que
get(i)retorna unLlibre. - L'API es documenta sola.
List<Llibre> carregarCataleg()no admet dubtes. - L'IDE t'autocompleta. En escriure
llibre.t'ofereix els mètodes deLlibre, no els d'Object.
La idea central en una frase: un genèric és un paràmetre que no és un valor, sinó un tipus. Igual que un mètode rep paràmetres de valor (
int dies), una classe genèrica rep paràmetres de tipus (<T>), i el compilador comprova la coherència de tot l'ús.
- Classes genèriques pròpies:
Capsa<T>
Capsa<T>La sintaxi és directa: es declara el paràmetre de tipus entre <> just després del nom de la classe, i a partir d'aquí aquest nom es fa servir com si fos un tipus real dins de tot el cos.
package com.nexussoftware.bibliotech.domini;
/**
* Contenidor d'un unic valor de tipus T.
* T es un PARAMETRE DE TIPUS: no existeix fins que algu
* escriu Capsa<Llibre> o Capsa<String>.
*/
public class Capsa<T> {
private T contingut; // camp del tipus parametritzat
public Capsa(T contingut) { // parametre del tipus parametritzat
this.contingut = contingut;
}
public T obtenir() { // retorn del tipus parametritzat
return contingut;
}
public void desar(T contingut) {
this.contingut = contingut;
}
public boolean estaBuida() {
return contingut == null;
}
@Override
public String toString() {
return "Capsa[" + contingut + "]";
}
}Ús:
Capsa<Llibre> capsaLlibre = new Capsa<>(new Llibre("Java Eficac", "978-0000000001"));
Llibre l = capsaLlibre.obtenir(); // sense conversio: el compilador sap que es Llibre
Capsa<String> capsaText = new Capsa<>("Sala 3");
String s = capsaText.obtenir();
// capsaLlibre.desar("aixo no"); // NO COMPILACapsa<Llibre> i Capsa<String> són tipus diferents per al compilador. Una Capsa<Llibre> no es pot assignar a una variable Capsa<String> ni a l'inrevés. A això se'n diu que la classe està parametritzada, i a Capsa<Llibre> se'n diu tipus parametritzat; a Capsa a seques, tipus cru (apartat 17).
Una classe pot tenir diversos paràmetres de tipus, separats per comes:
/**
* Parell de valors de tipus possiblement diferents.
* Es, en essencia, el que fa Map.Entry<K, V>.
*/
public class Parell<A, B> {
private final A primer;
private final B segon;
public Parell(A primer, B segon) {
this.primer = primer;
this.segon = segon;
}
public A primer() { return primer; }
public B segon() { return segon; }
/** Retorna el parell amb els elements intercanviats. */
public Parell<B, A> invertir() {
return new Parell<>(segon, primer);
}
@Override
public String toString() {
return "(" + primer + ", " + segon + ")";
}
}Parell<String, Integer> prestecsPerEmpleat = new Parell<>("Marta Ruiz", 4);
Parell<Integer, String> invertit = prestecsPerEmpleat.invertir();
System.out.println(prestecsPerEmpleat); // (Marta Ruiz, 4)
System.out.println(invertit); // (4, Marta Ruiz)Observa invertir(): retorna Parell<B, A>, és a dir, els paràmetres de tipus es poden combinar i reordenar lliurement. El compilador ho segueix tot.
Nota sobre
record. Unrecord(04-07) també pot ser genèric:public record Parell<A, B>(A primer, B segon) { }fa el mateix en una línia. El veuràs combinat ambsealeda 10-06.
- Convencions de noms per als paràmetres de tipus
Els paràmetres de tipus es poden dir com vulguis (Capsa<TipusDelContingut> compila), però existeix una convenció universal d'una sola lletra majúscula que convé respectar perquè fa el codi immediatament llegible:
| Lletra | Significat | Exemple del JDK |
|---|---|---|
T |
Type: un tipus qualsevol, el cas general | Optional<T>, Class<T> |
E |
Element: element d'una col·lecció | List<E>, Set<E> |
K |
Key: clau d'un mapa | Map<K, V> |
V |
Value: valor d'un mapa | Map<K, V> |
R |
Result: tipus del resultat d'una funció | Function<T, R> |
S, U, V |
Tipus addicionals quan ja has fet servir T |
BiFunction<T, U, R> |
N |
Number: tipus numèric | Menys comú |
Recorda que a 04-06 vas fer servir Function<T, R>, Predicate<T>, Consumer<T> i Supplier<T> sense preguntar-te d'on sortien aquestes lletres. Ara ja ho saps: Function<T, R> significa literalment "funció que rep alguna cosa de tipus T i retorna alguna cosa de tipus R", i per això Function<Llibre, String> és la funció que extreu el títol.
Un paràmetre de tipus no és una variable. No es pot assignar, no ocupa memòria, no existeix en execució (apartat 14). És una instrucció per al compilador.
- Instanciació i l'operador diamant
<>
<>Abans de Java 7 calia repetir el tipus als dos costats:
Java 7 va introduir l'operador diamant <>, que li diu al compilador "infereix aquí el mateix que hi ha a l'esquerra":
Amb var (Java 10, que veuràs a 10-06) el diamant buit no serveix, perquè no hi ha res a l'esquerra d'on inferir:
var cataleg = new ArrayList<Llibre>(); // BE: el tipus va a la dreta
// var llista = new ArrayList<>(); // infereix ArrayList<Object>: gairebe mai es el que volsEl diamant també funciona en classes anònimes des de Java 9:
Comparator<Llibre> perTitol = new Comparator<>() { // Java 9+
@Override
public int compare(Llibre a, Llibre b) {
return a.getTitol().compareTo(b.getTitol());
}
};
- Interfícies genèriques pròpies
Les interfícies es parametritzen igual que les classes. BiblioTech en necessita una que marqui "això té un identificador de tipus T":
package com.nexussoftware.bibliotech.domini;
/**
* Tot el que BiblioTech pot emmagatzemar en un repositori
* sap dir quin es el seu identificador.
*/
public interface Identificable {
String getId();
}I una interfície genèrica de debò, amb paràmetre de tipus:
package com.nexussoftware.bibliotech.servei;
/**
* Converteix objectes de tipus T en una linia de text i viceversa.
* Substitueix els metodes aText/desDeText repetits al modul 7.
*/
public interface Serialitzador<T> {
String serialitzar(T objecte);
T deserialitzar(String linia);
}En implementar-la hi ha dues formes de fer-ho, i la diferència és fonamental:
// FORMA 1: fixar el tipus. La implementacio ja NO es generica.
public class SerialitzadorLlibre implements Serialitzador<Llibre> {
@Override
public String serialitzar(Llibre llibre) {
return llibre.getIsbn() + ";" + llibre.getTitol();
}
@Override
public Llibre deserialitzar(String linia) {
String[] parts = linia.split(";", 2);
return new Llibre(parts[1], parts[0]);
}
}// FORMA 2: propagar el parametre. La implementacio continua sent generica.
public class SerialitzadorAmbRegistre<T> implements Serialitzador<T> {
private final Serialitzador<T> delegat;
public SerialitzadorAmbRegistre(Serialitzador<T> delegat) {
this.delegat = delegat;
}
@Override
public String serialitzar(T objecte) {
String resultat = delegat.serialitzar(objecte);
System.out.println("[SER] " + objecte.getClass().getSimpleName());
return resultat;
}
@Override
public T deserialitzar(String linia) {
return delegat.deserialitzar(linia);
}
}La forma 2 és un decorador (el patró que vas veure aplicat als fluxos a 07-03 i que formalitzaràs a 12-02) i funciona amb qualsevol T: new SerialitzadorAmbRegistre<>(new SerialitzadorLlibre()) és un Serialitzador<Llibre>.
Les interfícies funcionals del mòdul 4 són exactament això: Function<T, R> és una interfície genèrica amb un únic mètode abstracte. Res de màgic.
- Mètodes genèrics i inferència
Un mètode pot declarar els seus propis paràmetres de tipus, independents dels de la classe. Es posen entre els modificadors i el tipus de retorn:
public class Utilitats {
/**
* Intercanvia dues posicions d'una llista de qualsevol tipus.
* modificadors <T> retorn nom
*/
public static <T> void intercanviar(List<T> llista, int i, int j) {
T temporal = llista.get(i);
llista.set(i, llista.get(j));
llista.set(j, temporal);
}
/** Retorna el primer element o null si la llista esta buida. */
public static <T> T primer(List<T> llista) {
return llista.isEmpty() ? null : llista.get(0);
}
/** Construeix una llista a partir d'elements solts. */
@SafeVarargs
public static <T> List<T> llistaDe(T... elements) {
return new ArrayList<>(Arrays.asList(elements));
}
/** Compta quants elements de la llista compleixen el predicat. */
public static <T> long comptar(List<T> llista, Predicate<T> criteri) {
long total = 0;
for (T element : llista) {
if (criteri.test(element)) {
total++;
}
}
return total;
}
}La inferència fa que gairebé mai no hagis d'escriure el tipus:
List<Llibre> cataleg = Utilitats.llistaDe(
new Llibre("Java Eficac", "978-0000000001"),
new Llibre("Refactoritzacio", "978-0000000003"));
Utilitats.intercanviar(cataleg, 0, 1); // T s infereix: Llibre
Llibre primer = Utilitats.primer(cataleg); // T s infereix: Llibre
long prestats = Utilitats.comptar(cataleg, Llibre::estaPrestat);Quan la inferència no basta (o vols ser explícit), la sintaxi per indicar el tipus és incòmoda però existeix: va abans del nom del mètode, després del punt.
Mètode genèric o classe genèrica? La regla pràctica:
| Situació | Elecció |
|---|---|
| El tipus apareix en diversos membres i defineix l'estat de l'objecte | Classe genèrica (Capsa<T>, Repositori<T>) |
| El tipus només es fa servir dins d'un mètode, a la seva signatura | Mètode genèric (static <T> void intercanviar) |
| És un mètode estàtic d'utilitat | Sempre mètode genèric (un static no pot fer servir la T de la classe) |
Aquest últim punt sorprèn. Això no compila:
public class Capsa<T> {
// ERROR: non-static type variable T cannot be referenced from a static context
public static T crearBuida() { return null; }
}La T de la classe pertany a una instància concreta (Capsa<Llibre>), i un mètode estàtic no té instància. La solució és declarar el mètode genèric amb el seu propi paràmetre:
- Límits:
<T extends Comparable<T>>
<T extends Comparable<T>>Una T sense restriccions només garanteix que és un Object: pots cridar toString(), equals() i hashCode(), i res més. Si necessites més, cal acotar el paràmetre amb extends:
/**
* Retorna l'element mes gran de la llista.
* T HA DE saber comparar-se amb si mateix.
*/
public static <T extends Comparable<T>> T maxim(List<T> llista) {
if (llista.isEmpty()) {
throw new IllegalArgumentException("llista buida");
}
T major = llista.get(0);
for (T element : llista) {
if (element.compareTo(major) > 0) { // legal NOMES gracies al limit
major = element;
}
}
return major;
}Sense l'extends Comparable<T>, la línia element.compareTo(major) no compilaria: Object no té compareTo.
Dos matisos importants:
extends significa aquí "és un subtipus de", no "hereta de". Es fa servir la mateixa paraula per a classes i per a interfícies. <T extends Runnable> accepta qualsevol classe que implementi Runnable.
El límit superior té un efecte secundari útil: dins del mètode, T es comporta com el seu límit. Si escrius <T extends Material>, a dins pots cridar element.getTitol() sense conversió.
/** Concatena els titols de qualsevol colleccio de materials. */
public static <T extends Material> String titols(List<T> materials) {
StringBuilder sb = new StringBuilder();
for (T m : materials) {
sb.append(m.getTitol()).append("; "); // getTitol() es de Material
}
return sb.toString();
}
- Límits múltiples amb
&
&Un paràmetre pot exigir diverses condicions alhora, unides amb &:
/**
* Ordena i imprimeix elements que a mes de comparables
* saben serialitzar-se.
*/
public static <T extends Comparable<T> & Serializable> void arxivarOrdenat(List<T> items) {
Collections.sort(items);
for (T item : items) {
System.out.println("Arxivant: " + item);
}
}Dues regles de sintaxi que el compilador imposa:
- Com a màxim una classe, i la resta interfícies (Java no té herència múltiple de classes).
- Si hi ha una classe, va la primera.
<T extends Material & Comparable<T>>és correcte;<T extends Comparable<T> & Material>no compila.
A BiblioTech això té un ús molt natural: "qualsevol cosa que sigui un material i que a més es pugui comparar".
public static <T extends Material & Comparable<T>> List<T> ordenarCataleg(List<T> materials) {
List<T> copia = new ArrayList<>(materials);
Collections.sort(copia);
return copia;
}
- Invariància: per què
List<String> no és un List<Object>
List<String> no és un List<Object>Aquesta és la part que més costa, i mereix calma.
String és un subtipus d'Object. Sembla raonable esperar que List<String> sigui un subtipus de List<Object>. No ho és. Els genèrics a Java són invariants: Capsa<A> i Capsa<B> no tenen cap relació d'herència, per molta que en tinguin A i B.
graph TD
O["Object"] --> S["String"]
LO["List de Object"] -.->|"SENSE relacio"| LS["List de String"]
LS -.->|"SENSE relacio"| LO
Per què aquesta aparent rigidesa? Perquè si es permetés, el sistema de tipus es trencaria. Mira què passaria si compilés:
List<String> textos = new ArrayList<>();
List<Object> objectes = textos; // suposem que aixo compiles
objectes.add(Integer.valueOf(42)); // legal: un Integer ES un Object
String s = textos.get(0); // BUM: ClassCastException en execucioLa llista és la mateixa; només hem canviat l'etiqueta amb què la mirem. A través de l'etiqueta List<Object> hi fiquem un Integer, i a través de l'etiqueta List<String> el traiem com a String. Hauríem tornat exactament al problema de l'apartat 1, que era el que els genèrics venien a resoldre.
La invariància és el preu de la garantia. El compilador prohibeix l'assignació perquè és l'única manera d'assegurar que un List<String> només contindrà String.
- El contrast amb els arrays covariants
L'interessant és que els arrays sí que són covariants, perquè es van dissenyar abans que existissin els genèrics i calia poder escriure Arrays.sort(Object[]). I el resultat és exactament el desastre que els genèrics eviten:
public class CovarianciaArrays {
public static void main(String[] args) {
String[] textos = { "Java Eficac", "Refactoritzacio" };
Object[] objectes = textos; // COMPILA: els arrays son covariants
objectes[0] = Integer.valueOf(42); // compila... i explota en execucio
System.out.println(textos[0]);
}
}Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
at CovarianciaArrays.main(CovarianciaArrays.java:8)La JVM comprova en execució el tipus real de cada element que es guarda en un array, i llança ArrayStoreException. És a dir: els arrays paguen una comprovació en cada escriptura i tot i així l'error arriba tard.
| Aspecte | Arrays | Genèrics |
|---|---|---|
| Variància | Covariants (String[] és un Object[]) |
Invariants (List<String> no és un List<Object>) |
| Comprovació de tipus | En execució (ArrayStoreException) |
En compilació |
| Informació en execució | Reïficats: l'array sap el seu tipus | Esborrats: no la conserven |
| Elements permesos | Només tipus reïficables | Qualsevol tipus de referència |
Aquesta taula explica també per què arrays i genèrics no es barregen bé (apartat 15): són dos mecanismes amb filosofies oposades. La recomanació clàssica és preferir List<T> a T[].
- Comodins:
<?>, <? extends X> i <? super X>
<?>, <? extends X> i <? super X>La invariància és correcta però incòmoda. Si escrius aquest mètode:
public static void imprimirTitols(List<Material> materials) {
for (Material m : materials) {
System.out.println(m.getTitol());
}
}...no li pots passar un List<Llibre>, encara que Llibre estengui Material. I això és absurd: el mètode només llegeix.
Els comodins (?) recuperen la flexibilitat sense perdre la seguretat. Hi ha tres formes.
12.1. El comodí sense límit: <?>
List<?> significa "una llista d'algun tipus desconegut però concret".
/** Serveix per a QUALSEVOL llista: nomes fa servir metodes que no depenen de T. */
public static void informarMida(List<?> llista) {
System.out.println("La llista te " + llista.size() + " elements");
for (Object o : llista) { // l unic segur es llegir com a Object
System.out.println(" - " + o);
}
}informarMida(List.of("a", "b")); // List<String>
informarMida(List.of(new Llibre("Java Eficac", "978-0000000001"))); // List<Llibre>El que NO es pot fer amb un List<?>: afegir-hi res (llevat de null).
public static void intentarAfegir(List<?> llista) {
// llista.add("hola"); // NO COMPILA
// llista.add(new Object()); // NO COMPILA
llista.add(null); // unic permes
}I és lògic: si no saps de quin tipus és la llista, no pots saber què és segur ficar-hi. Podria ser un List<Llibre> i hi estaries ficant un String.
List<?> no és el mateix que List<Object>. List<Object> és una llista que accepta qualsevol cosa (i a la qual sí que hi pots afegir); List<?> és una llista d'un tipus específic que tu desconeixes.
12.2. Comodí amb límit superior: <? extends X>
List<? extends Material> significa "una llista de Material o de qualsevol subtipus seu". Ara sí:
public static void imprimirTitols(List<? extends Material> materials) {
for (Material m : materials) { // llegir com a Material: SEGUR
System.out.println(m.getTitol());
}
// materials.add(new Llibre(...)); // NO COMPILA
}List<Llibre> llibres = new ArrayList<>();
List<Revista> revistes = new ArrayList<>();
List<Material> barreja = new ArrayList<>();
imprimirTitols(llibres); // OK
imprimirTitols(revistes); // OK
imprimirTitols(barreja); // OKPots llegir, no pots escriure. Per què no pots afegir-hi un Llibre? Perquè List<? extends Material> podria ser en realitat un List<Revista>, i ficar-hi un Llibre el corrompria. El compilador no sap quin dels subtipus és, així que prohibeix totes les escriptures.
12.3. Comodí amb límit inferior: <? super X>
List<? super Llibre> significa "una llista de Llibre o de qualsevol supertipus seu": podria ser List<Llibre>, List<Material> o List<Object>.
public static void afegirLlibresDeProva(List<? super Llibre> desti) {
desti.add(new Llibre("Java Eficac", "978-0000000001")); // SEGUR
desti.add(new Llibre("Refactoritzacio", "978-0000000003"));
// Llibre l = desti.get(0); // NO COMPILA
Object o = desti.get(0); // l unic segur es Object
}List<Llibre> nomesLlibres = new ArrayList<>();
List<Material> materials = new ArrayList<>();
List<Object> qualsevol = new ArrayList<>();
afegirLlibresDeProva(nomesLlibres); // OK
afegirLlibresDeProva(materials); // OK
afegirLlibresDeProva(qualsevol); // OKPots escriure, gairebé no pots llegir. Afegir un Llibre sempre és segur: tant si la llista és de Llibre, com de Material o d'Object, un Llibre hi cap a totes. Però en llegir, l'única cosa que el compilador pot garantir és Object, perquè la llista podria ser un List<Object> ple de String.
- PECS: Producer Extends, Consumer Super
La regla que resumeix els dos apartats anteriors es coneix com a PECS, encunyada per Joshua Bloch:
Producer Extends, Consumer Super. Si el paràmetre produeix valors que tu llegeixes, fes servir
? extends. Si el paràmetre consumeix valors que tu li dones, fes servir? super. Si fa les dues coses, no facis servir comodí: fes servir el tipus exacte.
La manera de decidir és preguntar-se: d'on surten les dades i cap a on van?
graph LR
A["Colleccio origen<br/>PRODUEIX dades<br/>? extends T"] -->|"hi llegeixes"| M["El teu metode"]
M -->|"hi escrius"| B["Colleccio desti<br/>CONSUMEIX dades<br/>? super T"]
L'exemple canònic és un mètode de còpia:
/**
* Copia elements d'origen a desti.
* origen PRODUEIX elements -> ? extends T
* desti CONSUMEIX elements -> ? super T
*/
public static <T> void copiar(List<? extends T> origen, List<? super T> desti) {
for (T element : origen) {
desti.add(element);
}
}Amb aquesta signatura, això funciona:
List<Llibre> llibres = List.of(
new Llibre("Java Eficac", "978-0000000001"),
new Llibre("Patrons de Disseny", "978-0000000002"));
List<Material> magatzem = new ArrayList<>();
List<Object> registre = new ArrayList<>();
Utilitats.<Material>copiar(llibres, magatzem); // Llibre produeix, Material consumeix
Utilitats.<Llibre>copiar(llibres, registre); // Llibre produeix, Object consumeixExemples que fallen en triar malament
Error 1: fer servir extends on cal super.
// MALAMENT: desti declarat com a productor
public static <T> void copiarMalament(List<? extends T> origen, List<? extends T> desti) {
for (T element : origen) {
desti.add(element); // NO COMPILA
}
}error: no suitable method found for add(T)
desti.add(element);
^
method Collection.add(CAP#1) is not applicable
(argument mismatch; T cannot be converted to CAP#1)Aquest CAP#1 és el tipus capturat: el compilador diu "no sé quin tipus concret és el comodí, així que no puc garantir que la teva T hi encaixi".
Error 2: fer servir super on cal extends.
// MALAMENT: origen declarat com a consumidor
public static <T> void copiarMalament2(List<? super T> origen, List<? super T> desti) {
for (T element : origen) { // NO COMPILA
desti.add(element);
}
}Llegir d'un ? super T dona Object, no T.
PECS al JDK
Un cop entès, comences a veure PECS per tot arreu. Aquestes signatures del JDK deixen de semblar soroll:
// Collections: origen produeix, desti consumeix
public static <T> void copy(List<? super T> dest, List<? extends T> src)
// El comparador CONSUMEIX elements per comparar-los
public static <T> void sort(List<T> list, Comparator<? super T> c)
// Stream.map: la funcio CONSUMEIX T i PRODUEIX R
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
// Optional.ifPresent: el consumidor CONSUMEIX T
public void ifPresent(Consumer<? super T> action)El cas de Comparator<? super T> és el més útil d'entendre. Significa: "per ordenar una llista de Llibre, em val un Comparator<Llibre>, però també un Comparator<Material>". I és evident: si saps comparar materials qualssevol, saps comparar llibres.
Comparator<Material> perTitol = Comparator.comparing(Material::getTitol);
List<Llibre> llibres = new ArrayList<>(...);
llibres.sort(perTitol); // funciona gracies al ? super TSense aquest ? super, aquesta línia no compilaria i caldria duplicar comparadors per cada subtipus.
Consell pràctic: aplica PECS a les API que escrius per a altres (mètodes públics, biblioteques). Dins del teu codi d'aplicació, amb tipus concrets, no el necessites i afegeix soroll.
Excepció a la regla: no facis servir mai un comodí al tipus de retorn d'un mètode públic. Obliga qui el crida a bregar amb comodins sense guanyar-hi res.
- Esborrat de tipus: què fa realment el compilador
Aquí hi ha la clau per entendre totes les limitacions que vindran després.
Quan Java 5 va introduir els genèrics, hi havia milions de línies de codi escrites contra List sense paràmetres, i bilions de bytes de classes ja compilades. La decisió de disseny va ser: els genèrics existeixen només en temps de compilació. El compilador els fa servir per comprovar el teu codi, i després els esborra. A això se'n diu esborrat de tipus (type erasure).
En concret, el compilador:
- Substitueix cada paràmetre de tipus pel seu límit (o per
Objectsi no en té). - Insereix les conversions necessàries on calen.
- Genera mètodes pont quan l'herència ho requereix (apartat 16).
El que tu escrius:
public class Capsa<T> {
private T contingut;
public T obtenir() { return contingut; }
public void desar(T c) { this.contingut = c; }
}El que queda al .class (vist conceptualment):
public class Capsa {
private Object contingut;
public Object obtenir() { return contingut; }
public void desar(Object c) { this.contingut = c; }
}I al punt d'ús, tu escrius:
i el compilador genera:
Capsa capsa = new Capsa(llibre);
Llibre l = (Llibre) capsa.obtenir(); // la conversio la posa el compiladorLes conversions no van desaparèixer: les escriu el compilador per tu, i només després d'haver comprovat que són correctes. Aquesta és la diferència amb el codi de 1999.
Amb límit, l'esborrat fa servir el límit en lloc d'Object:
public static <T extends Material> String titols(List<T> llista)
// s esborra a
public static String titols(List llista) // i a dins, T es tracta com a MaterialHo pots veure tu mateix amb javap:
public class Capsa<T> {
private T contingut;
public Capsa(T);
public T obtenir();
public void desar(T);
}javap mostra la signatura genèrica perquè aquesta informació sí que es guarda a les metadades de la classe (l'atribut Signature), perquè el compilador pugui comprovar el codi que faci servir aquesta classe. Però la JVM no la fa servir en execució: el bytecode real opera sobre Object. Amb javap -c veuries les instruccions sobre Object i els checkcast.
Matís important per a 10-03: l'esborrat no elimina tota la informació genèrica del fitxer
.class. Les signatures de classes, camps i mètodes sí que conserven els seus tipus genèrics declarats; el que es perd és el tipus d'una instància concreta en execució. Un objecteArrayListno sap si va néixer com aArrayList<Llibre>. La reflexió (10-03) pot llegir el primer ambgetGenericType(), no el segon.
- Les conseqüències pràctiques de l'esborrat
Aquestes cinc limitacions són directes de l'esborrat, i convé reconèixer-les a l'instant quan el compilador es queixa.
15.1. No es pot fer new T()
public class Repositori<T> {
public T crearBuit() {
// return new T(); // ERROR: type parameter T cannot be instantiated directly
return null;
}
}En execució T no existeix: no hi ha cap classe de la qual construir un objecte. La solució és passar un Supplier<T> (el del mòdul 4) o un testimoni de tipus Class<T> (apartat 19):
public class Repositori<T> {
private final Supplier<T> fabrica;
public Repositori(Supplier<T> fabrica) {
this.fabrica = fabrica;
}
public T crearBuit() {
return fabrica.get(); // la fabrica SI que sap construir una T
}
}
// us
Repositori<Llibre> repo = new Repositori<>(Llibre::new);15.2. No es pot fer instanceof List<String>
if (objecte instanceof List<String>) { } // ERROR: illegal generic type for instanceof
if (objecte instanceof List<?>) { } // OK: nomes el tipus cru es comprovableEn execució no hi ha manera de distingir un List<String> d'un List<Llibre>: tots dos són un ArrayList a seques. Només pots comprovar List<?> (o List, amb avís).
15.3. No es poden sobrecarregar mètodes que només difereixen en el genèric
public class Informes {
// ERROR: name clash: tots dos s esborren a processar(List)
public void processar(List<Llibre> llibres) { }
public void processar(List<Revista> revistes) { }
}Després de l'esborrat, totes dues signatures són processar(List). La solució és donar noms diferents (processarLlibres, processarRevistes), que a més es llegeix millor.
15.4. No es poden crear arrays genèrics
Els arrays són reïficats (apartat 11): necessiten saber el seu tipus en execució per llançar ArrayStoreException. Un T[] no ho pot saber. Les dues sortides:
// SOLUCIO 1 (recomanada): fer servir una List
private final List<T> items = new ArrayList<>();
// SOLUCIO 2: array d Object amb conversio i avis suprimit (el que fa ArrayList per dins)
@SuppressWarnings("unchecked")
private T[] items = (T[]) new Object[10];La segona és la que fa servir el mateix ArrayList del JDK (transient Object[] elementData), i és segura només perquè l'array és privat i mai no es retorna a l'exterior com a T[]. Si el retornessis, qui el rebés obtindria un ClassCastException en assignar-lo.
15.5. Els tipus parametritzats comparteixen la mateixa classe
List<Llibre> llibres = new ArrayList<>();
List<String> textos = new ArrayList<>();
System.out.println(llibres.getClass()); // class java.util.ArrayList
System.out.println(llibres.getClass() == textos.getClass()); // trueTots dos són el mateix Class. El mateix passa amb els membres static: una classe genèrica té un únic joc de membres estàtics, compartit per totes les parametritzacions. Capsa<Llibre> i Capsa<String> comparteixen el mateix comptador estàtic.
- Mètodes pont (bridge methods)
Una nota tècnica que explica un detall estrany que et pots trobar en traces de pila i a la reflexió.
Quan una classe implementa una interfície genèrica amb un tipus concret, l'esborrat crea un desajust de signatures. Considera:
public class ComparadorLlibres implements Comparator<Llibre> {
@Override
public int compare(Llibre a, Llibre b) {
return a.getTitol().compareTo(b.getTitol());
}
}Després de l'esborrat, Comparator declara int compare(Object, Object), però la teva classe defineix int compare(Llibre, Llibre). Són signatures diferents: el polimorfisme no funcionaria. El compilador ho resol generant un mètode addicional invisible per a tu:
// generat pel compilador, marcat com a sintetic i pont
public int compare(Object a, Object b) {
return compare((Llibre) a, (Llibre) b); // delega, amb la conversio
}Conseqüències pràctiques:
- Si a 10-03 crides
getDeclaredMethods()sobreComparadorLlibres, veuràs dos mètodescompare. Es filtren ambmetode.isBridge()ometode.isSynthetic(). - Si algú passa un objecte del tipus equivocat per una via sense comprovar (un tipus cru), el
ClassCastExceptionsalta dins del mètode pont, en una línia que tu no vas escriure. Veurecompare(Unknown Source)en una traça sol significar això.
- Tipus crus (raw types) i per què existeixen
Un tipus cru és un tipus genèric fet servir sense els seus paràmetres: List en lloc de List<Llibre>.
Compila, amb un avís:
Note: Exemple.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.Existeixen únicament per compatibilitat cap enrere. Quan Java 5 va introduir els genèrics, tot el codi existent feia servir List sense paràmetres. Prohibir-ho hauria trencat l'ecosistema sencer d'un dia per l'altre. Així que es va permetre barrejar codi antic i nou.
El perillós és que un tipus cru desactiva la comprovació genèrica de tot el que toca, fins i tot cap amunt:
public static void enverinar(List cru) { // tipus cru
cru.add("no soc un Llibre");
}
public static void main(String[] args) {
List<Llibre> cataleg = new ArrayList<>();
enverinar(cataleg); // compila, nomes amb avis
for (Llibre l : cataleg) { // ClassCastException aqui
System.out.println(l.getTitol());
}
}| Escriptura | Compila? | Comprovació | Quan fer-la servir |
|---|---|---|---|
List<Llibre> |
Sí | Completa | Sempre |
List<?> |
Sí | Segura: només lectura com a Object |
Quan el tipus és igual |
List<Object> |
Sí | Completa (llista de qualsevol cosa) | Poques vegades |
List (cru) |
Sí, amb avís | Cap | Mai en codi nou |
Regla: compila amb -Xlint:unchecked i tracta els avisos com a errors. Si al teu codi apareix un tipus cru, és un bug esperant el seu torn.
@SuppressWarnings("unchecked") i quan és legítim
@SuppressWarnings("unchecked") i quan és legítim@SuppressWarnings (que veuràs formalment a 10-02) silencia un avís del compilador. És una eina necessària i, mal utilitzada, un desastre.
Les tres regles d'or:
- Àmbit mínim. Mai a la classe sencera; idealment sobre una variable local.
- Comentari obligatori explicant per què l'operació és segura malgrat l'avís.
- Només quan ho pots demostrar, no quan vols que el compilador calli.
Malament:
@SuppressWarnings("unchecked") // sobre tota la classe: silencia bugs futurs
public class Repositori<T> { ... }Bé:
public <T> T[] copiarA(T[] desti) {
if (desti.length < mida) {
// SEGUR: Arrays.copyOf amb el tipus real de l array de desti
// retorna un array del mateix tipus que 'desti', que es T[].
@SuppressWarnings("unchecked")
T[] nou = (T[]) Arrays.copyOf(items, mida, desti.getClass());
return nou;
}
System.arraycopy(items, 0, desti, 0, mida);
return desti;
}Fixa't en el truc: es declara una variable local només per poder posar-hi l'anotació amb l'àmbit més petit possible. És idiomàtic i apareix al mateix JDK.
- El testimoni de tipus:
Class<T>
Class<T>Si T no existeix en execució, com s'ho fan Jackson, Hibernate o Spring per construir objectes del tipus correcte? Amb un testimoni de tipus: es passa el Class<T> com a argument normal, i aquest objecte sí que existeix en execució.
package com.nexussoftware.bibliotech.servei;
import java.lang.reflect.InvocationTargetException;
/**
* Fabrica generica que SI que pot crear instancies de T,
* perque rep el Class<T> com a testimoni de tipus.
*/
public class FabricaEntitats<T> {
private final Class<T> tipus;
public FabricaEntitats(Class<T> tipus) {
this.tipus = tipus;
}
/** Crea una instancia fent servir el constructor sense arguments. */
public T crear() {
try {
return tipus.getDeclaredConstructor().newInstance();
} catch (NoSuchMethodException | InstantiationException
| IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(
"No s ha pogut instanciar " + tipus.getSimpleName(), e);
}
}
/**
* Converteix amb seguretat: conversio comprovada en EXECUCIO.
* Si l objecte no es del tipus esperat llanca ClassCastException
* aqui mateix, no tres capes mes avall.
*/
public T convertir(Object candidat) {
return tipus.cast(candidat);
}
public String nomTipus() {
return tipus.getSimpleName();
}
}FabricaEntitats<Llibre> fabrica = new FabricaEntitats<>(Llibre.class);
Llibre nou = fabrica.crear();
System.out.println(fabrica.nomTipus()); // LlibreClass<T> és genèric ell mateix: Llibre.class té tipus Class<Llibre>, i per això tipus.cast(x) retorna una T sense necessitat de conversió manual ni de @SuppressWarnings. És la manera neta de recuperar en execució el que l'esborrat es va endur.
Aquest patró s'anomena contenidor heterogeni amb seguretat de tipus, i així s'implementen coses com els atributs d'una petició web:
public class ContextTipat {
private final Map<Class<?>, Object> valors = new HashMap<>();
public <T> void posar(Class<T> tipus, T valor) {
valors.put(Objects.requireNonNull(tipus), valor);
}
public <T> T obtenir(Class<T> tipus) {
return tipus.cast(valors.get(tipus)); // conversio segura
}
}ContextTipat ctx = new ContextTipat();
ctx.posar(Empleat.class, new Empleat("Marta Ruiz"));
ctx.posar(String.class, "sessio-4711");
Empleat qui = ctx.obtenir(Empleat.class); // sense conversio, sense avisLimitació dels testimonis de tipus: no poden representar tipus parametritzats. No existeix List<Llibre>.class. Les biblioteques ho resolen amb la tècnica del super type token (una classe anònima que sí que conserva el tipus genèric a la seva signatura, recuperable per reflexió); és el que hi ha darrere del TypeReference de Jackson, que veuràs a 11-07. La mecànica de per què funciona l'explicaràs a 10-03.
- Genèrics i excepcions
Dues limitacions concretes que convé conèixer.
No es pot capturar un tipus genèric:
public <E extends Exception> void executar(Runnable tasca, Class<E> tipus) {
try {
tasca.run();
} catch (E e) { // ERROR: cannot use the type variable E in a catch clause
// ...
}
}En execució no hi ha E, i el mecanisme de captura de la JVM compara tipus reals. L'alternativa és capturar el supertipus i comprovar amb el testimoni:
public <E extends Exception> void executar(Runnable tasca, Class<E> tipus) throws E {
try {
tasca.run();
} catch (Exception e) {
if (tipus.isInstance(e)) {
throw tipus.cast(e); // rellancem ja tipat
}
throw new BiblioTechException("Fallada inesperada a la tasca", e);
}
}Una classe genèrica no pot estendre Throwable:
// ERROR: a generic class may not extend java.lang.Throwable
public class ErrorTipat<T> extends BiblioTechException { }Per la mateixa raó: el catch no podria distingir ErrorTipat<Llibre> d'ErrorTipat<Prestec>.
Sí que es pot fer servir un tipus genèric a la clàusula throws, i és un truc útil:
/** Executa la tasca propagant la seva excepcio tipada. */
public static <T, E extends Exception> T intentar(SupplierAmbError<T, E> tasca) throws E {
return tasca.obtenir();
}
@FunctionalInterface
public interface SupplierAmbError<T, E extends Exception> {
T obtenir() throws E;
}
- BiblioTech:
Repositori<T extends Identificable>
Repositori<T extends Identificable>Anem a saldar el deute. CatalegConcurrent i RegistrePrestecsSegur fan el mateix amb tipus diferents: desar per identificador, cercar, llistar, eliminar. Un únic repositori genèric els substitueix.
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.Identificable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
/**
* Repositori generic i segur entre fils per a qualsevol entitat
* que sapiga donar el seu identificador.
*
* El limit <T extends Identificable> permet cridar getId()
* dins de la classe sense cap conversio.
*/
public class Repositori<T extends Identificable> {
private final Map<String, T> perId = new ConcurrentHashMap<>();
private final String nom;
public Repositori(String nom) {
this.nom = Objects.requireNonNull(nom, "nom");
}
/** Desa o reemplaca. Retorna el valor anterior, si n hi havia. */
public Optional<T> desar(T entitat) {
Objects.requireNonNull(entitat, "entitat");
return Optional.ofNullable(perId.put(entitat.getId(), entitat));
}
/** Desa nomes si no existia. true si s ha inserit. */
public boolean desarSiAbsent(T entitat) {
return perId.putIfAbsent(entitat.getId(), entitat) == null;
}
public Optional<T> cercarPerId(String id) {
return Optional.ofNullable(perId.get(id));
}
public Optional<T> eliminar(String id) {
return Optional.ofNullable(perId.remove(id));
}
public boolean conte(String id) {
return perId.containsKey(id);
}
public int mida() {
return perId.size();
}
/** Vista immodificable: ningu no pot alterar el repositori per la porta del darrere. */
public List<T> llistarTots() {
return List.copyOf(perId.values());
}
/**
* Filtra amb un predicat.
* Predicate<? super T> per PECS: el predicat CONSUMEIX elements,
* aixi que un Predicate<Identificable> tambe val.
*/
public List<T> cercar(Predicate<? super T> criteri) {
List<T> resultat = new ArrayList<>();
for (T entitat : perId.values()) {
if (criteri.test(entitat)) {
resultat.add(entitat);
}
}
return resultat;
}
/**
* Ordena una copia.
* Comparator<? super T> per PECS: un Comparator<Identificable> val
* per ordenar qualsevol subtipus.
*/
public List<T> llistarOrdenat(Comparator<? super T> ordre) {
List<T> copia = new ArrayList<>(perId.values());
copia.sort(ordre);
return copia;
}
/**
* Bolca totes les entitats al desti.
* ? super T per PECS: el desti CONSUMEIX.
*/
public void bolcarA(Collection<? super T> desti) {
desti.addAll(perId.values());
}
@Override
public String toString() {
return "Repositori[" + nom + ", " + perId.size() + " entitats]";
}
}Ara les entitats implementen Identificable:
public abstract class Material implements Identificable {
private final String isbn;
private final String titol;
// ... resta del modul 3
@Override
public String getId() {
return isbn;
}
}public class Prestec implements Identificable {
private final String referencia; // p. ex. "PR-2026-0041"
@Override
public String getId() {
return referencia;
}
}I l'ús, amb dos tipus diferents i una sola classe de repositori:
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.servei.Repositori;
import java.util.Comparator;
import java.util.List;
public class BiblioTechApp {
public static void main(String[] args) {
Repositori<Material> cataleg = new Repositori<>("cataleg");
Repositori<Prestec> prestecs = new Repositori<>("prestecs");
cataleg.desar(new Llibre("Java Eficac", "978-0000000001"));
cataleg.desar(new Llibre("Patrons de Disseny", "978-0000000002"));
cataleg.desar(new Llibre("Refactoritzacio", "978-0000000003"));
prestecs.desar(new Prestec("PR-2026-0041", "978-0000000001", "Marta Ruiz"));
prestecs.desar(new Prestec("PR-2026-0042", "978-0000000003", "Diego Alonso"));
// Ni una sola conversio en tot el que segueix.
cataleg.cercarPerId("978-0000000001")
.ifPresent(m -> System.out.println("Trobat: " + m.getTitol()));
List<Material> ordenat =
cataleg.llistarOrdenat(Comparator.comparing(Material::getTitol));
ordenat.forEach(m -> System.out.println(" " + m.getTitol()));
List<Prestec> deMarta = prestecs.cercar(p -> p.getEmpleat().equals("Marta Ruiz"));
System.out.println("Prestecs de la Marta: " + deMarta.size());
System.out.println(cataleg);
System.out.println(prestecs);
}
}Trobat: Java Eficac
Java Eficac
Patrons de Disseny
Refactoritzacio
Prestecs de la Marta: 1
Repositori[cataleg, 3 entitats]
Repositori[prestecs, 2 entitats]Què s'ha guanyat, en concret:
| Abans | Ara |
|---|---|
CatalegConcurrent (≈120 línies) |
Repositori<Material> (1 línia de declaració) |
RegistrePrestecsSegur (≈130 línies) |
Repositori<Prestec> (1 línia) |
| Dues implementacions per corregir en paral·lel quan hi ha un bug | Una de sola |
Un Repositori d'Object obligaria a convertir a cada cercarPerId |
Sense conversions |
I afegir un repositori de SalaReunions o de Reserva és ara una línia, no una classe.
- BiblioTech:
Resultat<T> genèric
Resultat<T> genèricA 06-07 vas crear Resultat com a "objecte resultat": una alternativa a llançar excepcions per a errors esperables. Però era un Resultat que només sabia portar un String, o que portava Object i obligava a convertir. Genèrics al rescat.
package com.nexussoftware.bibliotech.servei;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Function;
/**
* Resultat d una operacio que pot fallar de forma esperable.
* O conte un valor de tipus T, o conte un missatge d error.
* Mai les dues coses, mai cap.
*
* Constructor privat + fabriques estatiques: es impossible construir
* un Resultat incoherent.
*/
public final class Resultat<T> {
private final T valor;
private final String error;
private Resultat(T valor, String error) {
this.valor = valor;
this.error = error;
}
/**
* Metode generic estatic: declara la seva propia <U>, perque un
* metode static NO pot fer servir la T de la classe (apartat 7).
*/
public static <U> Resultat<U> exit(U valor) {
return new Resultat<>(Objects.requireNonNull(valor, "valor"), null);
}
public static <U> Resultat<U> fallada(String error) {
return new Resultat<>(null, Objects.requireNonNull(error, "error"));
}
public boolean esExit() { return error == null; }
public boolean esFallada() { return error != null; }
public T valor() {
if (esFallada()) {
throw new NoSuchElementException("El resultat es una fallada: " + error);
}
return valor;
}
public String error() {
if (esExit()) {
throw new NoSuchElementException("El resultat es un exit");
}
return error;
}
public T oPerDefecte(T alternativa) {
return esExit() ? valor : alternativa;
}
/**
* Transforma el valor si hi ha exit; propaga l error si no.
* Function<? super T, ? extends R> per PECS:
* la funcio CONSUMEIX T -> ? super T
* la funcio PRODUEIX R -> ? extends R
*/
public <R> Resultat<R> map(Function<? super T, ? extends R> transformacio) {
if (esFallada()) {
return Resultat.fallada(error); // l error viatja intacte
}
return Resultat.exit(transformacio.apply(valor));
}
/** Encadena operacions que al seu torn poden fallar, sense imbricar Resultat<Resultat<R>>. */
public <R> Resultat<R> flatMap(Function<? super T, Resultat<R>> seguent) {
return esFallada() ? Resultat.fallada(error) : seguent.apply(valor);
}
@Override
public String toString() {
return esExit() ? "Exit[" + valor + "]" : "Fallada[" + error + "]";
}
}Ús al servei de préstecs:
package com.nexussoftware.bibliotech.servei;
import com.nexussoftware.bibliotech.domini.*;
public class GestorPrestecs {
private final Repositori<Material> cataleg;
private final Repositori<Prestec> prestecs;
public GestorPrestecs(Repositori<Material> cataleg, Repositori<Prestec> prestecs) {
this.cataleg = cataleg;
this.prestecs = prestecs;
}
/** El tipus de retorn DIU que aixo pot fallar, i amb quin valor surt be. */
public Resultat<Prestec> prestar(String isbn, String empleat) {
Material material = cataleg.cercarPerId(isbn).orElse(null);
if (material == null) {
return Resultat.fallada("No existeix cap material amb ISBN " + isbn);
}
if (material.estaPrestat()) {
return Resultat.fallada("El material '" + material.getTitol() + "' ja esta prestat");
}
material.marcarPrestat(empleat);
Prestec prestec = new Prestec(seguentReferencia(), isbn, empleat);
prestecs.desar(prestec);
return Resultat.exit(prestec);
}
private String seguentReferencia() {
return String.format("PR-2026-%04d", prestecs.mida() + 1);
}
}I el punt clau, a la presentació:
Resultat<Prestec> resultat = gestor.prestar("978-0000000001", "Nuria Vidal");
if (resultat.esExit()) {
System.out.println("Prestec registrat: " + resultat.valor().getId());
} else {
System.out.println("No s ha pogut prestar: " + resultat.error());
}
// Encadenament sense comprovacions intermedies
String rebut = gestor.prestar("978-0000000002", "Diego Alonso")
.map(Prestec::getId)
.map(id -> "Rebut num. " + id)
.oPerDefecte("Sense rebut: el prestec no s ha pogut registrar");
System.out.println(rebut);El tipus de retorn documenta el contracte. Resultat<Prestec> diu, sense llegir ni una línia de la implementació: això pot fallar de forma esperable, i quan va bé et dona un Prestec. Compara-ho amb Prestec prestar(...) throws BiblioTechException (que obliga a try/catch per a un cas normal) o amb Prestec prestar(...) retornant null (que no diu res i produeix NullPointerException).
A 10-04 veuràs Optional<T>, que és el cas particular d'aquest patró quan l'error no porta informació: només hi ha valor o no n'hi ha.
Errors Comuns i Consells
1. Confondre List<Object> amb List<?>. El primer accepta qualsevol cosa i permet afegir; el segon és d'un tipus desconegut i només permet llegir com a Object i afegir null. Si el teu mètode només llegeix, fes servir List<?> o List<? extends X>, mai List<Object>, perquè aquest últim no accepta un List<String>.
2. Fer servir tipus crus "perquè compili". Cada tipus cru apaga la comprovació genèrica en una zona del codi. Compila sempre amb -Xlint:unchecked -Xlint:rawtypes i arregla els avisos.
3. Esperar que els genèrics existeixin en execució. Ni new T(), ni instanceof List<String>, ni new T[10], ni sobrecàrregues que només difereixin en el paràmetre. Quan necessitis el tipus real, passa un Class<T> o un Supplier<T>.
4. Creure que List<Llibre> és un List<Material>. No ho és. Si el teu mètode només llegeix de la llista, declara List<? extends Material> i funcionarà amb totes dues.
5. Posar comodins al tipus de retorn. public List<? extends Material> llistar() obliga qui l'usa a arrossegar comodins per tot el seu codi. Retorna List<Material>.
6. Silenciar avisos amb @SuppressWarnings a la classe sencera. Àmbit mínim, sempre amb un comentari que justifiqui per què l'operació és segura. Si no la saps justificar, l'avís és un bug.
7. Oblidar PECS en escriure API públiques. Un Comparator<T> en lloc de Comparator<? super T> obliga a duplicar comparadors. Un Consumer<T> en lloc de Consumer<? super T> impedeix reutilitzar un consumidor genèric.
8. Barrejar arrays i genèrics. List<Llibre>[] no es pot crear directament i la covariància dels arrays lluita contra la invariància dels genèrics. Fes servir List<List<Llibre>>.
Consell 1: comença concret i generalitza després. Escriu Repositori de Material primer, fes que funcioni, i només quan necessitis el segon tipus converteix-lo en Repositori<T>. Generalitzar abans de tenir dos casos reals sol produir abstraccions equivocades.
Consell 2: posa el límit més fluix que et serveixi. <T extends Identificable> en lloc de <T extends Material> si només crides getId(). Cada restricció de més tanca portes als usuaris de la teva API.
Consell 3: llegeix les signatures del JDK. <R> Stream<R> map(Function<? super T, ? extends R> mapper) ja no és soroll: és "transforma cada T en una R amb una funció que accepta T o qualsevol supertipus i produeix R o qualsevol subtipus". Entendre-les és el 80 % de dominar els genèrics.
Consell 4: quan el compilador digui CAP#1, pensa en comodins. Aquest identificador és el tipus capturat d'un ?. Gairebé sempre significa que has triat malament entre extends i super.
Exercicis
Exercici 1: Cache<K, V> genèrica amb límit de mida
Escriu una classe Cache<K, V> per a BiblioTech que:
- Desi parells clau-valor amb capacitat màxima fixa.
- En superar-se la capacitat, elimini l'entrada menys recentment usada (LRU).
- Ofereixi
Optional<V> obtenir(K clau),void posar(K clau, V valor),int mida()iList<K> clausPerAntiguitat(). - Tingui un mètode
V obtenirOCalcular(K clau, Function<? super K, ? extends V> calcul)que retorni el valor en memòria cau o el calculi i el desi. - Porti estadístiques d'encerts i errades.
Pista: LinkedHashMap amb el constructor de tres arguments i removeEldestEntry sobreescrit fa l'LRU per tu.
Exercici 2: PECS en un agregador d'estadístiques
Escriu una classe d'utilitat Agregador amb aquests mètodes estàtics, triant correctament els comodins:
sumar(Collection<? ...> nombres)→ retornadoubleamb la suma d'una col·lecció de qualsevol tipus numèric.copiarFiltrant(Collection<? ...> origen, Collection<? ...> desti, Predicate<? ...> criteri)→ copia de l'origen al destí els elements que compleixin el criteri.maximSegons(Collection<? ...> elements, Comparator<? ...> ordre)→ retorna el màxim, oOptional.empty()si està buida.
Després escriu un main que demostri, amb les classes de BiblioTech, que es pot passar un List<Llibre> com a origen i un List<Material> com a destí, i un Comparator<Material> per ordenar llibres.
Exercici 3: ValidadorEncadenable<T> amb Resultat<T>
Construeix un validador genèric per a BiblioTech:
ValidadorEncadenable<T>acumula regles: cada regla és unPredicate<? super T>amb el seu missatge d'error.- Mètode
ambRegla(Predicate<? super T> regla, String missatge)que retorni el mateix validador (encadenable). - Mètode
Resultat<T> validar(T objecte)que retorni èxit amb l'objecte si passa totes les regles, o fallada amb tots els missatges de les regles incomplertes, separats per;. - Un mètode estàtic
de(Class<T> tipus)que creï el validador (fes servir el testimoni de tipus per al missatge).
Fes-lo servir per validar un Llibre: ISBN no nul i de 17 caràcters, títol no buit, títol de menys de 200 caràcters.
Solucions
Solució 1
package com.nexussoftware.bibliotech.servei;
import java.util.*;
import java.util.function.Function;
/**
* Cache LRU generica amb capacitat maxima.
*
* K i V son independents: es pot tenir una Cache<String, Fitxa>
* o una Cache<Integer, List<Prestec>> amb la mateixa classe.
*/
public class Cache<K, V> {
private final int capacitat;
private final LinkedHashMap<K, V> mapa;
private long encerts = 0;
private long errades = 0;
public Cache(int capacitat) {
if (capacitat <= 0) {
throw new IllegalArgumentException("La capacitat ha de ser positiva: " + capacitat);
}
this.capacitat = capacitat;
// Tercer argument true = ordre d ACCES (no d insercio): aixo es l LRU.
// 0.75f es el factor de carrega habitual (reprem 05-05).
this.mapa = new LinkedHashMap<>(capacitat, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> mesAntiga) {
// Quan retorna true, LinkedHashMap elimina l entrada mes antiga.
return size() > Cache.this.capacitat;
}
};
}
/** Retorna Optional en lloc de null: l absencia es explicita (veure 10-04). */
public Optional<V> obtenir(K clau) {
V valor = mapa.get(clau); // get() reordena per ser ordre d acces
if (valor != null) {
encerts++;
} else {
errades++;
}
return Optional.ofNullable(valor);
}
public void posar(K clau, V valor) {
Objects.requireNonNull(clau, "clau");
Objects.requireNonNull(valor, "valor");
mapa.put(clau, valor);
}
/**
* PECS aplicat: la funcio CONSUMEIX la clau (? super K)
* i PRODUEIX el valor (? extends V).
* Aixi accepta una Function<Object, Fitxa> per a una Cache<String, Fitxa>.
*/
public V obtenirOCalcular(K clau, Function<? super K, ? extends V> calcul) {
V existent = mapa.get(clau);
if (existent != null) {
encerts++;
return existent;
}
errades++;
V calculat = calcul.apply(clau);
mapa.put(clau, calculat);
return calculat;
}
public int mida() {
return mapa.size();
}
/** De la menys usada recentment a la mes recent. */
public List<K> clausPerAntiguitat() {
return new ArrayList<>(mapa.keySet());
}
public double taxaEncerts() {
long total = encerts + errades;
return total == 0 ? 0.0 : (double) encerts / total;
}
@Override
public String toString() {
return String.format("Cache[%d/%d entrades, %d encerts, %d errades, %.0f%% encert]",
mapa.size(), capacitat, encerts, errades, taxaEncerts() * 100);
}
}Prova:
public class ProvaCache {
public static void main(String[] args) {
Cache<String, Fitxa> fitxes = new Cache<>(3);
// El calcul simula una consulta cara (llegir del CSV, o demanar per xarxa)
Function<String, Fitxa> consultaCara = isbn -> {
System.out.println(" [CALCULANT fitxa de " + isbn + "]");
return new Fitxa(isbn, "Titol de " + isbn);
};
System.out.println("1) " + fitxes.obtenirOCalcular("978-0000000001", consultaCara));
System.out.println("2) " + fitxes.obtenirOCalcular("978-0000000002", consultaCara));
System.out.println("3) " + fitxes.obtenirOCalcular("978-0000000001", consultaCara)); // encert
System.out.println("4) " + fitxes.obtenirOCalcular("978-0000000003", consultaCara));
System.out.println("Claus abans de desbordar: " + fitxes.clausPerAntiguitat());
// Aquesta quarta clau expulsa la menys usada recentment,
// que es ...0002 (la ...0001 es va refrescar al pas 3).
System.out.println("5) " + fitxes.obtenirOCalcular("978-0000000004", consultaCara));
System.out.println("Claus despres: " + fitxes.clausPerAntiguitat());
System.out.println(fitxes);
}
} [CALCULANT fitxa de 978-0000000001]
1) Fitxa[978-0000000001, Titol de 978-0000000001]
[CALCULANT fitxa de 978-0000000002]
2) Fitxa[978-0000000002, Titol de 978-0000000002]
3) Fitxa[978-0000000001, Titol de 978-0000000001]
[CALCULANT fitxa de 978-0000000003]
4) Fitxa[978-0000000003, Titol de 978-0000000003]
Claus abans de desbordar: [978-0000000002, 978-0000000001, 978-0000000003]
[CALCULANT fitxa de 978-0000000004]
5) Fitxa[978-0000000004, Titol de 978-0000000004]
Claus despres: [978-0000000001, 978-0000000003, 978-0000000004]
Cache[3/3 entrades, 1 encerts, 4 errades, 20% encert]Comentaris. Tres coses que aquest exercici deixa clares.
La classe anònima que estén LinkedHashMap fa servir K i V de la classe externa. Això funciona perquè és una classe interna (no estàtica), i les classes internes sí que poden referir-se als paràmetres de tipus de la que les conté (04-03). Fixa't també en Cache.this.capacitat: dins de la classe anònima, capacitat a seques no existiria.
L'ordre d'accés és el que fa l'LRU. Amb accessOrder = true, cada get() mou l'entrada al final. Per això en desbordar s'expulsa ...0002 i no ...0001, encara que ...0001 s'inserís abans: s'hi va accedir al pas 3.
obtenirOCalcular és el patró de memòria cau correcte. Sense ell, qui fa servir la memòria cau escriu if (cache.obtenir(k).isEmpty()) { ... }, amb dues cerques i una condició de cursa si hi ha diversos fils. Compara-ho amb Map.computeIfAbsent del mòdul 5: és la mateixa idea.
Solució 2
package com.nexussoftware.bibliotech.servei;
import java.util.*;
import java.util.function.Predicate;
public final class Agregador {
private Agregador() { } // classe d utilitat: no s instancia
/**
* PRODUEIX nombres que llegim -> ? extends Number.
* Aixi accepta List<Integer>, List<Double>, List<Long>...
* Amb Collection<Number> NO acceptaria un List<Integer> (invariancia).
*/
public static double sumar(Collection<? extends Number> nombres) {
double total = 0;
for (Number n : nombres) {
total += n.doubleValue();
}
return total;
}
/**
* origen PRODUEIX -> ? extends T
* desti CONSUMEIX -> ? super T
* criteri CONSUMEIX -> Predicate<? super T>
*/
public static <T> int copiarFiltrant(Collection<? extends T> origen,
Collection<? super T> desti,
Predicate<? super T> criteri) {
int copiats = 0;
for (T element : origen) {
if (criteri.test(element)) {
desti.add(element);
copiats++;
}
}
return copiats;
}
/**
* elements PRODUEIX -> ? extends T
* ordre CONSUMEIX -> Comparator<? super T>
* El retorn NO porta comodi: Optional<T> net.
*/
public static <T> Optional<T> maximSegons(Collection<? extends T> elements,
Comparator<? super T> ordre) {
Iterator<? extends T> it = elements.iterator();
if (!it.hasNext()) {
return Optional.empty();
}
T major = it.next();
while (it.hasNext()) {
T candidat = it.next();
if (ordre.compare(candidat, major) > 0) {
major = candidat;
}
}
return Optional.of(major);
}
}Demostració:
public class ProvaAgregador {
public static void main(String[] args) {
// 1) sumar accepta qualsevol colleccio de subtipus de Number
List<Integer> pagines = List.of(412, 395, 448);
List<Double> multes = List.of(1.50, 0.75, 3.20);
System.out.printf("Pagines totals: %.0f%n", Agregador.sumar(pagines));
System.out.printf("Multes totals: %.2f €%n", Agregador.sumar(multes));
// 2) origen List<Llibre>, desti List<Material>: dos tipus DIFERENTS
List<Llibre> llibres = new ArrayList<>(List.of(
new Llibre("Java Eficac", "978-0000000001", 412, true),
new Llibre("Patrons de Disseny", "978-0000000002", 395, false),
new Llibre("Refactoritzacio", "978-0000000003", 448, true)));
List<Material> prestats = new ArrayList<>();
int copiats = Agregador.copiarFiltrant(llibres, prestats, Llibre::estaPrestat);
System.out.println("Copiats a la llista de materials: " + copiats);
prestats.forEach(m -> System.out.println(" " + m.getTitol()));
// 3) Comparator<Material> ordenant una List<Llibre>: gracies al ? super T
Comparator<Material> perTitol = Comparator.comparing(Material::getTitol);
Agregador.maximSegons(llibres, perTitol)
.ifPresent(l -> System.out.println("Ultim alfabeticament: " + l.getTitol()));
// I el maxim retorna Llibre, no Material: el tipus es conserva
Optional<Llibre> ultim = Agregador.maximSegons(llibres, perTitol);
System.out.println("El tipus estatic es Llibre: " + ultim.map(Llibre::getIsbn).orElse("-"));
}
}Pagines totals: 1255
Multes totals: 5,45 €
Copiats a la llista de materials: 2
Java Eficac
Refactoritzacio
Ultim alfabeticament: Refactoritzacio
El tipus estatic es Llibre: 978-0000000003Comentaris.
Sense ? extends Number, sumar(pagines) no compilaria. Un List<Integer> no és un List<Number> per la invariància de l'apartat 10. Aquest és l'exemple més curt de per què existeixen els comodins.
El destí pot ser d'un supertipus. copiarFiltrant(llibres, prestats, ...) copia Llibre en un List<Material>, que és exactament el que un espera poder fer i que sense ? super T seria impossible.
El retorn conserva el tipus més específic. maximSegons(llibres, perTitol) retorna Optional<Llibre>, no Optional<Material>, perquè T s'infereix de la col·lecció. Això és el que es perd si poses el comodí al retorn.
Solució 3
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
/**
* Validador generic i encadenable.
* Acumula regles i retorna un Resultat<T> amb TOTES les fallades,
* no nomes la primera: qui omple un formulari agraeix veure-les juntes.
*/
public final class ValidadorEncadenable<T> {
/** Parell regla-missatge. Un record generic (04-07 + aquest apartat 3). */
private record Regla<T>(Predicate<? super T> condicio, String missatge) { }
private final String nomTipus;
private final List<Regla<T>> regles = new ArrayList<>();
private ValidadorEncadenable(String nomTipus) {
this.nomTipus = nomTipus;
}
/**
* Testimoni de tipus (apartat 19): el Class<T> aporta el nom llegible
* i a mes fixa T sense necessitat d escriure-la.
*/
public static <T> ValidadorEncadenable<T> de(Class<T> tipus) {
return new ValidadorEncadenable<>(tipus.getSimpleName());
}
/**
* Retorna this per encadenar.
* Predicate<? super T> per PECS: la regla CONSUMEIX l objecte,
* aixi que un Predicate<Object> o un Predicate<Material> tambe valen.
*/
public ValidadorEncadenable<T> ambRegla(Predicate<? super T> condicio, String missatge) {
regles.add(new Regla<>(Objects.requireNonNull(condicio, "condicio"),
Objects.requireNonNull(missatge, "missatge")));
return this;
}
public Resultat<T> validar(T objecte) {
if (objecte == null) {
return Resultat.fallada("El " + nomTipus + " es nul");
}
List<String> fallades = new ArrayList<>();
for (Regla<T> regla : regles) {
if (!regla.condicio().test(objecte)) {
fallades.add(regla.missatge());
}
}
if (fallades.isEmpty()) {
return Resultat.exit(objecte);
}
return Resultat.fallada(nomTipus + " no valid: " + String.join("; ", fallades));
}
public int nombreDeRegles() {
return regles.size();
}
}Ús:
public class ProvaValidador {
public static void main(String[] args) {
ValidadorEncadenable<Llibre> validador = ValidadorEncadenable.de(Llibre.class)
.ambRegla(l -> l.getIsbn() != null, "l'ISBN es obligatori")
.ambRegla(l -> l.getIsbn() != null
&& l.getIsbn().length() == 17, "l'ISBN ha de tenir 17 caracters")
.ambRegla(l -> l.getTitol() != null
&& !l.getTitol().isBlank(), "el titol no pot estar buit")
.ambRegla(l -> l.getTitol() == null
|| l.getTitol().length() < 200, "el titol supera els 200 caracters");
System.out.println("Regles configurades: " + validador.nombreDeRegles());
List<Llibre> candidats = List.of(
new Llibre("Java Eficac", "978-0000000001"),
new Llibre("Patrons de Disseny", "978-1"),
new Llibre("", "978-0000000003"));
for (Llibre candidat : candidats) {
Resultat<Llibre> resultat = validador.validar(candidat);
// Encadenament amb map: nomes s executa si hi va haver exit
String linia = resultat
.map(Llibre::getTitol)
.map(t -> "ACCEPTAT: " + t)
.oPerDefecte("REBUTJAT: " + (resultat.esFallada() ? resultat.error() : ""));
System.out.println(linia);
}
// El validador es reutilitzable amb qualsevol Predicate<Material>
Predicate<Material> teTitol = m -> m.getTitol() != null;
ValidadorEncadenable<Llibre> altre = ValidadorEncadenable.de(Llibre.class)
.ambRegla(teTitol, "necessita titol"); // Predicate<Material> en un validador de Llibre
System.out.println(altre.validar(new Llibre("Refactoritzacio", "978-0000000003")));
}
}Regles configurades: 4
ACCEPTAT: Java Eficac
REBUTJAT: Llibre no valid: l'ISBN ha de tenir 17 caracters
REBUTJAT: Llibre no valid: el titol no pot estar buit
Exit[Llibre[Refactoritzacio, 978-0000000003]]Comentaris.
El record Regla<T> privat i genèric demostra que els records també es parametritzen, i que pots tenir tipus genèrics imbricats. Com que és privat i estàtic (els records ho són implícitament), ha de declarar la seva pròpia <T>: no pot fer servir la de la classe externa.
Predicate<? super T> no és decoratiu. L'última línia del main passa un Predicate<Material> a un ValidadorEncadenable<Llibre>. Amb Predicate<T> a seques això no compilaria, i hauries de duplicar els predicats comuns per cada subtipus de Material.
Acumular totes les fallades en lloc de tallar a la primera és una decisió de disseny, no un detall. Un validador que talla obliga l'usuari a corregir un error, reenviar, descobrir el següent, corregir, reenviar. A 11-02 veuràs que la validació d'Spring fa exactament això i per la mateixa raó.
I fixa't que el Resultat<T> genèric ha aparegut aquí sense cap esforç. El validador no sap res de préstecs ni de llibres: retorna Resultat<T> del que sigui que validi. Aquesta és la guanyança d'haver generalitzat.
Conclusió
Has saldat el deute més antic del curs: ja saps exactament què significa aquest <T>.
Saps quin problema resolen: abans de Java 5, un contenidor era d'Object, cada lectura exigia una conversió i cada error de tipus arribava a producció com a ClassCastException. Amb genèrics el mateix error arriba al teu editor en compilació, desapareixen les conversions, l'API es documenta sola i l'IDE et pot ajudar.
Escrius classes genèriques pròpies (Capsa<T>, Parell<A, B>), coneixes les convencions (T, E, K, V, R), fas servir el diamant <> i saps que en un mètode static cal declarar un paràmetre de tipus nou perquè el de la classe pertany a la instància. Escrius interfícies genèriques i saps que en implementar-les pots fixar el tipus (implements Serialitzador<Llibre>) o propagar-lo (class Decorador<T> implements Serialitzador<T>). Escrius mètodes genèrics amb static <T> void ... i deixes que la inferència faci la feina.
Acotes amb límits: <T extends Comparable<T>> per poder cridar compareTo, <T extends Material> per poder cridar getTitol, i límits múltiples amb & amb la classe sempre primer.
I entens la part difícil. Els genèrics són invariants —List<String> no és un List<Object>— i saps exactament per què: permetre-ho deixaria ficar un Integer en una llista de String i tornaria el problema de 1999. Has vist el contrast amb els arrays covariants, que sí que ho permeten i ho paguen amb ArrayStoreException en execució. Els comodins recuperen la flexibilitat: <?> per al que és igual, <? extends X> per llegir, <? super X> per escriure. I PECS —Producer Extends, Consumer Super— és la regla que decideix quin, amb els errors de compilació i el CAP#1 que apareixen en equivocar-se. Ara les signatures del JDK (Comparator<? super T>, Function<? super T, ? extends R>) es llegeixen soles.
Saps què fa realment el compilador: l'esborrat de tipus. Substitueix cada paràmetre pel seu límit, insereix les conversions i genera mètodes pont. I d'aquí surten totes les limitacions que ara reconeixes a l'instant: no hi ha new T(), no hi ha instanceof List<String>, no hi ha sobrecàrregues que només difereixin en el genèric, no hi ha arrays genèrics, i List<Llibre>.class no existeix. Els tipus crus només hi són per compatibilitat i desactiven la comprovació de tot el que toquen; @SuppressWarnings("unchecked") és legítim amb àmbit mínim i una justificació escrita. I el testimoni de tipus Class<T> és la via neta per recuperar en execució el que l'esborrat es va endur, amb tipus.cast() fent la conversió comprovada sense avisos.
BiblioTech ha canviat de forma visible. CatalegConcurrent i RegistrePrestecsSegur —dues-centes cinquanta línies gairebé idèntiques— han desaparegut en favor d'un únic Repositori<T extends Identificable> que s'instancia en una línia per a materials, préstecs, sales o reserves, amb PECS ben aplicat a cercar, llistarOrdenat i bolcarA. I el Resultat del mòdul 6 és ara un Resultat<T> genèric amb map i flatMap, el tipus de retorn del qual documenta el contracte: això pot fallar, i quan va bé et dona un Prestec.
Però hi ha una cosa que els genèrics no poden fer. Els genèrics li diuen coses al compilador, i només al compilador: quins tipus encaixen on. No serveixen per dir "aquest camp s'ha d'exportar al CSV a la tercera columna", ni "aquesta operació cal registrar-la a l'auditoria", ni "aquest mètode està obsolet des de la versió 3". Aquesta mena d'informació —metadades sobre el codi, dirigides no només al compilador sinó també a les eines de construcció i als frameworks que s'executen— necessita un altre mecanisme.
Aquest mecanisme són les anotacions, i és la lliçó següent. Veuràs les estàndard del JDK i què fa realment cadascuna, aprendràs a crear les teves amb @interface, i sobretot entendràs les metaanotacions —començant per @Retention, que decideix si la teva anotació arribarà viva a l'execució o morirà al compilador—. BiblioTech definirà @CampCsv per marcar quins camps s'exporten i @Auditable per marcar quines operacions es registren. I aleshores quedarà una pregunta oberta que resoldrà 10-03: una anotació no fa res per si sola; algú l'ha de llegir. Aquest algú és la reflexió.
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
