Les teves classes ja modelen el domini, protegeixen els seus invariants i exposen una API mínima. I tanmateix, si imprimeixes un llibre obtens això: com.nexussoftware.bibliotech.domini.Llibre@1b6d3586. I si compares dos exemplars de "Java Eficaç" amb el mateix ISBN, Java et diu que no són iguals. Tots dos comportaments vénen del mateix lloc: la classe Object, arrel de tota la jerarquia de Java, que proporciona implementacions per defecte d'una sèrie de mètodes universals. Aquestes implementacions funcionen, però són deliberadament conservadores: toString no sap què és important de la teva classe i equals no sap què significa "igual" al teu domini. Només tu ho pots dir. Aquesta lliçó t'ensenya a fer-ho bé, amb el contracte complet de cada mètode, perquè equals i hashCode mal escrits produeixen errors que no es manifesten fins que els teus objectes entren en una col·lecció, i llavors la fallada sembla màgia negra. I en acabar, tancaràs el mòdul complint la promesa que vas deixar al final del mòdul 2.

Contingut

  1. Object, l'arrel de tot
  2. Recorregut pels mètodes d'Object
  3. toString: què és aquest @1b6d3586
  4. Sobreescriure toString correctament
  5. equals: igualtat de referència enfront d'igualtat lògica
  6. El contracte d'equals
  7. Implementar equals pas a pas
  8. L'error clàssic: equals(Llibre) en lloc d'equals(Object)
  9. hashCode: el contracte i per què va sempre amb equals
  10. Implementar hashCode amb Objects.hash
  11. Què es trenca si no sobreescrius hashCode
  12. getClass() enfront d'instanceof a equals
  13. Objects.equals i Objects.requireNonNull
  14. Tancament del mòdul: la refactorització final de BiblioTech
  15. Errors Habituals i Consells
  16. Exercicis

  1. Object, l'arrel de tot

Com vas veure a 03-05, tota classe Java hereta de java.lang.Object, directament o indirectament. Si no escrius extends, el compilador ho posa per tu.

classDiagram
    Object <|-- Material
    Object <|-- Empleat
    Object <|-- Prestec
    Object <|-- String
    Material <|-- Llibre
    Material <|-- Revista
    Material <|-- Dvd
    class Object {
        +toString() String
        +equals(Object) boolean
        +hashCode() int
        +getClass() Class
        #clone() Object
    }

Això té dues conseqüències pràctiques de les quals ja t'has beneficiat sense saber-ho:

  • Tot objecte té els mètodes d'Object. Pots cridar toString() sobre qualsevol cosa.
  • Object serveix com a tipus comú universal. Un paràmetre Object accepta qualsevol objecte, i per això System.out.println(Object) funciona amb el que hi posis. És el polimorfisme de 03-06 portat al límit.

  1. Recorregut pels mètodes d'Object

Mètode Què fa per defecte Se sobreescriu?
toString() Retorna NomClasse@hashHex Gairebé sempre
equals(Object) Compara referències (==) Quan la igualtat sigui lògica
hashCode() Retorna un enter derivat de la identitat Sempre que sobreescriguis equals
getClass() Retorna la classe real de l'objecte No (és final)
clone() Còpia superficial; requereix Cloneable Desaconsellat: fes servir un constructor de còpia (03-04)
finalize() S'invocava abans de recollir Obsolet des de Java 9; no el facis servir mai
wait(), notify(), notifyAll() Coordinació entre fils No; s'estudien al mòdul 8

Dos aclariments sobre les files problemàtiques:

clone() està desaconsellat. El seu disseny obliga a implementar la interfície marcadora Cloneable, produeix còpies superficials per defecte i té una interacció confusa amb final i l'herència. L'alternativa és la que ja coneixes: un constructor de còpia.

finalize() no s'ha de fer servir mai. Està marcat com a obsolet (deprecated) des de Java 9 i eliminat de l'ús pràctic: no hi ha cap garantia que s'executi, ni de quan, ni tan sols que s'executi alguna vegada. Per alliberar recursos existeix try-with-resources, que s'estudia a la lliçó 06-06.

  1. toString: què és aquest @1b6d3586

Quan concatenes o imprimeixes un objecte, Java crida el seu toString():

Llibre llibre = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
System.out.println(llibre);
com.nexussoftware.bibliotech.domini.Llibre@1b6d3586

La implementació per defecte a Object és, literalment:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

Descompost:

Part Què és
com.nexussoftware.bibliotech.domini.Llibre El nom complet de la classe, amb paquet
@ Un separador literal
1b6d3586 El hashCode() de l'objecte, en hexadecimal

I aquí convé desfer un malentès molt estès: aquest número no és l'adreça de memòria. És el valor retornat per hashCode(), que en la implementació per defecte es deriva de la identitat de l'objecte, però que la JVM pot calcular de diverses formes i que no canvia encara que el recol·lector de brossa mogui l'objecte de lloc. És un identificador, no una posició.

Com a identificador funciona; com a informació, és inútil. Per això toString se sobreescriu gairebé sempre.

  1. Sobreescriure toString correctament

Un bon toString és breu, informatiu i sense efectes secundaris. Ha de contenir les dades que identifiquen l'objecte per a un humà.

@Override
public String toString() {
    return String.format("Llibre[isbn=%s, titol=%s, autor=%s, any=%d, disponible=%b]",
                         getIsbn(), getTitol(), autor, anyPublicacio, estaDisponible());
}
System.out.println(llibre);
// Llibre[isbn=978-0000000001, titol=Java Eficac, autor=Joshua Bloch, any=2018, disponible=true]

Regles pràctiques:

Regla Motiu
Inclou els camps identificatius És el que buscaràs en un log
Mantén-lo en una línia Els logs es llegeixen i es filtren per línies
No el facis servir com a format de dades Si algú analitza el teu toString, no el podràs canviar mai
No incloguis dades sensibles Contrasenyes i tokens acaben als logs
No provoquis efectes secundaris Es crida en moments inesperats, fins i tot des del depurador
Compte amb la recursió Si Prestec.toString() imprimeix l'Empleat i aquest imprimeix els seus préstecs, tens un StackOverflowError

Un toString útil per a Prestec, que delega en el que ja sap:

