Nexus Software acaba de comunicar una novetat a BiblioTech: la biblioteca tècnica no presta només llibres. També presta revistes especialitzades —que es retornen en una setmana perquè circulen molt— i DVD de formació, que es presten tres dies i la tarifa de retard dels quals és el doble, perquè hi ha poques còpies. Amb el que saps fins ara, l'única sortida seria duplicar la classe Llibre dues vegades, canviant dos números i el nom: tres classes gairebé idèntiques, amb la mateixa lògica de disponibilitat copiada tres cops i tres llocs on corregir cada error. L'herència és el mecanisme amb què Java evita exactament això: permet definir una classe com una especialització d'una altra, heretant tot el que ja té i declarant únicament allò en què difereix. És el pilar més potent de la POO i també el més fàcil de fer servir malament, així que aquesta lliçó t'ensenya tant a aplicar-lo com a saber quan no aplicar-lo.
Contingut
- La relació "és un"
extends: sintaxi i primer exemple- Què s'hereta i què no
super: el constructor de la superclassesuper: accedir a membres de la superclasse- Ordre de construcció en una jerarquia
- Sobreescriptura de mètodes i
@Override - Sobreescriure enfront de sobrecarregar
finalen classes i mètodes- Tota classe hereta d'
Object - Herència enfront de composició
- Jerarquies profundes i la fragilitat de la classe base
- BiblioTech: la jerarquia de materials prestables
- Errors Habituals i Consells
- Exercicis
- La relació "és un"
L'herència modela una relació molt concreta: "X és un Y". Si la frase sona natural i és certa sempre, l'herència és candidata:
- Un llibre és un material prestable. ✔
- Una revista és un material prestable. ✔
- Un DVD és un material prestable. ✔
- Un préstec és un llibre. ✘ (un préstec té un llibre)
- Un empleat és un llibre. ✘
Vocabulari:
| Terme | Sinònims | Significat |
|---|---|---|
| Superclasse | classe base, classe mare | La classe general de la qual s'hereta |
| Subclasse | classe derivada, classe filla | La classe especialitzada que hereta |
| Herència | extensió | El mecanisme que les uneix |
I una regla que Java imposa i convé conèixer des del principi: una classe només pot estendre una superclasse. No hi ha herència múltiple de classes (sí que n'hi ha d'interfícies, lliçó 04-01). Aquesta restricció evita ambigüitats famoses d'altres llenguatges.
extends: sintaxi i primer exemple
extends: sintaxi i primer exemplepublic class Material {
public String titol;
public boolean disponible;
public void prestar() {
disponible = false;
}
}Amb només aquesta línia, Revista disposa de titol, disponible i prestar() sense haver-los escrit:
Revista r = new Revista();
r.titol = "Java Magazine"; // heretat
r.numero = 42; // propi
r.prestar(); // heretat
System.out.println(r.disponible); // falseLa subclasse és igual o més gran que la superclasse: mai menys. Pot afegir camps, afegir mètodes i canviar el comportament dels heretats, però no pot eliminar res.
- Què s'hereta i què no
Una taula que resol la majoria dels dubtes:
| Element de la superclasse | S'hereta? | Matís |
|---|---|---|
Camps public i protected |
Sí | Accessibles directament des de la subclasse |
Camps private |
Existeixen, però no són accessibles | L'objecte els conté; la subclasse ha de fer servir mètodes per arribar-hi |
| Camps i mètodes de paquet (default) | Només si la subclasse és al mateix paquet | Vegeu 03-07 |
Mètodes public i protected |
Sí | Es poden sobreescriure |
Mètodes static |
S'hereten, però no se sobreescriuen | S'"amaguen" (lliçó 03-06) |
| Constructors | No | Cal declarar-los a cada subclasse, encara que es poden invocar amb super(...) |
| Blocs d'inicialització | S'executen com a part de la construcció | No s'"hereten" com a membres |
El punt que més confon és el dels camps private. Un objecte Revista sí que conté els camps privats de Material —ocupen memòria i s'inicialitzen—, però el codi de Revista no els pot anomenar. Ha de fer servir els mètodes que la superclasse exposi. No és una limitació capriciosa: és encapsulament (03-07), i permet canviar la representació interna de Material sense trencar les seves filles.
L'altre punt és el dels constructors: no s'hereten. Si Material té un constructor amb tres paràmetres, new Revista(a, b, c) no funciona per art de màgia; Revista ha de declarar el seu propi constructor.
super: el constructor de la superclasse
super: el constructor de la superclassesuper(...) invoca un constructor de la superclasse. I és obligatori en el sentit següent:
Tot constructor d'una subclasse comença cridant un constructor de la seva superclasse. Si no l'escrius tu, el compilador insereix una crida implícita a
super()sense arguments.
public class Material {
public final String titol;
public final String referencia;
public boolean disponible;
public Material(String titol, String referencia) {
this.titol = titol;
this.referencia = referencia;
this.disponible = true;
}
}public class Revista extends Material {
public final int numero;
public Revista(String titol, String referencia, int numero) {
super(titol, referencia); // PRIMERA sentencia, obligatoria aqui
this.numero = numero;
}
}Regles, bessones de les de this(...) que vas veure a 03-04:
super(...)ha de ser la primera sentència del constructor.- No poden coexistir
super(...)ithis(...)al mateix constructor: si fas servirthis(...), la cadena acabarà en algun constructor que cridisuper(...). - Si la superclasse no té constructor sense arguments i la subclasse no crida explícitament
super(...), hi ha error de compilació:
error: constructor Material in class Material cannot be applied to given types; required: String,String found: no arguments
Aquest error, molt freqüent, té una lectura clara: "has creat una subclasse d'una cosa que exigeix títol i referència, però no els hi estàs donant".
super: accedir a membres de la superclasse
super: accedir a membres de la superclassesuper també serveix, fora dels constructors, per arribar a un membre de la superclasse que la subclasse ha sobreescrit:
public class Material {
public String descriure() {
return titol + " (ref. " + referencia + ")";
}
}
public class Revista extends Material {
public int numero;
@Override
public String descriure() {
return super.descriure() + " - numero " + numero; // reutilitza i amplia
}
}Aquest patró —cridar super.metode() i afegir-hi alguna cosa— és un dels usos més valuosos de l'herència: no dupliques la lògica de la superclasse, l'estens.
Revista r = new Revista("Java Magazine", "REV-2024-42", 42);
System.out.println(r.descriure());
// Java Magazine (ref. REV-2024-42) - numero 42
- Ordre de construcció en una jerarquia
Quan crees un objecte d'una subclasse, la construcció va de dalt a baix: primer es construeix la part heretada, després la pròpia. Comprova-ho amb traces:
public class Material {
public Material() {
System.out.println("2. Constructor de Material");
}
{ System.out.println("1. Bloc d'instancia de Material"); }
}
public class Llibre extends Material {
public Llibre() {
super(); // implicit si no s'escriu
System.out.println("4. Constructor de Llibre");
}
{ System.out.println("3. Bloc d'instancia de Llibre"); }
}Sortida:
1. Bloc d'instancia de Material 2. Constructor de Material 3. Bloc d'instancia de Llibre 4. Constructor de Llibre
flowchart TD
A["new Llibre()"] --> B["Constructor de Llibre
crida super() com a primera sentencia"]
B --> C["Blocs i inicialitzadors de Material"]
C --> D["Cos del constructor de Material"]
D --> E["Blocs i inicialitzadors de Llibre"]
E --> F["Cos del constructor de Llibre"]
F --> G["Objecte complet"]
Aquest ordre explica el consell que va quedar pendent a 03-04: no cridis mètodes sobreescrivibles des d'un constructor. Si Material cridés al seu constructor un mètode que Llibre sobreescriu, s'executaria el codi de Llibre abans que els camps de Llibre estiguessin inicialitzats, i aquest veuria null i zeros. Demostració:
public class Material {
public Material() {
System.out.println("Material construeix: " + descriure()); // PERILL
}
public String descriure() { return "material generic"; }
}
public class Llibre extends Material {
private final String autor;
public Llibre(String autor) {
super();
this.autor = autor;
}
@Override
public String descriure() { return "llibre de " + autor; }
}El camp autor és final i acabarà valent "Joshua Bloch", però en l'instant de la crida encara val null. Regla pràctica: des d'un constructor, crida només mètodes private, static o final.
- Sobreescriptura de mètodes i
@Override
@OverrideSobreescriure (override) és redefinir a la subclasse un mètode heretat, amb la mateixa signatura, per canviar-ne el comportament.
public class Material {
public int getDiesPrestec() {
return 15;
}
}
public class Dvd extends Material {
@Override
public int getDiesPrestec() {
return 3; // els DVD circulen mes rapid
}
}Regles d'una sobreescriptura vàlida:
| Element | Regla |
|---|---|
| Nom i paràmetres | Idèntics (mateixa signatura) |
| Tipus de retorn | Igual, o un subtipus de l'original (retorn covariant) |
| Visibilitat | Igual o més permissiva (protected → public sí; public → private no) |
Mètode final |
No es pot sobreescriure |
Mètode static |
No se sobreescriu, s'amaga (03-06) |
Mètode private |
No s'hereta, així que no se sobreescriu |
L'anotació @Override no és obligatòria, però l'has de posar sempre. La seva funció és demanar al compilador que verifiqui que realment estàs sobreescrivint alguna cosa. Sense ella, un error tipogràfic es converteix en un mètode nou que ningú no crida:
public class Dvd extends Material {
public int getDiesPrestecs() { // compte! "Prestecs" amb S
return 3;
}
}Aquest codi compila perfectament i no sobreescriu res: els DVD continuaran tenint 15 dies de termini, i descobrir per què et costarà una tarda. Amb @Override al davant, el compilador falla immediatament:
Una anotació que s'escriu en un segon i evita tota una classe d'errors silenciosos. Les anotacions en general s'estudien a la lliçó 10-02.
- Sobreescriure enfront de sobrecarregar
Aquesta és una de les confusions més persistents de qui aprèn Java, agreujada pel fet que els termes sonen molt semblants. La taula ho deixa clar:
| Aspecte | Sobrecàrrega (overload) | Sobreescriptura (override) |
|---|---|---|
| On passa | A la mateixa classe (o heretant) | Entre superclasse i subclasse |
| Signatura | Diferent llista de paràmetres | Idèntica |
| Nom | Igual | Igual |
| Tipus de retorn | Pot canviar lliurement | Igual o subtipus |
| Quan es decideix | En compilació, segons el tipus declarat | En execució, segons l'objecte real |
| Anotació | Cap | @Override |
| Per a què serveix | Oferir variants de la mateixa operació | Especialitzar el comportament heretat |
| Exemple | calcularMulta() i calcularMulta(int) |
Dvd.getDiesPrestec() sobre Material.getDiesPrestec() |
Exemple amb totes dues coses alhora:
public class Material {
public double calcularMulta(int diesRetard) { return diesRetard * 0.25; }
}
public class Dvd extends Material {
@Override
public double calcularMulta(int diesRetard) { // SOBREESCRIPTURA
return diesRetard * 0.50;
}
public double calcularMulta(int diesRetard, double descompte) { // SOBRECARREGA
return calcularMulta(diesRetard) * (1 - descompte);
}
}La fila més important de la taula és la de "quan es decideix": la sobrecàrrega la resol el compilador mirant els tipus escrits al codi; la sobreescriptura la resol la JVM en execució mirant l'objecte real. Aquesta diferència és el cor del polimorfisme, i és el tema de la lliçó següent.
final en classes i mètodes
final en classes i mètodesfinal té tres usos en Java, i ja en coneixes dos:
| Aplicat a | Significa |
|---|---|
| Variable o camp | No es pot reassignar (mòduls 1 i 03-04) |
| Mètode | No es pot sobreescriure en cap subclasse |
| Classe | No es pot estendre: no admet subclasses |
public class Material {
/** El calcul del sostre legal no es pot alterar en cap subclasse. */
public final double aplicarSostre(double quantitat) {
return Math.min(quantitat, MULTA_MAXIMA);
}
}Per a què serveix? Per protegir invariants. Si una regla s'ha de complir sempre, sense excepció, un mètode final impedeix que una subclasse se la salti. Les classes final més famoses de Java són String i Integer: si poguessis estendre String i sobreescriure'n els mètodes, la seguretat de tot el llenguatge se n'aniria en orris.
El consell professional, formulat per Joshua Bloch a Java Eficaç (sí, el llibre del catàleg de BiblioTech): dissenya per a l'herència i documenta-la, o prohibeix-la. Una classe que no s'ha pensat explícitament per ser estesa és millor declarar-la final.
- Tota classe hereta d'
Object
ObjectUna regla del llenguatge que probablement ja has notat: si una classe no declara extends, hereta implícitament de java.lang.Object.
Per això pots escriure això sense haver definit res:
Llibre l = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
System.out.println(l.toString()); // com.nexussoftware...Llibre@1b6d3586
System.out.println(l.equals(l)); // true
System.out.println(l.hashCode()); // 460141958La jerarquia real de BiblioTech, per tant, és aquesta:
classDiagram
Object <|-- Material
Material <|-- Llibre
Material <|-- Revista
Material <|-- Dvd
Object <|-- Empleat
Object <|-- Prestec
class Object {
+toString() String
+equals(Object) boolean
+hashCode() int
+getClass() Class
}
Els mètodes que Object regala funcionen, però el seu comportament per defecte rarament és el que vols: toString() imprimeix un críptic Llibre@1b6d3586 i equals compara adreces de memòria, de manera que dos llibres amb el mateix ISBN són diferents. Sobreescriure'ls correctament és el tema de la lliçó 03-09, que tanca aquest mòdul.
- Herència enfront de composició
L'herència és tan còmoda que se n'abusa. L'alternativa és la composició: en lloc de "ser un", l'objecte té un col·laborador a qui delega la feina.
| Criteri | Herència | Composició |
|---|---|---|
| Relació | "és un" | "té un" |
| Sintaxi | class B extends A |
class B { private A a; } |
| Acoblament | Fort: B depèn dels detalls interns d'A | Feble: B només fa servir l'API pública d'A |
| Es decideix en | Compilació (no canvia mai) | Execució (es pot substituir el col·laborador) |
| Reutilitza | Tota la implementació d'A, vulgui o no | Només el que decideixi fer servir |
| Risc | Canvis a A trenquen B | Baix |
Un cas on l'herència seria un error. Imagina que vols que Prestec reutilitzi el codi de Llibre per no repetir el títol i la referència, i escrius:
public class Prestec extends Llibre { // ERROR DE DISSENY
public Empleat empleat;
public int diesTranscorreguts;
}Compila. I és un desastre, per tres raons:
- La frase és falsa. Un préstec no és un llibre; un préstec té un llibre. Si la frase "és un" sona estranya en dir-la en veu alta, no facis servir herència.
- Hereta operacions absurdes.
prestec.prestar()iprestec.retornar()passen a existir, i no signifiquen res. Estàs ampliant la superfície pública amb mètodes sense sentit. - Trenca el polimorfisme. Qualsevol codi que rebi un
Llibreacceptaria unPrestec, i un catàleg de llibres podria acabar contenint préstecs.
La forma correcta és la que ja tens:
public class Prestec { // sense extends
public final Material material; // TE un material
public final Empleat empleat; // TE un empleat
}Regla pràctica que se cita constantment a la indústria: prefereix composició a herència. Fes servir herència només quan la subclasse sigui genuïnament substituïble per la superclasse en qualsevol context —el que es coneix com a principi de substitució de Liskov— i quan vulguis aprofitar el polimorfisme.
- Jerarquies profundes i la fragilitat de la classe base
Dos problemes que apareixen en projectes reals i que convé anticipar:
Jerarquies profundes. Cadenes com Object → Material → MaterialFisic → MaterialImpres → Publicacio → PublicacioPeriodica → Revista fan que entendre Revista exigeixi llegir sis fitxers, que un canvi a qualsevol nivell es propagui cap avall i que sigui impossible saber d'on surt un mètode concret. Criteri sa: dos o tres nivells com a màxim; si en necessites més, probablement el que busques és composició o interfícies (04-01).
Fragilitat de la classe base (fragile base class problem). Una subclasse depèn no només de què fa la superclasse, sinó de com ho fa. Un canvi intern inofensiu la pot trencar:
public class Material {
public int prestecsRegistrats;
public void registrar() { prestecsRegistrats++; }
public void registrarDiversos(int n) {
for (int i = 0; i < n; i++) {
registrar(); // detall d'implementacio
}
}
}
public class Dvd extends Material {
public int vegadesRegistratDesDeDvd;
@Override
public void registrar() {
vegadesRegistratDesDeDvd++;
super.registrar();
}
@Override
public void registrarDiversos(int n) {
vegadesRegistratDesDeDvd += n; // suma n...
super.registrarDiversos(n); // ...i super crida n vegades registrar()
}
}dvd.registrarDiversos(3) incrementa vegadesRegistratDesDeDvd en 6, no en 3: la superclasse crida internament un mètode que la subclasse ha sobreescrit. I si demà algú optimitza registrarDiversos per fer prestecsRegistrats += n sense cridar registrar(), el comptador de la subclasse passarà a valer 3. El mateix codi, resultat diferent, sense haver tocat la subclasse. Aquesta és la fragilitat: la subclasse depèn de detalls interns que ningú no va prometre mantenir.
La defensa: documentar exhaustivament quins mètodes crida internament la classe base, declarar final el que no s'hagi de tocar, i preferir composició quan no necessitis polimorfisme.
- BiblioTech: la jerarquia de materials prestables
Apliquem-ho tot al projecte. El model objectiu:
classDiagram
class Material {
+String titol
+String referencia
+boolean disponible
+MULTA_MAXIMA double
+LLINDAR_LLEU int
+getDiesPrestec() int
+getTarifaDiaria() double
+calcularDiesRetard(int) int
+calcularMulta(int) double
+classificarGravetat(int) String
+prestar() void
+retornar() void
+descriure() String
+getTipus() String
}
class Llibre {
+String autor
+int anyPublicacio
+getIsbn() String
}
class Revista {
+int numero
+String periodicitat
}
class Dvd {
+int duracioMinuts
}
Material <|-- Llibre
Material <|-- Revista
Material <|-- Dvd
La classe base Material
package com.nexussoftware.bibliotech.domini;
/**
* Material prestable del cataleg de Nexus Software.
*
* Defineix el que es comu a tots els suports: identificacio, disponibilitat i
* el calcul de multes. Les subclasses nomes declaren en que es diferencien:
* el termini de prestec i la tarifa diaria.
*/
public class Material {
/** Termini estandard de prestec, en dies. El fan servir els llibres. */
public static final int DIES_PRESTEC = 15;
/** Tarifa estandard per dia de retard, en euros. */
public static final double TARIFA_DIARIA = 0.25;
/** Import maxim d'una multa, comu a tots els materials. */
public static final double MULTA_MAXIMA = 20.0;
/** Retard maxim, en dies, que es considera lleu. */
public static final int LLINDAR_LLEU = 7;
public static int materialsCreats = 0;
public final String titol;
public final String referencia;
public boolean disponible;
public Material(String titol, String referencia, boolean disponible) {
this.titol = (titol == null || titol.isBlank()) ? "Sense titol" : titol.trim();
this.referencia = (referencia == null || referencia.isBlank())
? "000-0000000000" : referencia.trim();
this.disponible = disponible;
materialsCreats++;
}
// ---------- Parametres que cada suport redefineix ----------
/** @return dies de termini d'aquest tipus de material. */
public int getDiesPrestec() {
return DIES_PRESTEC;
}
/** @return cost per dia de retard d'aquest tipus de material. */
public double getTarifaDiaria() {
return TARIFA_DIARIA;
}
/** @return nom llegible del suport, per a llistats. */
public String getTipus() {
return "Material";
}
// ---------- Regles comunes, escrites UNA sola vegada ----------
/** @return dies de retard, saturats a zero, segons el termini d'aquest material. */
public int calcularDiesRetard(int diesTranscorreguts) {
return Math.max(0, diesTranscorreguts - getDiesPrestec());
}
/** @return multa en euros, amb el sostre comu aplicat. */
public double calcularMulta(int diesTranscorreguts) {
int retard = calcularDiesRetard(diesTranscorreguts);
return Math.min(retard * getTarifaDiaria(), MULTA_MAXIMA);
}
/** @return "SENSE RETARD", "LLEU" o "GREU". */
public String classificarGravetat(int diesTranscorreguts) {
int retard = calcularDiesRetard(diesTranscorreguts);
if (retard == 0) return "SENSE RETARD";
if (retard <= LLINDAR_LLEU) return "LLEU";
return "GREU";
}
public boolean estaDisponible() { return disponible; }
public void prestar() {
if (!disponible) {
System.out.println("AVIS: '" + titol + "' ja estava prestat.");
return;
}
disponible = false;
}
public void retornar() {
if (disponible) {
System.out.println("AVIS: '" + titol + "' ja estava disponible.");
return;
}
disponible = true;
}
/** @return descripcio llegible; les subclasses l'amplien amb super.descriure(). */
public String descriure() {
return titol + " (ref. " + referencia + ")";
}
}Fixa't en la peça clau del disseny: calcularMulta no fa servir les constants directament, sinó que crida getDiesPrestec() i getTarifaDiaria(). Com que aquests mètodes es poden sobreescriure, la fórmula s'escriu una sola vegada i serveix per a tots els suports. És el mecanisme que la lliçó següent formalitzarà amb el nom de despatx dinàmic.
Les tres subclasses
package com.nexussoftware.bibliotech.domini;
/** Llibre tecnic del cataleg. Fa servir el termini i la tarifa estandard. */
public class Llibre extends Material {
public final String autor;
public final int anyPublicacio;
public Llibre(String titol, String autor, String isbn,
int anyPublicacio, boolean disponible) {
super(titol, isbn, disponible); // l'ISBN es la referencia del llibre
this.autor = (autor == null || autor.isBlank()) ? "Desconegut" : autor.trim();
if (anyPublicacio < 1450 || anyPublicacio > 2100) {
System.out.println("AVIS: any invalid a '" + titol + "'. Es registra com a 0.");
this.anyPublicacio = 0;
} else {
this.anyPublicacio = anyPublicacio;
}
}
/** Alta habitual: un llibre nou entra disponible. */
public Llibre(String titol, String autor, String isbn, int anyPublicacio) {
this(titol, autor, isbn, anyPublicacio, true);
}
/** L'ISBN es la referencia d'un llibre; es mante el nom del domini. */
public String getIsbn() { return referencia; }
@Override
public String getTipus() { return "Llibre"; }
@Override
public String descriure() {
return super.descriure() + " - " + autor + ", " + anyPublicacio;
}
// No sobreescriu getDiesPrestec() ni getTarifaDiaria():
// hereta els valors estandard de 15 dies i 0,25 EUR/dia.
}package com.nexussoftware.bibliotech.domini;
/** Revista tecnica. Circula molt, aixi que el seu termini es mes curt i la tarifa menor. */
public class Revista extends Material {
public static final int DIES_PRESTEC_REVISTA = 7;
public static final double TARIFA_DIARIA_REVISTA = 0.10;
public final int numero;
public final String periodicitat;
public Revista(String titol, String referencia, int numero, String periodicitat) {
super(titol, referencia, true);
this.numero = Math.max(0, numero);
this.periodicitat = (periodicitat == null || periodicitat.isBlank())
? "Desconeguda" : periodicitat.trim();
}
@Override
public int getDiesPrestec() { return DIES_PRESTEC_REVISTA; }
@Override
public double getTarifaDiaria() { return TARIFA_DIARIA_REVISTA; }
@Override
public String getTipus() { return "Revista"; }
@Override
public String descriure() {
return super.descriure() + " - n." + numero + " (" + periodicitat + ")";
}
}package com.nexussoftware.bibliotech.domini;
/** DVD de formacio. Poques copies: termini molt curt i tarifa doble. */
public class Dvd extends Material {
public static final int DIES_PRESTEC_DVD = 3;
public static final double TARIFA_DIARIA_DVD = 0.50;
public final int duracioMinuts;
public Dvd(String titol, String referencia, int duracioMinuts) {
super(titol, referencia, true);
this.duracioMinuts = Math.max(0, duracioMinuts);
}
@Override
public int getDiesPrestec() { return DIES_PRESTEC_DVD; }
@Override
public double getTarifaDiaria() { return TARIFA_DIARIA_DVD; }
@Override
public String getTipus() { return "DVD"; }
@Override
public String descriure() {
return super.descriure() + " - " + duracioMinuts + " min";
}
}Comparativa dels tres suports
| Suport | Termini | Tarifa/dia | Multa als 10 dies transcorreguts | Camps propis |
|---|---|---|---|---|
Llibre |
15 dies | 0,25 € | 0,00 € (encara en termini) | autor, anyPublicacio |
Revista |
7 dies | 0,10 € | 0,30 € (3 dies de retard) | numero, periodicitat |
Dvd |
3 dies | 0,50 € | 3,50 € (7 dies de retard) | duracioMinuts |
Prova a BiblioTechApp
Llibre javaEficac = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
Revista javaMagazine = new Revista("Java Magazine", "REV-2024-42", 42, "Bimestral");
Dvd cursSpring = new Dvd("Curs de Spring", "DVD-0007", 240);
System.out.printf("%-9s %-22s %7s %8s %10s %10s%n",
"TIPUS", "TITOL", "TERMINI", "TARIFA", "MULTA 10d", "GRAVETAT");
imprimirFitxa(javaEficac);
imprimirFitxa(javaMagazine);
imprimirFitxa(cursSpring);
System.out.println();
System.out.println(javaEficac.descriure());
System.out.println(javaMagazine.descriure());
System.out.println(cursSpring.descriure());private static void imprimirFitxa(Material m) {
System.out.printf("%-9s %-22s %5d d %7.2f %9.2f %s%n",
m.getTipus(), m.titol, m.getDiesPrestec(), m.getTarifaDiaria(),
m.calcularMulta(10), m.classificarGravetat(10));
}Sortida:
TIPUS TITOL TERMINI TARIFA MULTA 10d GRAVETAT Llibre Java Eficac 15 d 0,25 0,00 SENSE RETARD Revista Java Magazine 7 d 0,10 0,30 LLEU DVD Curs de Spring 3 d 0,50 3,50 LLEU Java Eficac (ref. 978-0000000001) - Joshua Bloch, 2018 Java Magazine (ref. REV-2024-42) - n.42 (Bimestral) Curs de Spring (ref. DVD-0007) - 240 min
Observa una cosa que potser t'ha passat desapercebuda: el mètode imprimirFitxa rep un paràmetre de tipus Material i li passem un Llibre, una Revista i un Dvd. Funciona, i cadascun respon segons el seu propi tipus. Això és polimorfisme, el tema de la lliçó següent.
Nota sobre l'evolució del projecte. La classe
Materialtal com està escrita es pot instanciar (new Material(...)), i això no té sentit: a la biblioteca no hi ha "materials genèrics", hi ha llibres, revistes i DVD. A la lliçó 04-02 convertiràsMaterialen una classe abstracta, que impedeix crear instàncies soltes i permet declarar operacions sense cos que les subclasses estan obligades a implementar. Per ara, consideraMaterialuna bastida funcional.
Errors Habituals i Consells
- Oblidar
@Override. És l'error que produeix errors més difícils de trobar: un mètode mal escrit no sobreescriu res i la superclasse continua manant. Posa@Overridesempre. - Creure que la subclasse pot accedir als camps
privatede la mare. Existeixen a l'objecte, però no són accessibles. Si la subclasse els necessita, la superclasse ha d'exposar un mètode (o declarar-losprotected, amb compte; vegeu 03-07). - Esperar que els constructors s'heretin. No s'hereten. Cada subclasse declara els seus i crida
super(...). - Fer servir herència per reutilitzar codi, sense relació "és un". El cas
Prestec extends Llibrede l'apartat 11. Si només vols reutilitzar, compon. - Cridar mètodes sobreescrivibles des del constructor. El camp encara val
null. Documentat a l'apartat 6. - Canviar la visibilitat a menys permissiva en sobreescriure.
public→protectedno compila: trencaria el contracte de la superclasse. - Jerarquies de cinc o sis nivells. Costa més entendre-les que el problema que resolen.
- Consell: digues la frase en veu alta. "Un DVD és un material prestable": sona bé, herència. "Un préstec és un llibre": sona malament, composició.
- Consell: puja a la superclasse només el que sigui comú a totes les subclasses. Si un camp el fan servir dues de tres, no pertany a la classe base.
- Consell: fes servir la vista de jerarquia de l'IDE (
Ctrl+Ha IntelliJ) per veure d'un cop d'ull qui estén qui i quins mètodes se sobreescriuen.
Exercicis
Exercici 1: afegir un suport nou
Nexus Software incorpora audiollibres al catàleg: es presten 10 dies, amb una tarifa de 0,15 €/dia, i tenen un camp propi narrador i un altre duracioMinuts. Crea la classe AudioLlibre com a subclasse de Material, sobreescrivint el que calgui i sense tocar ni una línia de Material, Llibre, Revista o Dvd. Afegeix-la a la taula comparativa de BiblioTechApp i comprova que la multa als 20 dies transcorreguts és d'1,50 €.
Exercici 2: traçar l'ordre de construcció
Escriu una jerarquia de tres nivells (Material → Llibre → LlibreSignat) on cada classe tingui un bloc d'inicialització d'instància i un constructor, tots dos amb un System.out.println identificatiu. Predigues la sortida de new LlibreSignat(...) abans d'executar-la i després verifica-la. A continuació, provoca deliberadament el problema de l'apartat 6: fes que el constructor de Material cridi un mètode que LlibreSignat sobreescrigui fent servir un dels seus camps, i explica el resultat.
Exercici 3: detectar herència mal aplicada
Un company proposa aquestes quatre jerarquies per a BiblioTech. Per a cadascuna, decideix si l'herència és adequada i, si no ho és, proposa l'alternativa correcta amb composició:
class Empleat extends Material { }
class Bibliotecari extends Empleat { }
class Cataleg extends Llibre { }
class LlibreDescatalogat extends Llibre { }Solucions
Solució 1
package com.nexussoftware.bibliotech.domini;
/** Audiollibre del cataleg. Termini intermedi i tarifa reduida. */
public class AudioLlibre extends Material {
public static final int DIES_PRESTEC_AUDIO = 10;
public static final double TARIFA_DIARIA_AUDIO = 0.15;
public final String narrador;
public final int duracioMinuts;
public AudioLlibre(String titol, String referencia,
String narrador, int duracioMinuts) {
super(titol, referencia, true);
this.narrador = (narrador == null || narrador.isBlank())
? "Desconegut" : narrador.trim();
this.duracioMinuts = Math.max(0, duracioMinuts);
}
@Override
public int getDiesPrestec() { return DIES_PRESTEC_AUDIO; }
@Override
public double getTarifaDiaria() { return TARIFA_DIARIA_AUDIO; }
@Override
public String getTipus() { return "Audiollibre"; }
@Override
public String descriure() {
return super.descriure() + " - narrat per " + narrador
+ ", " + duracioMinuts + " min";
}
}Ús:
AudioLlibre audio = new AudioLlibre("Refactoritzacio", "AUD-0003", "Nuria Vidal", 610);
imprimirFitxa(audio);
System.out.println(audio.descriure());
System.out.printf("Multa a 20 dies: %.2f EUR (%s)%n",
audio.calcularMulta(20), audio.classificarGravetat(20));Sortida:
Audiollibre Refactoritzacio 10 d 0,15 0,00 SENSE RETARD Refactoritzacio (ref. AUD-0003) - narrat per Nuria Vidal, 610 min Multa a 20 dies: 1,50 EUR (GREU)
Comprova els dos càlculs a mà, perquè són la millor manera de verificar que l'herència funciona com esperes. Amb 10 dies transcorreguts (els que fa servir imprimirFitxa), el termini de l'audiollibre és de 10 dies, així que el retard és 0: no hi ha multa i la gravetat és SENSE RETARD. Amb 20 dies transcorreguts, el retard és 20 − 10 = 10 dies; 10 × 0,15 = 1,50 €, molt per sota del sostre de 20 €; i com que 10 supera el LLINDAR_LLEU de 7 dies, la gravetat és GREU.
El més important de l'exercici: per afegir un suport nou has escrit una classe i zero modificacions al codi existent. imprimirFitxa funciona amb audiollibres sense haver-se assabentat que existeixen. Aquesta propietat —estendre sense modificar— és una de les raons per les quals la POO es va imposar al programari de gran escala, i la lliçó següent l'explota a fons.
Solució 2
package com.nexussoftware.bibliotech;
class MaterialTraca {
{ System.out.println("1. bloc de Material"); }
MaterialTraca() {
System.out.println("2. constructor de Material");
System.out.println(" descriure() -> " + descriure()); // PERILL
}
String descriure() { return "material generic"; }
}
class LlibreTraca extends MaterialTraca {
{ System.out.println("3. bloc de Llibre"); }
LlibreTraca() {
super();
System.out.println("4. constructor de Llibre");
}
}
class LlibreSignatTraca extends LlibreTraca {
private final String signant;
{ System.out.println("5. bloc de LlibreSignat"); }
LlibreSignatTraca(String signant) {
super();
this.signant = signant;
System.out.println("6. constructor de LlibreSignat");
}
@Override
String descriure() { return "llibre signat per " + signant; }
}Sortida:
1. bloc de Material 2. constructor de Material descriure() -> llibre signat per null 3. bloc de Llibre 4. constructor de Llibre 5. bloc de LlibreSignat 6. constructor de LlibreSignat
Explicació. La construcció va de l'arrel a la fulla: els blocs i el constructor de cada nivell s'executen sencers abans de passar al següent. Per això l'ordre és 1-2, 3-4, 5-6.
El problema de l'apartat 6 es veu a la tercera línia. El constructor de MaterialTraca crida descriure(), i com que l'objecte real és un LlibreSignatTraca, la JVM executa la versió sobreescrita (això és el despatx dinàmic de la lliçó següent). Però en aquell instant el camp signant encara no s'ha assignat —la seva assignació és al pas 6, quatre passos més tard—, així que val null i la sortida és llibre signat per null. I observa el detall inquietant: signant és final, un camp que "no pot canviar", i tanmateix l'hem observat amb dos valors diferents durant la vida de l'objecte.
Solució 3
class Empleat extends Material — INCORRECTA. "Un empleat és un material prestable" és fals i, a més, moralment qüestionable. Empleat heretaria prestar(), retornar() i calcularMulta(), tots sense sentit, i podria acabar en un catàleg de materials. Alternativa: cap relació. Empleat és una classe independent que apareix associada des de Prestec.
class Bibliotecari extends Empleat — CORRECTA (amb matisos). "Un bibliotecari és un empleat" és cert, i un bibliotecari tindria comportament propi (donar d'alta materials, cancel·lar multes) a més de l'heretat. És un ús legítim. El matís: si l'única diferència fos un camp String rol, l'herència seria excessiva; n'hi hauria prou amb un atribut. Introdueix la subclasse només quan aporti comportament diferent, no només una dada.
class Cataleg extends Llibre — INCORRECTA, i de les pitjors. Un catàleg no és un llibre: un catàleg conté llibres. Aquí es confon el contenidor amb el contingut, un error clàssic. Cataleg heretaria prestar(), getIsbn() i autor, absurds per a una col·lecció. Alternativa per composició, que és exactament el que faràs al mòdul 5:
public class Cataleg {
// Conte materials; l'emmagatzematge multiple arriba al modul 5.
// De moment seria un array; despres, un ArrayList o un HashMap.
}class LlibreDescatalogat extends Llibre — DUBTOSA. La frase "un llibre descatalogat és un llibre" és certa, així que l'herència no és absurda. Però pregunta't quin comportament canvia: probablement només que no es pot prestar. Això és un estat, no un tipus:
public class Llibre extends Material {
private boolean descatalogat;
@Override
public void prestar() {
if (descatalogat) {
System.out.println("AVIS: '" + titol + "' esta descatalogat.");
return;
}
super.prestar();
}
}El senyal d'alarma general: si un objecte pot passar d'una subclasse a una altra durant la seva vida —un llibre es descataloga i després es torna a catalogar—, l'herència és el mecanisme equivocat, perquè en Java un objecte no pot canviar de classe un cop creat. Els estats que canvien es modelen amb camps.
Conclusió
Has après el mecanisme amb què Java expressa l'especialització. Saps quan escau —només quan la frase "X és un Y" és certa sempre— i com s'escriu amb extends. Coneixes amb precisió què s'hereta i què no: els camps privats existeixen però no són accessibles, i els constructors no s'hereten mai. Domines super(...) per encadenar constructors i super.metode() per estendre comportament sense duplicar-lo, i has traçat l'ordre de construcció d'una jerarquia completa, inclòs el perill real de cridar mètodes sobreescrivibles des d'un constructor i trobar-te camps final valent null. Tens clara —i avalada per una taula— la diferència entre sobreescriure i sobrecarregar, i saps que @Override no és decoració, sinó la xarxa de seguretat que converteix un error tipogràfic silenciós en un error de compilació. Saps fer servir final per prohibir l'extensió, coneixes l'arrel universal Object i, sobretot, tens criteri per preferir composició a herència i per reconèixer les jerarquies profundes i la fragilitat de la classe base abans de patir-les.
BiblioTech ha canviat d'escala: ja no gestiona llibres, gestiona materials prestables. Material concentra la identificació, la disponibilitat i les regles de multa escrites una sola vegada, i Llibre, Revista i Dvd declaren únicament en què es diferencien: el seu termini i la seva tarifa. Afegir un audiollibre t'ha costat una classe nova i zero canvis al codi existent.
I ha aparegut una cosa que encara no té nom. imprimirFitxa(Material m) rep indistintament un llibre, una revista o un DVD, i cadascun respon segons el que realment és. calcularMulta està escrit una sola vegada, a Material, i tanmateix cobra 0,25 € al llibre i 0,50 € al DVD. Aquest mecanisme s'anomena polimorfisme, és el pilar que fa que l'herència valgui la pena, i és el tema de la lliçó següent: veuràs com la JVM decideix en temps d'execució quin mètode executar, què són l'upcasting i el downcasting, com instanceof amb patrons de Java modern substitueix els antics if sobre el tipus, i per què aquests if eren, des del principi, una olor de disseny.
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
