En tancar la lliçó anterior va quedar una escletxa oberta a BiblioTech: Material continua sent una classe instanciable. Res no impedeix escriure new Material("Alguna cosa", "REF-1", true) i obtenir un objecte que no és un llibre, ni una revista, ni un DVD; un objecte sense autor, sense número i sense durada, que respon "Material" quan li preguntes el seu tipus. És un objecte que no hauria d'existir, i la seva existència és un accident del disseny, no una decisió.
La classe abstracta tanca aquella porta. És una classe que declara explícitament que està incompleta: serveix com a base comuna —amb camps, constructor i codi compartit— però no es pot instanciar, i pot obligar les seves subclasses a implementar els mètodes que només elles poden decidir. Allà on la interfície diu "això és el que cal saber fer", la classe abstracta diu "això és el que ja està fet, i això és el que et toca a tu". En acabar aquesta lliçó, Material serà abstracta, calcularMulta estarà escrit una sola vegada per a tot el sistema mitjançant el patró Template Method, i sabràs triar amb criteri entre interfície, classe abstracta o —el més freqüent en el disseny professional— totes dues alhora.
Contingut
abstracten classes: què significa i què impedeix- Mètodes abstractes: l'obligació que s'hereta
- Una classe abstracta AMB estat: camps, constructor i mètodes concrets
- Per a què serveix el constructor d'una classe que no s'instancia?
- La subclasse incompleta també ha de ser abstracta
- Interfície enfront de classe abstracta: la taula completa
- Regla pràctica de decisió
- El patró Template Method
- Combinar-les totes dues:
AbstractXxx - BiblioTech:
Materialesdevé abstracta - Refactorització de
Llibre,RevistaiDvd - Errors Habituals i Consells
- Exercicis
abstract en classes: què significa i què impedeix
abstract en classes: què significa i què impedeixEl modificador abstract aplicat a una classe declara que aquella classe és conceptualment incompleta: representa una idea general de la qual no existeixen exemplars concrets.
Amb aquesta sola paraula, això deixa de compilar:
És important entendre què es prohibeix i què no:
| Operació | Permesa en una classe abstracta? |
|---|---|
new Material(...) |
No. Error de compilació |
Material m = new Llibre(...) |
Sí. És un tipus vàlid per declarar |
Material[] cataleg = new Material[10] |
Sí. L'array desa referències, no instàncies |
Tenir camps, fins i tot private |
Sí |
| Tenir constructor | Sí, i és fonamental (apartat 4) |
| Tenir mètodes concrets amb cos | Sí |
Tenir mètodes abstract sense cos |
Sí, i és el que la caracteritza |
Tenir mètodes static i final |
Sí |
| Estendre una altra classe | Sí |
| Implementar interfícies | Sí, i és molt habitual (apartat 9) |
| No tenir cap mètode abstracte | Sí, és legal (encara que poc comú) |
Aquest últim punt sorprèn: una classe pot ser abstract sense tenir ni un sol mètode abstracte. És perfectament vàlid i de vegades útil: la marques com a abstracta simplement per declarar que instanciar-la no té sentit, encara que tècnicament estigui completa.
El guany immediat és de modelatge. "Material" és una abstracció: a la biblioteca de Nexus Software hi ha llibres, revistes i DVD, però no hi ha "materials" a seques. El codi passa a dir la veritat sobre el domini, i el compilador la fa complir.
- Mètodes abstractes: l'obligació que s'hereta
Un mètode abstracte és un mètode declarat sense cos, acabat en punt i coma, que la classe promet que existirà però es nega a implementar:
public abstract class Material {
/** Cada suport declara la seva etiqueta llegible. */
public abstract String getTipus();
/** Cada suport declara el seu termini de prestec en dies. */
public abstract int getDiesPrestec();
/** Cada suport declara la seva tarifa diaria de multa. */
public abstract double getTarifaDiaria();
}Les regles són estrictes i val la pena tenir-les totes juntes:
- Un mètode abstracte només pot existir dins d'una classe abstracta (o d'una interfície). Si poses un mètode abstracte en una classe normal, l'error és
missing method body, or declare abstract. - Tota subclasse concreta els ha d'implementar tots. Si en falta un, no compila.
abstractés incompatible ambprivate,staticifinal. Ambprivateperquè la subclasse no el veuria; ambstaticperquè els mètodes estàtics no són polimòrfics (03-06); ambfinalperquèfinalprohibeix exactament el queabstractexigeix.
La diferència amb el disseny anterior és de garanties. Compara les dues versions de getDiesPrestec:
// ABANS (modul 3): implementacio de farciment
public int getDiesPrestec() { return DIES_PRESTEC; } // 15 dies "per si de cas"
// ARA: obligacio
public abstract int getDiesPrestec();Amb la primera, si demà afegeixes AudioLlibre extends Material i te'n descuides el termini, el programa compila i aplica silenciosament 15 dies. L'error no es detecta fins que un empleat es queixa d'una multa mal calculada. Amb la segona, no compila: l'error arriba al segon zero, amb el nom del mètode i el número de línia.
Aquesta és la diferència entre un valor per defecte i un contracte. El valor per defecte amaga l'oblit; el contracte el denuncia.
- Una classe abstracta AMB estat: camps, constructor i mètodes concrets
Aquí hi ha la diferència essencial amb una interfície: una classe abstracta pot desar dades. I per tant pot escriure codi compartit de debò, no només codi que s'apuntala en crides al contracte.
public abstract class Material {
// 1. Constants compartides
public static final double MULTA_MAXIMA = 20.0;
public static final int LLINDAR_LLEU = 7;
// 2. Estat d'instancia: IMPOSSIBLE en una interficie
private final String titol;
private final String referencia;
private boolean disponible;
// 3. Constructor: IMPOSSIBLE en una interficie
protected 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;
}
// 4. Metodes concrets que operen sobre aquest estat
public String getTitol() { return titol; }
public String getReferencia() { return referencia; }
public boolean estaDisponible() { return disponible; }
public boolean prestar() {
if (!disponible) { return false; }
disponible = false;
return true;
}
// 5. Metodes abstractes que les subclasses han d'omplir
public abstract String getTipus();
public abstract int getDiesPrestec();
public abstract double getTarifaDiaria();
}Les cinc seccions numerades resumeixen el repartiment: la classe abstracta aporta el que és comú (2, 3, 4), i delega el que varia (5). Cap interfície no pot oferir els punts 2 i 3.
És la resposta directa a una pregunta que potser et vas fer a 04-01: si un mètode default pot tenir cos, per a què necessito una classe abstracta? Perquè prestar() necessita el camp disponible. Un default no el pot tenir: hauria de cridar estaDisponible() i un hipotètic setDisponible(), ampliant el contracte amb un mutador que trenca l'encapsulament que tant vas cuidar a 03-07. La classe abstracta desa el camp, el manté private i exposa només les operacions de negoci.
- Per a què serveix el constructor d'una classe abstracta?
És el dubte clàssic: si new Material(...) està prohibit, quin sentit té aquell constructor?
La resposta és a la cadena d'inicialització que vas estudiar a 03-04: el constructor d'una subclasse sempre invoca primer el de la seva superclasse, explícitament amb super(...) o implícitament. Aquell constructor sí que s'executa, simplement mai tot sol.
public class Llibre extends Material {
public Llibre(String titol, String autor, String isbn, int any) {
super(titol, isbn, true); // executa el constructor de Material
this.autor = autor;
this.anyPublicacio = any;
}
}flowchart TD
A["new Llibre(...)"] --> B["es reserva memoria per a l'objecte complet"]
B --> C["constructor de Llibre: super(titol, isbn, true)"]
C --> D["constructor d'Object"]
D --> E["cos del constructor de Material: valida i inicialitza titol, referencia, disponible"]
E --> F["cos del constructor de Llibre: inicialitza autor i anyPublicacio"]
F --> G["objecte Llibre completament construit"]
Així que el constructor d'una classe abstracta té tres funcions molt concretes:
- Inicialitzar l'estat comú que la subclasse no ha de tocar (els camps són
private). - Centralitzar la validació. La comprovació de títol i referència s'escriu una vegada i l'apliquen
Llibre,RevistaiDvd, sense possibilitat de saltar-se-la. - Garantir els invariants de la part comuna des del primer instant de vida de l'objecte.
I hi ha una decisió de disseny a la signatura: declarar-lo protected en lloc de public. En una classe abstracta, public al constructor és enganyós —suggereix que algú el pot cridar des de fora, i no pot—, mentre que protected diu exactament la veritat: aquest constructor existeix per a les subclasses. És el conveni que segueix el mateix JDK.
- La subclasse incompleta també ha de ser abstracta
Què passa si una subclasse implementa només una part dels mètodes abstractes? Que continua sent incompleta, i Java t'obliga a dir-ho:
/** Base comuna dels suports que es consulten a sala i no surten de l'edifici. */
public abstract class MaterialDeConsulta extends Material {
protected MaterialDeConsulta(String titol, String referencia) {
super(titol, referencia, true);
}
/** Tots els materials de consulta comparteixen termini i tarifa... */
@Override public int getDiesPrestec() { return 1; }
@Override public double getTarifaDiaria() { return 1.0; }
// ... pero getTipus() segueix sense implementar: la classe segueix sent abstracta
}Si li lleves l'abstract a MaterialDeConsulta, el compilador diria:
error: MaterialDeConsulta is not abstract and does not override
abstract method getTipus() in MaterialL'obligació s'hereta cap avall fins que algú la compleix. Això permet jerarquies de diversos nivells on cada nivell resol el que sap i deixa la resta pendent:
classDiagram
class Material {
<<abstract>>
+getTipus()* String
+getDiesPrestec()* int
+getTarifaDiaria()* double
}
class MaterialDeConsulta {
<<abstract>>
+getDiesPrestec() int
+getTarifaDiaria() double
}
class Atlas {
+getTipus() String
}
Material <|-- MaterialDeConsulta
MaterialDeConsulta <|-- Atlas
Atlas és la primera classe concreta de la branca: implementa l'única cosa que quedava pendent i ja es pot instanciar.
- Interfície enfront de classe abstracta: la taula completa
Aquesta és la taula que es va anunciar a 04-01. Estudia-la sencera: cobreix les sis dimensions que realment decideixen.
| Dimensió | Interfície | Classe abstracta |
|---|---|---|
| Estat d'instància | Impossible. Només public static final |
Camps de qualsevol tipus i visibilitat |
| Constructor | No en té | Sí, i s'executa via super(...) |
| Herència múltiple | Una classe n'implementa diverses | Una classe n'estén una de sola |
| Visibilitat dels membres | Tot public (llevat dels private de Java 9, invisibles a fora) |
public, protected, package, private |
| Mètodes amb cos | Sí: default, static, private |
Sí, sense restriccions |
| Blocs d'inicialització | No | Sí, estàtics i d'instància |
Membres protected |
No | Sí: el mecanisme natural per a les subclasses |
| Evolució de l'API | Afegir un abstracte trenca; afegir un default no |
Afegir un mètode concret no trenca; un d'abstracte sí |
| Relació que expressa | "pot fer" (capacitat) | "és un" (identitat) |
| Acoblament que imposa | Mínim: no consumeix l'herència | Alt: consumeix l'única herència disponible |
| Exemple del JDK | Comparable, Runnable, List |
AbstractList, InputStream, Number |
Tres files mereixen comentari addicional:
Evolució de l'API. És asimètrica i sovint es confon. En una interfície, afegir un mètode abstracte trenca tots els implementadors, però un default no trenca ningú. En una classe abstracta, afegir un mètode concret és segur (totes les subclasses l'hereten), però afegir-ne un d'abstracte trenca totes les subclasses concretes. Cadascuna té la seva via segura de créixer.
Acoblament. És l'argument decisiu i sovint el que s'oblida. Si obligues els teus usuaris a estendre la teva classe abstracta, els gastes la seva única herència i no podran estendre res més. Una interfície no costa res en aquest sentit. Per això la recomanació professional és: el tipus públic és la interfície; la classe abstracta és una ajuda opcional.
Membres protected. Una classe abstracta pot oferir a les seves subclasses eines que la resta del món no veu. Una interfície no: tot el que declara és públic. Quan necessites aquest "canal privat" cap a les subclasses, la classe abstracta és l'única opció.
- Regla pràctica de decisió
flowchart TD
A["Necessito un tipus comu"] --> B{"Cal estat compartit o un constructor?"}
B -- "No" --> C{"El signaran classes sense parentiu?"}
B -- "Si" --> D["Classe abstracta"]
C -- "Si" --> E["Interficie"]
C -- "No" --> F{"Necessito codi compartit?"}
F -- "No" --> E
F -- "Si" --> G["Interficie mes classe abstracta base"]
D --> H{"Tambe vull que el signin altres?"}
H -- "Si" --> G
H -- "No" --> D
I en tres frases memoritzables:
- Interfície quan defineixes què es pot fer i vols que ho signin classes que no comparteixen família. És l'opció per defecte: comença sempre per aquí.
- Classe abstracta quan comparteixes estat, constructor i codi real entre classes que sí que formen una família genuïna.
- Totes dues quan vols les dues coses: la interfície com a tipus públic i la classe abstracta com a implementació base opcional. És el que fa el JDK i el que faràs a BiblioTech.
- El patró Template Method
Ara arriba un dels usos més potents de les classes abstractes. Observa aquest mètode, que ja existeix al teu Material des del mòdul 3:
public double calcularMulta(int diesTranscorreguts) {
return Math.min(calcularDiesRetard(diesTranscorreguts) * getTarifaDiaria(),
MULTA_MAXIMA);
}L'interessant és la seva estructura: defineix l'esquelet invariable de l'algorisme —calcular el retard, multiplicar-lo per la tarifa, aplicar el sostre— però delega en getTarifaDiaria(), que cada subclasse implementa a la seva manera. L'esquelet s'escriu una vegada; els passos variables, tantes vegades com suports hi hagi.
Això és exactament el patró Template Method: un mètode concret de la classe base defineix la seqüència de passos, i els passos que varien són mètodes abstractes que les subclasses omplen.
I hi ha un detall clau que fa el patró sòlid: el mètode plantilla es declara final.
/**
* PLANTILLA: la sequencia del calcul es politica de l'empresa
* i cap subclasse no la pot alterar.
*/
public final double calcularMulta(int diesTranscorreguts) {
int retard = calcularDiesRetard(diesTranscorreguts); // pas fix
double bruta = retard * getTarifaDiaria(); // PAS VARIABLE
return Math.min(bruta, MULTA_MAXIMA); // pas fix (sostre)
}Per què final? Perquè el sostre de 20 € i la fórmula són política de Nexus Software, no una decisió de cada suport. Sense final, un Dvd podria sobreescriure calcularMulta i saltar-se el sostre. Amb final, el compilador ho impedeix: les subclasses només poden influir a través dels forats que la plantilla els ofereix.
flowchart TD
A["calcularMulta(dies) FINAL a Material"] --> B["pas fix: calcularDiesRetard(dies)"]
B --> C["PAS VARIABLE: getTarifaDiaria()"]
C --> D["pas fix: aplicar MULTA_MAXIMA"]
D --> E["resultat"]
C -.-> F["Llibre retorna 0.25"]
C -.-> G["Revista retorna 0.10"]
C -.-> H["Dvd retorna 0.50"]
Repartir així les responsabilitats té un nom a la literatura: el principi de Hollywood — "no ens truquis, ja et trucarem nosaltres". La subclasse no controla el flux; es limita a omplir els forats que la classe base li reserva.
Aquest patró es diu així formalment perquè és un dels patrons de disseny clàssics. Aquí l'has descobert de manera natural, escrivint la classe que calia; a la lliçó 12-02 el veuràs formalitzat al costat de Strategy, Factory, Observer i els altres, amb la seva fitxa completa i les seves alternatives.
- Combinar-les totes dues:
AbstractXxx
AbstractXxxGairebé mai tries entre interfície i classe abstracta: fas servir totes dues en capes.
- La interfície és el tipus públic. El que declaren les variables, els paràmetres i els retorns.
- La classe abstracta implementa la interfície i ofereix una base parcial amb la feina repetitiva ja resolta.
- Les classes concretes estenen la base i només escriuen el que és seu.
I qui no vulgui la base, implementa la interfície directament. L'ajuda és opcional, i tota la gràcia és aquí.
El JDK està construït així, i el conveni de noms el delata:
| Interfície (tipus públic) | Base parcial (ajuda opcional) | Classe concreta |
|---|---|---|
List |
AbstractList |
ArrayList, LinkedList |
Map |
AbstractMap |
HashMap, TreeMap |
Set |
AbstractSet |
HashSet |
ArrayList és un AbstractList i és una List. Però pots escriure la teva pròpia List sense tocar AbstractList. Totes aquestes classes arriben al mòdul 5; el que importa avui és reconèixer el patró estructural.
A BiblioTech el repartiment queda exactament igual:
classDiagram
class Prestable {
<<interface>>
+prestar() boolean
+retornar() boolean
+estaDisponible() boolean
+getDiesPrestec() int
}
class Notificable {
<<interface>>
+getCanalAvis() String
+generarAvis(int) String
}
class Material {
<<abstract>>
-titol String
-referencia String
-disponible boolean
+calcularMulta(int)$ double
+getTipus()* String
}
class SalaReunions
Prestable <|.. Material
Notificable <|.. Material
Prestable <|.. SalaReunions
Material <|-- Llibre
Material <|-- Revista
Material <|-- Dvd
SalaReunions implementa Prestable sense passar per Material: no vol la base, i no la necessita. És la prova que l'ajuda és opcional.
- BiblioTech:
Material esdevé abstracta
Material esdevé abstractaAquest és l'estat final de la classe després d'aplicar tot el que s'ha après.
package com.nexussoftware.bibliotech.domini;
/**
* Base abstracta de tot material del cataleg de Nexus Software.
*
* <p>Defineix l'estat comu (titol, referencia, disponibilitat), les regles
* de negoci compartides i la plantilla del calcul de multes. Cada suport
* concret aporta el seu tipus, el seu termini i la seva tarifa.</p>
*/
public abstract class Material implements Prestable, Notificable {
public static final double MULTA_MAXIMA = 20.0;
public static final int LLINDAR_LLEU = 7;
private static int materialsCreats = 0;
private final String titol;
private final String referencia;
private boolean disponible;
/** Constructor protected: existeix per a les subclasses, no per a l'exterior. */
protected 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++;
}
// ---------- El que CADA suport ha de declarar ----------
/** @return etiqueta llegible del suport: "Llibre", "Revista", "DVD". */
public abstract String getTipus();
/** @return dies de termini de prestec d'aquest suport. */
@Override public abstract int getDiesPrestec();
/** @return euros de multa per dia de retard d'aquest suport. */
public abstract double getTarifaDiaria();
// ---------- Estat comu (implementat una sola vegada) ----------
public String getTitol() { return titol; }
public String getReferencia() { return referencia; }
@Override public boolean estaDisponible() { return disponible; }
public static int getMaterialsCreats() { return materialsCreats; }
// ---------- Regles de negoci: PLANTILLES final ----------
/** Dies de retard sobre el termini del suport. Mai negatiu. */
public final int calcularDiesRetard(int diesTranscorreguts) {
return Math.max(0, diesTranscorreguts - getDiesPrestec());
}
/**
* TEMPLATE METHOD. L'esquelet del calcul es politica d'empresa
* i per aixo es final; el pas variable es getTarifaDiaria().
*/
public final double calcularMulta(int diesTranscorreguts) {
int retard = calcularDiesRetard(diesTranscorreguts);
double bruta = retard * getTarifaDiaria();
return Math.min(bruta, MULTA_MAXIMA);
}
/** Classificacio del retard. A 04-07 deixara de retornar String. */
public final String classificarGravetat(int diesTranscorreguts) {
int retard = calcularDiesRetard(diesTranscorreguts);
if (retard == 0) { return "SENSE RETARD"; }
if (retard <= LLINDAR_LLEU) { return "LLEU"; }
return "GREU";
}
// ---------- Operacions de negoci (contracte Prestable) ----------
@Override
public boolean prestar() {
if (!disponible) {
System.out.println("AVIS: '" + titol + "' ja estava prestat.");
return false;
}
disponible = false;
return true;
}
@Override
public boolean retornar() {
if (disponible) {
System.out.println("AVIS: '" + titol + "' ja estava disponible.");
return false;
}
disponible = true;
return true;
}
// ---------- Contracte Notificable ----------
@Override public String getCanalAvis() { return "correu"; }
public int getDiesPreavis() { return 2; }
@Override
public String generarAvis(int diesTranscorreguts) {
return String.format("Avis per %s amb %d dies de preavis: '%s' acumula %.2f EUR.",
getCanalAvis(), getDiesPreavis(), titol,
calcularMulta(diesTranscorreguts));
}
// ---------- Representacio i identitat ----------
public String descriure() {
return getTipus() + " \"" + titol + "\" (ref. " + referencia + ")";
}
@Override
public boolean equals(Object o) {
if (this == o) { return true; }
if (o == null || getClass() != o.getClass()) { return false; }
Material altre = (Material) o;
return referencia.equals(altre.referencia);
}
@Override
public int hashCode() { return referencia.hashCode(); }
@Override
public String toString() {
return getTipus() + "[ref=" + referencia + ", titol=" + titol
+ ", disponible=" + disponible + "]";
}
}Quatre canvis que mereixen atenció:
abstract class:new Material(...)ja no compila.- Constructor
protected: diu la veritat sobre qui el pot cridar. getTipus(),getDiesPrestec()igetTarifaDiaria()són abstractes: desapareixen les constants de farcimentDIES_PRESTEC = 15iTARIFA_DIARIA = 0.25deMaterial. Cada suport declara les seves i ningú no se les pot descuidar.calcularDiesRetard,calcularMultaiclassificarGravetatsónfinal: són la política de l'empresa, no negociable per subclasse.
I observa descriure(): ara crida getTipus(), un mètode abstracte. Un mètode concret de la classe base invocant un mètode que encara no existeix. Funciona gràcies al despatx dinàmic de 03-06: en temps d'execució, this és sempre un objecte concret, i el seu getTipus() està implementat.
- Refactorització de
Llibre, Revista i Dvd
Llibre, Revista i DvdAra cada suport declara les seves tres decisions, i cap no es pot ometre.
package com.nexussoftware.bibliotech.domini;
/** Llibre tecnic del cataleg. Termini llarg i tarifa estandard. */
public class Llibre extends Material {
public static final int DIES_PRESTEC_LLIBRE = 15;
public static final double TARIFA_DIARIA_LLIBRE = 0.25;
private final String autor;
private final int anyPublicacio;
public Llibre(String titol, String autor, String isbn,
int anyPublicacio, boolean disponible) {
super(titol, isbn, disponible);
this.autor = (autor == null || autor.isBlank()) ? "Desconegut" : autor.trim();
this.anyPublicacio = (anyPublicacio < 1450 || anyPublicacio > 2100)
? 0 : anyPublicacio;
}
public Llibre(String titol, String autor, String isbn, int anyPublicacio) {
this(titol, autor, isbn, anyPublicacio, true);
}
public String getAutor() { return autor; }
public int getAnyPublicacio() { return anyPublicacio; }
public String getIsbn() { return getReferencia(); }
// --- Les tres decisions OBLIGATORIES ---
@Override public String getTipus() { return "Llibre"; }
@Override public int getDiesPrestec() { return DIES_PRESTEC_LLIBRE; }
@Override public double getTarifaDiaria() { return TARIFA_DIARIA_LLIBRE; }
@Override
public String descriure() {
return super.descriure() + " - " + autor + ", " + anyPublicacio;
}
}package com.nexussoftware.bibliotech.domini;
/** Revista tecnica. Circula molt: termini curt i tarifa baixa. */
public class Revista extends Material {
public static final int DIES_PRESTEC_REVISTA = 7;
public static final double TARIFA_DIARIA_REVISTA = 0.10;
private final int numero;
private 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();
}
public int getNumero() { return numero; }
public String getPeriodicitat() { return periodicitat; }
@Override public String getTipus() { return "Revista"; }
@Override public int getDiesPrestec() { return DIES_PRESTEC_REVISTA; }
@Override public double getTarifaDiaria() { return TARIFA_DIARIA_REVISTA; }
@Override public String getCanalAvis() { return "xat"; }
@Override public int getDiesPreavis() { return 1; }
@Override
public String descriure() {
return super.descriure() + " - n." + numero + " (" + periodicitat + ")";
}
}package com.nexussoftware.bibliotech.domini;
/** DVD de formacio. Poques copies: termini minim 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;
private final int duracioMinuts;
public Dvd(String titol, String referencia, int duracioMinuts) {
super(titol, referencia, true);
this.duracioMinuts = Math.max(0, duracioMinuts);
}
public int getDuracioMinuts() { return duracioMinuts; }
@Override public String getTipus() { return "DVD"; }
@Override public int getDiesPrestec() { return DIES_PRESTEC_DVD; }
@Override public double getTarifaDiaria() { return TARIFA_DIARIA_DVD; }
@Override public String getCanalAvis() { return "telefon"; }
@Override public int getDiesPreavis() { return 1; }
@Override
public String descriure() {
return super.descriure() + " - " + duracioMinuts + " min";
}
}Prova del resultat:
Material[] cataleg = {
new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Dvd("Refactoritzacio en directe", "DVD-0007", 95)
};
System.out.printf("%-10s %-8s %-8s %-10s %s%n",
"TIPUS", "TERMINI", "TARIFA", "MULTA@20d", "GRAVETAT");
for (Material m : cataleg) {
System.out.printf("%-10s %-8d %-8.2f %-10.2f %s%n",
m.getTipus(), m.getDiesPrestec(), m.getTarifaDiaria(),
m.calcularMulta(20), m.classificarGravetat(20));
}
// Material generic = new Material("X", "REF", true); // NO COMPILATIPUS TERMINI TARIFA MULTA@20d GRAVETAT Llibre 15 0,25 1,25 LLEU Revista 7 0,10 1,30 GREU DVD 3 0,50 8,50 GREU
Un únic calcularMulta, escrit una sola vegada, produint tres resultats diferents i correctes. I la línia comentada és la millora principal: l'objecte sense sentit ja no es pot crear.
Errors Habituals i Consells
Posar un mètode abstracte en una classe no abstracta. L'error és missing method body, or declare abstract. Si un mètode no es pot implementar a la classe base, la classe base és abstracta. No hi ha terme mitjà.
Intentar new sobre la classe abstracta. Material m = new Material(...) no compila. El que sí que és legal, i confon molta gent, és Material m = new Llibre(...): la classe abstracta és un tipus perfectament vàlid per declarar.
Declarar el constructor public en una classe abstracta. Compila, però menteix: suggereix un accés que no existeix. Fes servir protected.
Combinar abstract amb final, static o private. Les tres combinacions són contradictòries i el compilador les rebutja. abstract significa "cal sobreescriure'l"; final significa "no es pot", static significa "no és polimòrfic" i private significa "no es veu".
Descuidar que la subclasse parcial ha de continuar sent abstracta. Si implementes dos de tres mètodes abstractes, la classe resultant continua incompleta.
Triar classe abstracta per costum. És la trampa de disseny més cara del mòdul. En obligar a extends, gastes l'única herència dels teus usuaris. Comença sempre per la interfície i afegeix la base abstracta només si hi ha estat o codi real per compartir.
Cridar un mètode abstracte (o sobreescrivible) des del constructor de la base. És un error subtil i greu: quan s'executa el constructor de Material, els camps de Llibre encara no estan inicialitzats. Si Material cridés al seu constructor un descriure() que fa servir autor, obtindries null. Regla: des d'un constructor, invoca només mètodes private, static o final.
Consell: final al mètode plantilla. És el que converteix una jerarquia en un Template Method de debò. Si l'esquelet no és final, qualsevol subclasse es pot saltar la política.
Consell: documenta el contracte de cada mètode abstracte. El Javadoc d'un mètode abstracte és l'únic lloc on pots dir a qui l'implementi què s'espera d'ell: què ha de retornar, quins rangs són vàlids, si pot retornar null. És disseny per contracte (03-08) aplicat al punt exacte on més falta fa.
Exercicis
Exercici 1: AudioLlibre
Afegeix a BiblioTech un suport AudioLlibre extends Material amb un camp propi duracioHores (double) i un mètode getNarrador(). Termini: 10 dies. Tarifa: 0,15 €/dia. Canal d'avís: "correu" (l'heretat). Comprova que el compilador t'obliga a implementar els tres mètodes abstractes i integra el nou suport a l'array cataleg sense tocar el bucle que el recorre.
Exercici 2: Template Method per al rebut
Crea una classe abstracta InformeMaterial al paquet presentacio amb:
- Un mètode
public final String generar(Material m, int diesTranscorreguts)que retornicapcalera() + cos(m, dies) + peu(). capcalera()ipeu()com a mètodes concrets amb una implementació per defecte.cos(Material, int)com a mètode abstracte.
Crea dues subclasses: InformeBreu (una línia amb títol i multa) i InformeDetallat (títol, tipus, termini, retard, multa i gravetat, un per línia). Comprova que l'esquelet s'escriu una sola vegada.
Exercici 3: triar interfície o classe abstracta
Per a cada cas, decideix si faries servir interfície, classe abstracta o totes dues, i justifica-ho en una frase:
- Tot objecte de BiblioTech que es pugui exportar a un fitxer de text.
- La base comuna de tres tipus d'empleat (
Intern,Extern,Becari) que comparteixen nom, identificador i el límit de préstecs, però amb un màxim diferent. - La capacitat de comparar dos materials per ordenar-los.
- Un catàleg de préstecs amb diferents implementacions futures: en memòria, en fitxer i en base de dades.
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;
private final String narrador;
private final double duracioHores;
public AudioLlibre(String titol, String referencia,
String narrador, double duracioHores) {
super(titol, referencia, true); // validacio centralitzada a Material
this.narrador = (narrador == null || narrador.isBlank())
? "Desconegut" : narrador.trim();
this.duracioHores = Math.max(0.0, duracioHores);
}
public String getNarrador() { return narrador; }
public double getDuracioHores() { return duracioHores; }
// Els tres metodes que el compilador EXIGEIX:
@Override public String getTipus() { return "Audiollibre"; }
@Override public int getDiesPrestec() { return DIES_PRESTEC_AUDIO; }
@Override public double getTarifaDiaria() { return TARIFA_DIARIA_AUDIO; }
@Override
public String descriure() {
return super.descriure() + " - narrat per " + narrador
+ ", " + duracioHores + " h";
}
}Si ometes, per exemple, getTarifaDiaria():
error: AudioLlibre is not abstract and does not override
abstract method getTarifaDiaria() in MaterialI la integració:
Material[] cataleg = {
new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Dvd("Refactoritzacio en directe", "DVD-0007", 95),
new AudioLlibre("Patrons de Disseny", "AUD-0002", "Nuria Vidal", 12.5) // unica linia nova
};TIPUS TERMINI TARIFA MULTA@20d GRAVETAT Llibre 15 0,25 1,25 LLEU Revista 7 0,10 1,30 GREU DVD 3 0,50 8,50 GREU Audiollibre 10 0,15 1,50 GREU
El bucle no s'ha tocat. Un suport nou costa una classe i una línia de dades: zero modificacions a la lògica existent. I la novetat respecte al mòdul 3 és que el compilador ha garantit que vas declarar termini, tarifa i tipus. Amb la versió antiga de Material, descuidar la tarifa hauria cobrat silenciosament 0,25 €/dia.
Solució 2
package com.nexussoftware.bibliotech.presentacio;
import com.nexussoftware.bibliotech.domini.Material;
/** Base de tots els informes de material. Defineix l'esquelet; no el contingut. */
public abstract class InformeMaterial {
private static final String LINIA = "----------------------------------------";
/**
* TEMPLATE METHOD: l'estructura de l'informe es fixa (final)
* i l'unic pas variable es el cos.
*/
public final String generar(Material m, int diesTranscorreguts) {
return capcalera() + cos(m, diesTranscorreguts) + peu();
}
/** Pas concret: valid per defecte, sobreescrivible si cal. */
protected String capcalera() {
return LINIA + System.lineSeparator()
+ " BIBLIOTECH - NEXUS SOFTWARE" + System.lineSeparator()
+ LINIA + System.lineSeparator();
}
/** Pas concret. */
protected String peu() {
return LINIA + System.lineSeparator();
}
/** PAS VARIABLE: cada informe decideix que mostra. */
protected abstract String cos(Material m, int diesTranscorreguts);
}package com.nexussoftware.bibliotech.presentacio;
import com.nexussoftware.bibliotech.domini.Material;
/** Una sola linia: per a llistats llargs. */
public class InformeBreu extends InformeMaterial {
@Override
protected String cos(Material m, int diesTranscorreguts) {
return String.format(" %-28s %6.2f EUR%n",
m.getTitol(), m.calcularMulta(diesTranscorreguts));
}
}package com.nexussoftware.bibliotech.presentacio;
import com.nexussoftware.bibliotech.domini.Material;
/** Informe complet: per a incidencies i reclamacions. */
public class InformeDetallat extends InformeMaterial {
@Override
protected String cos(Material m, int diesTranscorreguts) {
StringBuilder sb = new StringBuilder();
sb.append(String.format(" %-14s %s%n", "Titol:", m.getTitol()));
sb.append(String.format(" %-14s %s%n", "Tipus:", m.getTipus()));
sb.append(String.format(" %-14s %d dies%n", "Termini:", m.getDiesPrestec()));
sb.append(String.format(" %-14s %d dies%n", "Retard:", m.calcularDiesRetard(diesTranscorreguts)));
sb.append(String.format(" %-14s %.2f EUR%n", "Multa:", m.calcularMulta(diesTranscorreguts)));
sb.append(String.format(" %-14s %s%n", "Gravetat:", m.classificarGravetat(diesTranscorreguts)));
return sb.toString();
}
}Material dvd = new Dvd("Refactoritzacio en directe", "DVD-0007", 95);
InformeMaterial breu = new InformeBreu();
InformeMaterial detallat = new InformeDetallat();
System.out.print(breu.generar(dvd, 20));
System.out.print(detallat.generar(dvd, 20));---------------------------------------- BIBLIOTECH - NEXUS SOFTWARE ---------------------------------------- Refactoritzacio en directe 8,50 EUR ---------------------------------------- ---------------------------------------- BIBLIOTECH - NEXUS SOFTWARE ---------------------------------------- Titol: Refactoritzacio en directe Tipus: DVD Termini: 3 dies Retard: 17 dies Multa: 8,50 EUR Gravetat: GREU ----------------------------------------
Quatre detalls de disseny: generar és final (l'estructura no es negocia); cos és protected perquè és un punt d'extensió per a les subclasses i no part de l'API pública; capcalera i peu són concrets i protected, així que una futura subclasse els pot sobreescriure sense veure-s'hi obligada; i la variable es declara InformeMaterial, no InformeBreu, així que canviar d'informe costa una paraula.
Solució 3
| Cas | Decisió | Justificació |
|---|---|---|
| 1. Exportable a text | Interfície (Exportable amb String aLinia()) |
És una capacitat que han de signar classes sense parentiu (Material, Empleat, Prestec, SalaReunions), i no requereix estat compartit |
| 2. Base de tres tipus d'empleat | Classe abstracta (Empleat amb getMaxPrestecs() abstracte) |
Comparteixen camps (nom, identificador, prestecsAcumulats), constructor amb validació i lògica real; formen una família genuïna i només varia un valor |
| 3. Comparar materials | Interfície, i a més una del JDK: Comparable<Material> |
És pur comportament sense estat, ja existeix a la biblioteca estàndard i no ha de consumir l'herència. S'aplica a 04-04 i 05-09 |
| 4. Catàleg amb diverses implementacions | Totes dues: interfície CatalegPrestecs més base AbstractCatalegPrestecs |
La interfície és el tipus públic del qual depenen les regles de negoci (inversió de dependències, 04-01); la base abstracta estalvia a les tres implementacions el codi repetit de validació i format, sense obligar ningú a fer-la servir |
El cas 4 és l'arquetip professional i el que veuràs una vegada i una altra a partir del mòdul 5: la interfície defineix el tipus, la classe abstracta ofereix ajuda opcional, i les classes concretes trien.
Conclusió
Has completat la parella que sosté el disseny en Java. Saps que abstract en una classe declara que està conceptualment incompleta i que impedeix new, sense impedir que la facis servir com a tipus de variables, paràmetres i arrays. Saps que un mètode abstracte és una obligació que es transmet cap avall fins que una subclasse concreta la compleix, i —el més important— per què aquella obligació val més que qualsevol implementació de farciment: converteix un oblit silenciós, que apareix setmanes després com una multa mal calculada, en un error de compilació instantani.
Entens el que una classe abstracta ofereix i una interfície no podrà oferir mai: camps d'instància, constructor i membres protected. Saps per a què serveix el constructor d'una classe que no s'instancia —s'executa sempre via super(...), centralitza la validació comuna i garanteix els invariants de la part compartida— i per què es declara protected. Tens la taula comparativa completa entre interfície i classe abstracta, inclosa la fila que més pesa a la pràctica: la classe abstracta consumeix l'única herència de qui la fa servir, i per això el camí professional comença sempre per la interfície.
I has descobert el Template Method escrivint-lo, no llegint-lo: un mètode final que fixa l'esquelet de l'algorisme i delega els passos variables en mètodes abstractes. És el que permet que la fórmula de la multa —retard per tarifa, amb sostre de 20 €— existeixi una sola vegada a tot BiblioTech i produeixi resultats diferents i correctes per a un llibre, una revista, un DVD i qualsevol suport que afegeixis demà. Aquest patró es formalitza a 12-02. Finalment, saps reconèixer el patró AbstractXxx del JDK, on interfície i classe abstracta no competeixen, sinó que es reparteixen la feina: la primera és el tipus públic, la segona una ajuda opcional.
BiblioTech queda amb una jerarquia honesta: Material és abstracta i signa Prestable i Notificable; Llibre, Revista, Dvd declaren obligatòriament el seu tipus, el seu termini i la seva tarifa; i SalaReunions demostra que es pot signar el contracte sense entrar a la família.
Però fixa't en una peça que arrossegues des de 03-07 i que continua sent poc satisfactòria: les incidències d'un Prestec són un String[] de textos solts. Una incidència real té un dia, un motiu i una gravetat, i mereix ser un tipus propi. Ara bé, una classe de primer nivell, en el seu propi fitxer, per a una cosa que només té sentit dins d'un préstec? A la lliçó 04-03, Classes Internes, veuràs els quatre tipus de classe imbricada que ofereix Java, aprendràs quan cadascun és l'elecció correcta —inclosa la que causa fuites de memòria reals en producció— i convertiràs aquelles cadenes soltes en una Prestec.Incidencia amb nom, estructura i encapsulament propis.
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