@Override
public String toString() {
    return String.format("Prestec[%s, material=%s, empleat=%s, transcorreguts=%d, multa=%.2f, %s]",
                         referencia, getTitolMaterial(), getNomEmpleat(),
                         diesTranscorreguts, calcularMulta(), classificarGravetat());
}
Prestec[PR-0001, material=Java Eficac, empleat=Marta Ruiz, transcorreguts=20, multa=1,25, LLEU]

Aquest canvi, per si sol, millora radicalment la depuració: la finestra de variables de l'IDE i qualsevol System.out.println passen a mostrar informació llegible en lloc de @1b6d3586.

  1. equals: igualtat de referència enfront d'igualtat lògica

Reprenem el == del mòdul 1, ara amb objectes propis. Hi ha dues nocions d'igualtat:

Noció Operador Pregunta que respon
Igualtat de referència (identitat) == Són el mateix objecte en memòria?
Igualtat lògica (equivalència) equals Representen el mateix segons el domini?

Amb la implementació per defecte d'Object, totes dues són idèntiques, perquè Object.equals és exactament això:

public boolean equals(Object obj) {
    return (this == obj);
}

D'aquí el comportament que sorprèn:

Llibre a = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
Llibre b = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);

System.out.println(a == b);        // false: dos objectes diferents
System.out.println(a.equals(b));   // false: equals per defecte es ==

És correcte? Depèn del que signifiqui "igual" a BiblioTech. I aquesta és una decisió de negoci, no tècnica:

  • Si Llibre representa una obra bibliogràfica, dos objectes amb el mateix ISBN són el mateix llibre. → cal sobreescriure equals.
  • Si Llibre representa un exemplar físic, dos exemplars de la mateixa obra són coses diferents encara que comparteixin ISBN. → la igualtat per identitat és correcta.

A BiblioTech adoptem la primera interpretació: dos llibres són iguals si tenen el mateix ISBN. És la definició que fa servir el sector editorial mateix, i encaixa amb el nostre vocabulari del domini.

Recorda a més la lliçó del mòdul 1 amb String: "Java Eficac" == altraCadena pot donar false encara que el text sigui idèntic, i per això sempre comparaves amb equals. Ara ja saps exactament per què: String sí que sobreescriu equals per comparar el contingut.

  1. El contracte d'equals

equals no és un mètode qualsevol: té un contracte que tota la biblioteca estàndard de Java assumeix que compleixes. Si el trenques, les col·leccions del mòdul 5 es comportaran de manera impredictible i la culpa no serà seva.

Per a referències no nul·les x, y, z:

Propietat Què exigeix Exemple de violació
Reflexiva x.equals(x) és true Comparar un camp double que valgui NaN
Simètrica Si x.equals(y) llavors y.equals(x) Un Llibre que es consideri igual a un String amb el seu ISBN
Transitiva Si x.equals(y) i y.equals(z), llavors x.equals(z) Comparacions que ignoren camps en una subclasse
Consistent Crides repetides retornen el mateix si no canvia l'estat Fer servir un camp mutable o aleatori
Davant de null x.equals(null) és false, mai no llança cap error Oblidar la comprovació de nul·litat

La violació més freqüent en codi real és la simetria, i sol aparèixer així:

// MALAMENT: asimetric
@Override
public boolean equals(Object obj) {
    if (obj instanceof String) {
        return getIsbn().equals(obj);      // un Llibre "igual" a un String
    }
    // ...
}
llibre.equals("978-0000000001");   // true
"978-0000000001".equals(llibre);   // false   <-- trencat

String.equals mai no considerarà igual un Llibre, així que la relació no és simètrica i qualsevol col·lecció que en depengui fallarà de manera aparentment aleatòria.

Consell derivat del contracte: compara només amb objectes del teu propi tipus.

  1. Implementar equals pas a pas

L'estructura canònica té cinc passos, i convé escriure-la sempre igual:

@Override
public boolean equals(Object obj) {

    // 1. Drecera per identitat: el mateix objecte sempre es igual a si mateix.
    //    Es rapid i garanteix la propietat reflexiva.
    if (this == obj) {
        return true;
    }

    // 2. Comprovacio de tipus i de nullitat alhora.
    //    instanceof retorna false si obj es null, aixi que cobreix el contracte
    //    de "x.equals(null) es false" sense una comprovacio a part.
    if (!(obj instanceof Llibre)) {
        return false;
    }

    // 3. Conversio segura al tipus propi.
    Llibre altre = (Llibre) obj;

    // 4. Comparacio dels camps significatius.
    //    A BiblioTech, la identitat d'un llibre es el seu ISBN.
    return getIsbn().equals(altre.getIsbn());
}

Amb l'instanceof amb patró de Java 16 (lliçó 03-06), els passos 2 i 3 es fonen en un:

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof Llibre altre)) return false;
    return getIsbn().equals(altre.getIsbn());
}

Comprovació:

Llibre a = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
Llibre b = new Llibre("Java Eficac", "J. Bloch", "978-0000000001", 2017);   // dades diferents
Llibre c = new Llibre("Refactoritzacio", "Martin Fowler", "978-0000000003", 2018);

System.out.println(a.equals(a));      // true   (reflexiva)
System.out.println(a.equals(b));      // true   (mateix ISBN)
System.out.println(b.equals(a));      // true   (simetrica)
System.out.println(a.equals(c));      // false  (ISBN diferent)
System.out.println(a.equals(null));   // false  (contracte amb null)
System.out.println(a.equals("978-0000000001"));   // false (tipus diferent)
System.out.println(a == b);           // false  (continuen sent dos objectes)

Fixa't en el cas a.equals(b): els títols i anys són diferents, però l'ISBN coincideix, així que són el mateix llibre. Això és exactament el que vam decidir a l'apartat 5: la igualtat la defineix el domini, no la coincidència de tots els camps.

Sobre quins camps fer servir, tres criteris:

  1. Només camps significatius per a la identitat. Un camp derivat o d'estat transitori (com disponible) no hi ha d'entrar.
  2. Preferiblement camps final. Si un camp fet servir a equals canvia, l'objecte canvia d'identitat, amb les conseqüències de l'apartat 11.
  3. Compte amb els tipus: per a float i double, fes servir Float.compare / Double.compare en lloc de ==, perquè NaN != NaN trencaria la reflexivitat, i 0.0 i -0.0 són == però es distingeixen en el bit.

  1. L'error clàssic: equals(Llibre) en lloc d'equals(Object)

Aquest és l'error que cal veure un cop per no repetir-lo mai. Fixa't en la signatura:

public class Llibre extends Material {

    // MALAMENT: aixo NO sobreescriu Object.equals, ho SOBRECARREGA
    public boolean equals(Llibre altre) {
        return getIsbn().equals(altre.getIsbn());
    }
}

Compila sense cap queixa. I funciona... de vegades:

Llibre a = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
Llibre b = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);

System.out.println(a.equals(b));                 // true  <-- fa servir EL TEU metode

Object oa = a;
Object ob = b;
System.out.println(oa.equals(ob));               // false <-- fa servir Object.equals

El mateix parell d'objectes, dos resultats oposats. Per què?

Perquè equals(Llibre) i equals(Object) tenen signatures diferents: són una sobrecàrrega, no una sobreescriptura (taula de 03-05). I la sobrecàrrega es resol en compilació, segons el tipus declarat (03-06). Amb a i b declarats com a Llibre, el compilador tria el teu mètode; amb oa i ob declarats com a Object, tria el d'Object, que compara referències.

I el pitjor és que totes les col·leccions del mòdul 5 criden equals(Object), perquè internament treballen amb referències genèriques. Així que el teu mètode no es faria servir mai on més falta fa, i l'error apareixeria com "el HashSet té llibres duplicats" sense cap pista sobre la causa.

La defensa és l'anotació que ja coneixes:

@Override
public boolean equals(Llibre altre) { ... }
error: method does not override or implement a method from a supertype

Un segon escrivint @Override enfront d'una tarda depurant. Aquesta és, probablement, la raó més contundent de totes les que hem donat per posar-la sempre.

  1. hashCode: el contracte i per què va sempre amb equals

hashCode() retorna un enter que resumeix el contingut de l'objecte. El seu ús és a les estructures basades en taules hash —HashMap i HashSet, mòdul 5—, que l'empren per localitzar objectes ràpidament sense comparar un a un.

El seu contracte té tres clàusules:

Clàusula Enunciat
1. Consistència Si l'objecte no canvia, hashCode() retorna sempre el mateix valor durant l'execució
2. Coherència amb equals Si x.equals(y) és true, llavors x.hashCode() == y.hashCode() obligatòriament
3. Recíproca no exigida Si x.hashCode() == y.hashCode(), x i y no han de ser iguals per força (a això se'n diu col·lisió, i és normal)

La clàusula 2 és la que imposa la regla d'or:

Sempre que sobreescriguis equals, sobreescriu hashCode. Sense excepcions.

La lògica és inevitable: si dos objectes són "iguals" però produeixen codis hash diferents, una taula hash els col·locarà en cubells diferents i mai no els compararà entre si, de manera que no descobrirà mai que són iguals.

Bona notícia: la clàusula 3 significa que un hashCode no ha de ser únic. Hi pot haver col·lisions; l'estructura les resol comparant amb equals dins del cubell. Un hashCode que retornés sempre 0 seria correcte (compleix el contracte) però inútil, perquè degradaria el HashMap a una cerca lineal.

  1. Implementar hashCode amb Objects.hash

La forma moderna i recomanada és una línia, fent servir la classe utilitària java.util.Objects:

import java.util.Objects;

@Override
public int hashCode() {
    return Objects.hash(getIsbn());
}

Regla imprescindible: hashCode ha de fer servir exactament els mateixos camps que equals. Si equals compara l'ISBN, hashCode es calcula amb l'ISBN. Si n'afegeixes un camp a l'un, afegeix-l'hi a l'altre. És la causa número u d'incoherències.

Amb diversos camps:

@Override
public int hashCode() {
    return Objects.hash(referencia, diaPrestec);
}

Objects.hash(...) és un varargs (03-03) que combina els codis hash dels seus arguments amb un algorisme estàndard. Internament fa el mateix que la fórmula clàssica, que convé conèixer perquè la veuràs en codi antic i en entrevistes:

@Override
public int hashCode() {
    int resultat = 17;                                    // primer inicial
    resultat = 31 * resultat + isbn.hashCode();           // 31: primer senar
    resultat = 31 * resultat + Integer.hashCode(any);
    return resultat;
}

El 31 es fa servir perquè és primer senar i perquè 31 * x s'optimitza a (x << 5) - x. No cal que l'escriguis a mà: Objects.hash és més llegible i prou bo.

Una precaució de rendiment: Objects.hash crea un array intern per als varargs, així que en classes amb milions d'instàncies en bucles crítics es pot notar. Per a tota la resta —inclòs BiblioTech—, és l'opció correcta.

  1. Què es trenca si no sobreescrius hashCode

Aquest apartat justifica tot l'anterior. Suposa que sobreescrius equals a Llibre però oblides hashCode:

Llibre a = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
Llibre b = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);

System.out.println(a.equals(b));               // true
System.out.println(a.hashCode());              // 460141958
System.out.println(b.hashCode());              // 1163157884   <-- diferents

Són "iguals" i tenen codis hash diferents: la clàusula 2 està trencada. Les conseqüències, que patiràs al mòdul 5:

// Avancament del modul 5, nomes per veure el simptoma
Set<Llibre> cataleg = new HashSet<>();
cataleg.add(a);

System.out.println(cataleg.contains(b));   // false, encara que a.equals(b) sigui true!
cataleg.add(b);
System.out.println(cataleg.size());        // 2, amb el mateix ISBN dues vegades

Un HashSet que admet duplicats. Un HashMap on guardes amb una clau i no pots recuperar amb una altra d'idèntica. I cap error, cap excepció, cap pista: simplement, resultats incorrectes.

flowchart TD
    A["cataleg.contains(b)"] --> B["Calcula b.hashCode()"]
    B --> C{"Hi ha alguna cosa
    en aquest cubell?"}
    C -- "no (cubell buit)" --> D["Retorna false
    sense cridar mai equals"]
    C -- "si" --> E["Compara amb equals
    dins del cubell"]

Aquí hi ha la clau del diagrama: si el hashCode no coincideix, equals ni tan sols s'arriba a cridar. Per això l'error és tan desconcertant: el teu equals és perfecte i tot i així el resultat és incorrecte.

Regla final, en una frase: equals i hashCode s'escriuen junts, es modifiquen junts i es revisen junts.

  1. getClass() enfront d'instanceof a equals

Existeix una decisió de disseny al pas 2 de l'equals, i mereix conèixer-se perquè té implicacions reals amb l'herència.

Variant amb instanceof (la que hem fet servir):

if (!(obj instanceof Llibre altre)) return false;

Variant amb getClass():

if (obj == null || getClass() != obj.getClass()) return false;
Llibre altre = (Llibre) obj;

La diferència apareix amb subclasses. Imagina un LlibreSignat extends Llibre:

Comparació Amb instanceof Amb getClass()
llibre.equals(llibreSignat) amb el mateix ISBN true false
llibreSignat.equals(llibre) amb el mateix ISBN Depèn de si la subclasse sobreescriu equals false
Simetria garantida? No, si la subclasse afegeix camps a la comparació Sí, sempre
Permet igualtat entre classes de la jerarquia? No

El problema clàssic d'instanceof: si LlibreSignat sobreescriu equals afegint el signant a la comparació, llavors llibre.equals(llibreSignat) donaria true (només mira l'ISBN) mentre que llibreSignat.equals(llibre) donaria false (a més mira el signant). Asimetria, i contracte trencat.

Tria... Quan...
getClass() Vols simetria garantida i que cada classe concreta sigui el seu propi tipus d'igualtat
instanceof Vols que les subclasses siguin intercanviables, i et compromets que cap no afegeixi camps a equals
Cap preocupació La classe és final (llavors són equivalents)

Decisió per a BiblioTech. Com que Llibre pot tenir subclasses i volem simetria a prova de tot, la implementació definitiva farà servir getClass(). I l'alternativa més neta continua sent la de 03-07: declarar final la classe quan no estigui dissenyada per ser estesa, amb la qual cosa el dilema desapareix.

Nota sobre record. Un record (lliçó 04-07) genera automàticament equals, hashCode i toString a partir de tots els seus components, amb el contracte correctament implementat. És la millor raó per fer-los servir en objectes de dades immutables. Però fixa't en el matís: l'equals generat fa servir tots els components, mentre que el nostre fa servir només l'ISBN. Si la teva noció d'igualtat no és "tots els camps coincideixen", el record no et serveix tal com és.

  1. Objects.equals i Objects.requireNonNull

La classe java.util.Objects aporta dues utilitats que veuràs constantment.

Objects.equals(a, b) compara dues referències tractant null correctament, sense que hagis d'escriure la guarda:

// Sense Objects.equals: cal protegir-se
return (autor == null) ? (altre.autor == null) : autor.equals(altre.autor);

// Amb Objects.equals: una linia, i null-safe
return Objects.equals(autor, altre.autor);

La seva implementació és exactament (a == b) || (a != null && a.equals(b)). Fes-la servir sempre dins d'equals quan un camp pugui ser nul.

Objects.requireNonNull(objecte, missatge) verifica que una referència no sigui nul·la i falla immediatament si ho és. El seu valor és a fallar aviat i amb un missatge clar, al constructor, en lloc de deixar que un null viatgi pel sistema i peti tres capes més enllà:

public Prestec(Material material, Empleat empleat, int diaPrestec, int diesTranscorreguts) {
    this.material = Objects.requireNonNull(material, "El prestec necessita un material");
    this.empleat  = Objects.requireNonNull(empleat, "El prestec necessita un empleat");
    // ...
}

Si algú passa null, el programa s'atura allà mateix amb:

Exception in thread "main" java.lang.NullPointerException: El prestec necessita un material
        at com.nexussoftware.bibliotech.domini.Prestec.<init>(Prestec.java:42)

Compara-ho amb el marcador "Material desconegut" que fèiem servir: aquell emmascarava l'error i permetia que el sistema continués amb dades falses. requireNonNull és la primera forma correcta de rebutjar un argument invàlid que veus al curs, i és un avançament del mòdul 6, on aprendràs a llançar excepcions deliberadament. A partir d'ara, a les classes de domini, és preferible als avisos per consola.

  1. Tancament del mòdul: la refactorització final de BiblioTech

Ha arribat el moment de complir la promesa que va tancar el mòdul 2. Recorda el problema:

// BiblioTechApp 2.0: tres variables soltes que descriuen UNA sola cosa
int    retardMesGran        = 0;
String empleatRetardMesGran = "-";
String llibreRetardMesGran  = "-";

// ...i la seva actualitzacio, que cal recordar de fer sencera:
if (diesRetard > retardMesGran) {
    retardMesGran        = diesRetard;
    empleatRetardMesGran = empleat;
    llibreRetardMesGran  = titol;
}

Els tres defectes que assenyalàvem llavors: res no garanteix que les tres s'actualitzin juntes; el compilador no pot ajudar; i si demà vols afegir la multa o la referència, cal afegir una quarta variable i una altra línia que recordar.

Amb el que has après al mòdul, les tres variables es converteixen en una:

// BiblioTechApp 3.0: un unic objecte coherent
Prestec prestecRetardMesGran = null;

// ...i la seva actualitzacio, atomica per construccio:
if (prestecRetardMesGran == null
        || prestec.calcularDiesRetard() > prestecRetardMesGran.calcularDiesRetard()) {
    prestecRetardMesGran = prestec;
}

Una sola assignació. És impossible deixar l'estat a mitges, perquè no hi ha parts per actualitzar per separat: o canvia l'objecte sencer, o no canvia res. I mostrar-lo és una línia, gràcies a toString:

if (prestecRetardMesGran != null) {
    System.out.println("  Retard mes gran: " + prestecRetardMesGran);
} else {
    System.out.println("  Retard mes gran: (cap de registrat)");
}
  Retard mes gran: Prestec[PR-0003, material=Refactoritzacio, empleat=Nuria Vidal, transcorreguts=95, multa=20,00, GREU]

Compara les dues versions:

Aspecte Mòdul 2 Ara
Variables implicades 3 soltes 1 objecte
Línies a l'actualització 4, totes obligatòries 1
Es pot descoordinar Impossible
Afegir la multa a l'informe Quarta variable + quarta línia Ja hi és dins
Mostrar-ho 3 printf 1 println
Comparar dos màxims Comparar tres variables equals

Estat final de BiblioTech després del mòdul 3

Les classes del domini, amb els seus tres mètodes d'Object implementats:

// Llibre.java (extracte: els metodes d'Object)

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Llibre altre = (Llibre) obj;
    return getIsbn().equals(altre.getIsbn());      // igualtat per ISBN
}

@Override
public int hashCode() {
    return Objects.hash(getIsbn());                // MATEIX camp que equals
}

@Override
public String toString() {
    return String.format("Llibre[isbn=%s, titol=%s, autor=%s, any=%d, disponible=%b]",
                         getIsbn(), getTitol(), autor, anyPublicacio, estaDisponible());
}
// Empleat.java (extracte)

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Empleat altre = (Empleat) obj;
    return identificador.equals(altre.identificador);  // identitat = EMP-XXX
}

@Override
public int hashCode() { return Objects.hash(identificador); }

@Override
public String toString() {
    return String.format("Empleat[%s, %s, prestecs=%d]",
                         identificador, nom, prestecsAcumulats);
}
// Prestec.java (extracte)

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Prestec altre = (Prestec) obj;
    return referencia.equals(altre.referencia);        // identitat = PR-XXXX
}

@Override
public int hashCode() { return Objects.hash(referencia); }

@Override
public String toString() {
    return String.format("Prestec[%s, material=%s, empleat=%s, transcorreguts=%d, multa=%.2f, %s]",
                         referencia, getTitolMaterial(), getNomEmpleat(),
                         diesTranscorreguts, calcularMulta(), classificarGravetat());
}

I un main que ja no calcula res:

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.presentacio.RebutConsola;

public class BiblioTechApp {

    public static void main(String[] args) {

        RebutConsola rebut = new RebutConsola();

        System.out.println("=== BiblioTech 3.2 - Fi del modul 3 ===\n");

        Llibre  javaEficac = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
        Llibre  patrons    = new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994);
        Llibre  refactor   = new Llibre("Refactoritzacio", "Martin Fowler", "978-0000000003", 2018);
        Revista revista    = new Revista("Java Magazine", "REV-2024-42", 42, "Bimestral");
        Dvd     dvd        = new Dvd("Curs de Spring", "DVD-0007", 240);

        Empleat marta = new Empleat("Marta Ruiz",   "EMP-001");
        Empleat diego = new Empleat("Diego Alonso", "EMP-002");
        Empleat nuria = new Empleat("Nuria Vidal",  "EMP-003");

        // Array provisional: el modul 5 portara les colleccions
        Prestec[] prestecs = {
            new Prestec(javaEficac, marta, 100, 20),
            new Prestec(patrons,    diego, 100, 10),
            new Prestec(refactor,   nuria, 100, 95),
            new Prestec(revista,    marta, 100, 12),
            new Prestec(dvd,        diego, 100, 12)
        };

        // Estadistiques: un objecte en lloc de tres variables soltes
        Prestec prestecRetardMesGran = null;
        double  recaptacio           = 0.0;

        System.out.println("Prestecs registrats:");
        for (Prestec p : prestecs) {
            System.out.println("  " + p);
            recaptacio += p.calcularMulta();

            if (prestecRetardMesGran == null
                    || p.calcularDiesRetard() > prestecRetardMesGran.calcularDiesRetard()) {
                prestecRetardMesGran = p;
            }
        }

        System.out.printf("%nRecaptacio prevista: %.2f EUR%n", recaptacio);
        System.out.println("Retard mes gran: "
                + (prestecRetardMesGran != null ? prestecRetardMesGran : "(cap)"));

        // Devolucio completa del prestec mes greu
        if (prestecRetardMesGran != null) {
            prestecRetardMesGran.registrarDevolucio(
                    prestecRetardMesGran.getDiesTranscorreguts());
            rebut.imprimirRebut(prestecRetardMesGran);
        }
    }
}

Sortida:

=== BiblioTech 3.2 - Fi del modul 3 ===

Prestecs registrats:
  Prestec[PR-0001, material=Java Eficac, empleat=Marta Ruiz, transcorreguts=20, multa=1,25, LLEU]
  Prestec[PR-0002, material=Patrons de Disseny, empleat=Diego Alonso, transcorreguts=10, multa=0,00, SENSE RETARD]
  Prestec[PR-0003, material=Refactoritzacio, empleat=Nuria Vidal, transcorreguts=95, multa=20,00, GREU]
  Prestec[PR-0004, material=Java Magazine, empleat=Marta Ruiz, transcorreguts=12, multa=0,50, LLEU]
  Prestec[PR-0005, material=Curs de Spring, empleat=Diego Alonso, transcorreguts=12, multa=4,50, GREU]

Recaptacio prevista: 26,25 EUR
Retard mes gran: Prestec[PR-0003, material=Refactoritzacio, empleat=Nuria Vidal, transcorreguts=95, multa=20,00, GREU]

========================================
   REBUT DE DEVOLUCIO - BIBLIOTECH
  Referencia:          PR-0003
  Material:            Refactoritzacio
  Empleat:             Nuria Vidal
  Retard:              80 dies
  Multa:               20,00 EUR
  Gravetat:            GREU
========================================

Inventari del projecte després del mòdul 3

Classe Paquet Responsabilitat
Material domini Base dels suports: identificació, disponibilitat, regles de multa
Llibre domini Suport amb 15 dies i 0,25 €/dia; afegeix autor i any
Revista domini Suport amb 7 dies i 0,10 €/dia; afegeix número i periodicitat
Dvd domini Suport amb 3 dies i 0,50 €/dia; afegeix durada
Empleat domini Persona autoritzada; controla el seu límit de préstecs
Prestec domini Relaciona material i empleat; calcula retard, multa i gravetat
RebutConsola presentacio Genera els textos que veu l'usuari
BiblioTechApp arrel Arrencada i coordinació

El que continua faltant, i ja saps on es resol:

Mancança Mòdul
Material es pot instanciar; falta obligar a implementar getTipus() 4 (classes abstractes)
No hi ha contractes de comportament reutilitzables entre classes sense parentiu 4 (interfícies)
L'array de préstecs és rígid: no s'hi pot afegir ni buscar còmodament 5 (col·leccions)
La validació avisa per consola en lloc de rebutjar de debò 6 (excepcions)
En tancar el programa es perd tot 7 (fitxers)

Errors Habituals i Consells

  • Sobreescriure equals i oblidar hashCode. L'error més car d'aquesta lliçó: no falla res fins que els objectes entren en un HashSet o un HashMap, i llavors falla en silenci.
  • Escriure equals(LaMevaClasse) en lloc d'equals(Object). Sobrecàrrega en lloc de sobreescriptura. @Override ho detecta a l'instant.
  • Fer servir camps diferents a equals i hashCode. Trenca la clàusula 2 del contracte. Escriu-los sempre alhora i amb els mateixos camps.
  • Fer servir camps mutables a equals/hashCode. Si el camp canvia mentre l'objecte és en una col·lecció, l'objecte es torna introbable dins d'ella. Fes servir camps final.
  • Comparar double amb == dins d'equals. NaN != NaN trenca la reflexivitat. Fes servir Double.compare.
  • Formatar dades amb toString per processar-les. Si un altre codi analitza el teu toString, es converteix en una API que no podràs canviar. Per a això hi ha mètodes específics.
  • toString recursiu entre objectes que es referencien. StackOverflowError. Imprimeix l'identificador de l'altre objecte, no l'objecte sencer.
  • Consell: deixa que l'IDE els generi. IntelliJ (Alt+Insertequals() and hashCode()) i Eclipse (Alt+Shift+S) generen les tres implementacions amb el contracte correcte. Escriu-les a mà un cop per entendre-les, i després fes servir el generador.
  • Consell: si la classe és un contenidor de dades immutable, planteja't un record (04-07): et dona els tres mètodes de franc i amb el contracte ben implementat.
  • Consell: prova el contracte. Quatre assert o quatre println comprovant reflexivitat, simetria, transitivitat i null costen un minut i detecten el 90 % dels errors. Al mòdul 11 ho faràs amb JUnit.

Exercicis

Exercici 1: els tres mètodes a Material i les seves subclasses

Implementa toString, equals i hashCode a Material i decideix què han de fer les subclasses:

  1. A Material, la igualtat es defineix per referencia (l'ISBN d'un llibre, el codi d'un DVD...).
  2. Fes servir getClass() per garantir la simetria, i comprova amb sortida per consola que un Llibre i un Dvd amb la mateixa referència no són iguals.
  3. Sobreescriu toString a Material incloent-hi tipus, títol, referència i disponibilitat, i comprova-ho amb les quatre subclasses.
  4. Raona per escrit si Llibre, Revista i Dvd necessiten sobreescriure equals i hashCode, o si en tenen prou d'heretar-los.

Exercici 2: demostrar la sobrecàrrega accidental

Escriu una classe de prova que demostri l'error de l'apartat 8 en la mateixa execució:

  1. Una classe LlibreMal amb public boolean equals(LlibreMal altre) (sense @Override).
  2. Dos objectes amb el mateix ISBN.
  3. Compara'ls amb referències declarades com a LlibreMal i amb referències declarades com a Object, imprimint els dos resultats.
  4. Afegeix @Override i copia el missatge exacte del compilador.
  5. Corregeix la signatura i comprova que ara tots dos resultats coincideixen.

Exercici 3: verificador del contracte d'equals

Escriu un mètode static boolean verificarContracte(Object x, Object y, Object z) que comprovi les cinc propietats del contracte d'equals sobre tres objectes i imprimeixi un informe amb el resultat de cadascuna, més la coherència amb hashCode. Prova'l amb:

  1. Tres Llibre correctes: dos amb el mateix ISBN i un de diferent.
  2. Una classe LlibreSenseHashCode que sobreescrigui equals però no hashCode, per veure quina comprovació falla.

Solucions

Solució 1

package com.nexussoftware.bibliotech.domini;

import java.util.Objects;

public class Material {

    // ... camps, constructor i metodes anteriors ...

    /**
     * Dos materials son iguals si son de la MATEIXA classe i comparteixen referencia.
     * L'us de getClass() garanteix la simetria fins i tot amb subclasses.
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Material altre = (Material) obj;
        return referencia.equals(altre.referencia);
    }

    /** Mateix camp que equals: la referencia. */
    @Override
    public int hashCode() {
        return Objects.hash(referencia);
    }

    @Override
    public String toString() {
        return String.format("%s[ref=%s, titol=%s, disponible=%b]",
                             getTipus(), referencia, titol, disponible);
    }
}

Comprovació de la simetria entre tipus diferents:

Llibre llibre = new Llibre("Java Eficac", "Joshua Bloch", "REF-001", 2018);
Dvd    dvd    = new Dvd("Curs de Spring", "REF-001", 240);   // MATEIXA referencia

System.out.println("llibre.equals(dvd): " + llibre.equals(dvd));   // false
System.out.println("dvd.equals(llibre): " + dvd.equals(llibre));   // false  (simetric)
System.out.println(llibre);
System.out.println(dvd);

Llibre altreExemplar = new Llibre("Java Eficac", "J. Bloch", "REF-001", 2017);
System.out.println("llibre.equals(altreExemplar): " + llibre.equals(altreExemplar));  // true
System.out.println("mateix hash: " + (llibre.hashCode() == altreExemplar.hashCode())); // true

Sortida:

llibre.equals(dvd): false
dvd.equals(llibre): false
Llibre[ref=REF-001, titol=Java Eficac, disponible=true]
DVD[ref=REF-001, titol=Curs de Spring, disponible=true]
llibre.equals(altreExemplar): true
mateix hash: true

Observa un detall elegant del toString de Material: fa servir getTipus(), que és polimòrfic, així que cada subclasse s'identifica sola sense necessitat de sobreescriure toString. És la lliçó 03-06 aplicada aquí.

4. Han de sobreescriure les subclasses equals i hashCode? No, i no ho haurien de fer. Tres raons:

  • La referència ja identifica unívocament qualsevol material del catàleg: no hi ha dos llibres amb el mateix ISBN ni dos DVD amb el mateix codi. Afegir autor o duracioMinuts a la comparació no distingiria res de nou.
  • Sobreescriure equals en una subclasse afegint-hi camps és precisament la font d'asimetries de l'apartat 12. Com que Material.equals fa servir getClass(), un Llibre i un Dvd ja no són mai iguals; el problema està resolt a la classe base.
  • Si necessitessin comparar més camps, hashCode s'hauria d'actualitzar alhora a cada subclasse, multiplicant les oportunitats d'error.

Regla que se'n dedueix: defineix la igualtat una sola vegada, al nivell més alt de la jerarquia on tingui sentit, i fes servir un camp identificador estable en lloc de la suma de tots els camps.

Solució 2

package com.nexussoftware.bibliotech;

public class DemoEqualsMal {

    static class LlibreMal {
        final String isbn;
        LlibreMal(String isbn) { this.isbn = isbn; }

        // SOBRECARREGA, no sobreescriptura: la signatura rep LlibreMal, no Object
        public boolean equals(LlibreMal altre) {
            System.out.println("      [s'executa EL MEU equals(LlibreMal)]");
            return isbn.equals(altre.isbn);
        }
    }

    static class LlibreBe {
        final String isbn;
        LlibreBe(String isbn) { this.isbn = isbn; }

        @Override
        public boolean equals(Object obj) {
            System.out.println("      [s'executa EL MEU equals(Object)]");
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            return isbn.equals(((LlibreBe) obj).isbn);
        }

        @Override
        public int hashCode() { return isbn.hashCode(); }
    }

    public static void main(String[] args) {

        System.out.println("--- Versio INCORRECTA ---");
        LlibreMal a = new LlibreMal("978-0000000001");
        LlibreMal b = new LlibreMal("978-0000000001");

        System.out.println("  Amb tipus declarat LlibreMal:");
        System.out.println("   a.equals(b) = " + a.equals(b));

        Object oa = a;
        Object ob = b;
        System.out.println("  Amb tipus declarat Object:");
        System.out.println("   oa.equals(ob) = " + oa.equals(ob));

        System.out.println("\n--- Versio CORRECTA ---");
        LlibreBe c = new LlibreBe("978-0000000001");
        LlibreBe d = new LlibreBe("978-0000000001");

        System.out.println("  Amb tipus declarat LlibreBe:");
        System.out.println("   c.equals(d) = " + c.equals(d));

        Object oc = c;
        Object od = d;
        System.out.println("  Amb tipus declarat Object:");
        System.out.println("   oc.equals(od) = " + oc.equals(od));
    }
}

Sortida:

--- Versio INCORRECTA ---
  Amb tipus declarat LlibreMal:
      [s'executa EL MEU equals(LlibreMal)]
   a.equals(b) = true
  Amb tipus declarat Object:
   oa.equals(ob) = false

--- Versio CORRECTA ---
  Amb tipus declarat LlibreBe:
      [s'executa EL MEU equals(Object)]
   c.equals(d) = true
  Amb tipus declarat Object:
      [s'executa EL MEU equals(Object)]
   oc.equals(od) = true

Les traces ho deixen claríssim: a la versió incorrecta, la crida a través d'Object ni tan sols entra al teu mètode —no apareix el missatge—, perquè el compilador ha resolt la crida cap a Object.equals. A la correcta, el teu mètode s'executa en tots dos casos.

4. Missatge del compilador en afegir @Override:

error: method does not override or implement a method from a supertype
        @Override
        ^

Cinc paraules que t'haurien estalviat l'error sencer. En un HashSet (mòdul 5), la versió incorrecta produiria duplicats sense cap senyal d'error.

Solució 3

package com.nexussoftware.bibliotech;

/** Verificador didactic del contracte d'equals i la seva coherencia amb hashCode. */
public class VerificadorContracte {

    public static boolean verificarContracte(Object x, Object y, Object z) {

        System.out.println("Verificant el contracte amb:");
        System.out.println("  x = " + x);
        System.out.println("  y = " + y);
        System.out.println("  z = " + z);

        boolean totCorrecte = true;

        // 1. Reflexiva
        boolean reflexiva = x.equals(x) && y.equals(y) && z.equals(z);
        informar("Reflexiva  (x.equals(x))", reflexiva);
        totCorrecte &= reflexiva;

        // 2. Simetrica
        boolean simetrica = (x.equals(y) == y.equals(x))
                         && (y.equals(z) == z.equals(y))
                         && (x.equals(z) == z.equals(x));
        informar("Simetrica  (x=y implica y=x)", simetrica);
        totCorrecte &= simetrica;

        // 3. Transitiva: nomes es comprova si l'antecedent es compleix
        boolean transitiva = true;
        if (x.equals(y) && y.equals(z)) {
            transitiva = x.equals(z);
        }
        informar("Transitiva (x=y i y=z implica x=z)", transitiva);
        totCorrecte &= transitiva;

        // 4. Consistent: mateixes crides, mateixos resultats
        boolean consistent = (x.equals(y) == x.equals(y)) && (x.equals(y) == x.equals(y));
        informar("Consistent (crides repetides)", consistent);
        totCorrecte &= consistent;

        // 5. Davant de null
        boolean davantDeNull = !x.equals(null) && !y.equals(null) && !z.equals(null);
        informar("Null       (x.equals(null) es false)", davantDeNull);
        totCorrecte &= davantDeNull;

        // 6. Coherencia amb hashCode (clausula 2 del contracte de hashCode)
        boolean coherentHash = true;
        if (x.equals(y) && x.hashCode() != y.hashCode()) coherentHash = false;
        if (y.equals(z) && y.hashCode() != z.hashCode()) coherentHash = false;
        if (x.equals(z) && x.hashCode() != z.hashCode()) coherentHash = false;
        informar("hashCode   (iguals -> mateix hash)", coherentHash);
        totCorrecte &= coherentHash;

        System.out.println(totCorrecte ? "RESULTAT: contracte COMPLERT\n"
                                       : "RESULTAT: contracte TRENCAT\n");
        return totCorrecte;
    }

    private static void informar(String propietat, boolean ok) {
        System.out.printf("  %-38s %s%n", propietat, ok ? "OK" : "FALLA");
    }

    /** Classe defectuosa a proposit: equals sense hashCode. */
    static class LlibreSenseHashCode {
        final String isbn;
        LlibreSenseHashCode(String isbn) { this.isbn = isbn; }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            return isbn.equals(((LlibreSenseHashCode) obj).isbn);
        }

        @Override
        public String toString() { return "LlibreSenseHashCode[" + isbn + "]"; }
        // Falta hashCode() a proposit
    }

    public static void main(String[] args) {

        System.out.println("=== CAS 1: Llibre correcte ===");
        Llibre x = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
        Llibre y = new Llibre("Java Eficac", "J. Bloch",     "978-0000000001", 2017);
        Llibre z = new Llibre("Refactoritzacio", "Martin Fowler", "978-0000000003", 2018);
        verificarContracte(x, y, z);

        System.out.println("=== CAS 2: equals sense hashCode ===");
        LlibreSenseHashCode p = new LlibreSenseHashCode("978-0000000001");
        LlibreSenseHashCode q = new LlibreSenseHashCode("978-0000000001");
        LlibreSenseHashCode r = new LlibreSenseHashCode("978-0000000003");
        verificarContracte(p, q, r);
    }
}

Sortida:

=== CAS 1: Llibre correcte ===
Verificant el contracte amb:
  x = Llibre[isbn=978-0000000001, titol=Java Eficac, autor=Joshua Bloch, any=2018, disponible=true]
  y = Llibre[isbn=978-0000000001, titol=Java Eficac, autor=J. Bloch, any=2017, disponible=true]
  z = Llibre[isbn=978-0000000003, titol=Refactoritzacio, autor=Martin Fowler, any=2018, disponible=true]
  Reflexiva  (x.equals(x))               OK
  Simetrica  (x=y implica y=x)           OK
  Transitiva (x=y i y=z implica x=z)     OK
  Consistent (crides repetides)          OK
  Null       (x.equals(null) es false)   OK
  hashCode   (iguals -> mateix hash)     OK
RESULTAT: contracte COMPLERT

=== CAS 2: equals sense hashCode ===
Verificant el contracte amb:
  x = LlibreSenseHashCode[978-0000000001]
  y = LlibreSenseHashCode[978-0000000001]
  z = LlibreSenseHashCode[978-0000000003]
  Reflexiva  (x.equals(x))               OK
  Simetrica  (x=y implica y=x)           OK
  Transitiva (x=y i y=z implica x=z)     OK
  Consistent (crides repetides)          OK
  Null       (x.equals(null) es false)   OK
  hashCode   (iguals -> mateix hash)     FALLA
RESULTAT: contracte TRENCAT

El revelador del cas 2: l'equals és impecable —compleix les cinc propietats— i tot i així la classe està trencada. Aquest és exactament el motiu que la fallada sigui tan difícil de trobar en un projecte real: totes les proves d'igualtat passen, i el sistema falla molt més tard, dins d'un HashMap, sense llançar cap excepció.

Dos apunts tècnics sobre el verificador. Primer, la comprovació de transitivitat només té sentit quan l'antecedent es compleix; si x i y no són iguals, la implicació és certa per vacuïtat i no hi ha res a verificar. Segon, aquest verificador és un avançament del que faràs al mòdul 11 amb JUnit, on aquestes comprovacions s'escriuen com a proves automàtiques que s'executen a cada compilació en lloc d'imprimir per consola.

Conclusió

Has tancat el cercle. Saps que Object és l'arrel de tota la jerarquia i en coneixes els mètodes, inclosos els que no has de tocar (getClass), els desaconsellats (clone), els obsolets (finalize) i els que esperen al mòdul 8 (wait/notify). Entens què és el críptic Llibre@1b6d3586 —el nom de la classe i el hashCode en hexadecimal, no una adreça de memòria— i saps substituir-lo per un toString breu, informatiu i sense efectes secundaris que transforma la depuració. Distingeixes la igualtat de referència de la igualtat lògica, i saps que decidir quina s'aplica és una decisió del domini: a BiblioTech, dos llibres són el mateix llibre si comparteixen ISBN. Domines el contracte complet d'equals —reflexiu, simètric, transitiu, consistent i fals davant de null— i la seva implementació canònica en cinc passos, amb la variant moderna de l'instanceof amb patró. Has vist en execució l'error clàssic d'escriure equals(Llibre) en lloc d'equals(Object), i per què @Override és la diferència entre un segon i una tarda perduda. Coneixes el contracte de hashCode, la regla inquebrantable de sobreescriure'l sempre al costat d'equals i amb els mateixos camps, Objects.hash per escriure'l en una línia, i exactament què es trenca si l'oblides: un HashSet amb duplicats i un equals a qui ningú no arriba a cridar. I afegeixes al teu vocabulari Objects.equals i Objects.requireNonNull, la primera forma correcta de rebutjar un argument invàlid que veus al curs.

Amb això tanques el mòdul 3, i BiblioTech és un altre programa. On hi havia vint variables soltes en un main de dues-centes línies, ara hi ha vuit classes repartides en tres paquets: una jerarquia Material amb tres suports que apliquen les seves pròpies regles per polimorfisme, un Empleat que fa complir el seu límit, un Prestec que calcula el seu venciment en néixer i registra devolucions completes en una sola crida, una capa de presentació separada del domini, i objectes que s'imprimeixen i es comparen com cal. Els camps són privats, els invariants estan garantits, l'API pública està podada i afegir un suport nou costa una classe i zero modificacions. I aquelles tres variables incòmodes —retardMesGran, empleatRetardMesGran i llibreRetardMesGran— són avui un únic objecte Prestec coherent, tal com es va prometre en acabar el mòdul 2.

Al mòdul 4, POO Avançada, faràs el salt dels mecanismes bàsics als que fan servir de debò les biblioteques professionals de Java. Convertiràs Material en una classe abstracta que no es pugui instanciar i que obligui les seves subclasses a implementar el que els toca; descobriràs les interfícies, amb què una classe es pot comprometre a diversos contractes alhora sense herència múltiple; coneixeràs les classes internes i anònimes, les expressions lambda i les referències a mètodes que canvien per complet la manera d'escriure Java modern; i acabaràs amb enumeracions i records, que substituiran aquelles cadenes "LLEU" i "GREU" per tipus segurs i generaran sols els tres mètodes que acabes d'escriure a mà. BiblioTech està a punt per créixer.

Curs de Programació en Java

Mòdul 1: Introducció a Java

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats