Vas tancar el mòdul 3 amb una jerarquia sòlida: Material i els seus tres suports, Empleat, Prestec i una capa de presentació separada. L'herència et va donar reutilització i polimorfisme, però també una lligadura: en Java una classe només pot estendre una classe. I BiblioTech ja comença a topar amb aquest límit. Nexus Software vol prestar també sales de reunió, que es reserven i s'alliberen igual que un llibre, però que no són materials del catàleg: no tenen ISBN, ni títol, ni multa per retard. Les fiques a la jerarquia Material sabent que la relació "és un" és falsa? Dupliques el codi de reserva? Cap de les dues.
La resposta és la interfície: un contracte de comportament pur, sense estat, que qualsevol classe pot signar —i en pot signar uns quants alhora—. Les interfícies són el mecanisme amb què està construïda la biblioteca estàndard de Java: Comparable, Runnable, List, Iterable, AutoCloseable. Aprendre-les bé és el pas que separa escriure classes de dissenyar sistemes, perquè una interfície et permet programar contra allò que una cosa fa sense saber mai allò que una cosa és.
Contingut
- Què és una interfície: un contracte sense estat
- Sintaxi i
implements - Implementació múltiple i el problema del diamant
- Una interfície és un TIPUS: polimorfisme per contracte
- Els membres d'una interfície i els seus modificadors implícits
- Mètodes
default: evolucionar una API sense trencar res - Conflicte de
defaultiInterficie.super.metode() - Mètodes
staticen interfícies - Mètodes
privateen interfícies (Java 9) - Interfícies marcadores i
@FunctionalInterface - Dissenyar amb interfícies: contracte, inversió de dependències i testabilitat
- Interfície enfront de classe abstracta: l'avanç
- BiblioTech:
Prestable,NotificableiSalaReunions - Errors Habituals i Consells
- Exercicis
- Què és una interfície: un contracte sense estat
Una interfície és una declaració de capacitats: una llista d'operacions que una classe es compromet a oferir, sense dir res sobre com les compleix ni sobre quines dades desa.
La paraula clau és contracte. Quan escrius:
public interface Prestable {
boolean prestar();
boolean retornar();
boolean estaDisponible();
int getDiesPrestec();
}no estàs escrivint codi que s'executi. Estàs escrivint una promesa: "qualsevol classe que es declari Prestable sabrà prestar-se, retornar-se, dir si està disponible i dir quants dies dura el seu préstec". Qui rebi un Prestable pot cridar aquests quatre mètodes amb tota seguretat, sense conèixer la classe concreta que hi ha al darrere.
Tres notes fonamentals, que convé fixar des del principi:
- Una interfície no té estat d'instància. No pot declarar camps que variïn per objecte. Pot declarar constants, i prou (apartat 5).
- Una interfície no es pot instanciar.
new Prestable()no compila: no hi ha res a construir. El que s'instancia és una classe que la implementa. - La relació que expressa és "pot fer", no "és un".
Llibreés unMaterial(herència).Llibrees pot prestar (interfície). Aquesta distinció resol el 80 % dels dubtes de disseny.
| Classe | Interfície | |
|---|---|---|
| Respon a la pregunta | Què és això? | Què sap fer això? |
| Aporta | Estat + comportament | Contracte (i comportament per defecte) |
| Relació amb la subclasse | extends, una de sola |
implements, tantes com vulguis |
| Exemple del domini | Llibre extends Material |
Llibre implements Prestable |
| Exemple del JDK | String extends Object |
String implements Comparable, CharSequence |
- Sintaxi i
implements
implementsUna interfície es declara en el seu propi fitxer .java, amb el mateix nom, exactament igual que una classe:
package com.nexussoftware.bibliotech.domini;
/**
* Contracte de tot allo que Nexus Software pot prestar a un empleat:
* materials del cataleg, sales de reunio, equips informatics...
*/
public interface Prestable {
/** @return true si l'operacio ha tingut efecte. */
boolean prestar();
/** @return true si l'operacio ha tingut efecte. */
boolean retornar();
/** @return true si el recurs esta lliure ara mateix. */
boolean estaDisponible();
/** @return dies que dura el prestec d'aquest recurs. */
int getDiesPrestec();
}Fixa't en el que no hi apareix: no hi ha public als mètodes (és implícit), no hi ha abstract (també implícit), i els mètodes acaben en ; en lloc de { ... }, perquè no tenen cos.
Una classe signa el contracte amb implements:
public class SalaReunions implements Prestable {
private final String codi;
private final int capacitat;
private boolean lliure;
public SalaReunions(String codi, int capacitat) {
this.codi = codi;
this.capacitat = capacitat;
this.lliure = true;
}
@Override
public boolean prestar() {
if (!lliure) { return false; }
lliure = false;
return true;
}
@Override
public boolean retornar() {
if (lliure) { return false; }
lliure = true;
return true;
}
@Override
public boolean estaDisponible() { return lliure; }
@Override
public int getDiesPrestec() { return 1; } // una sala es reserva per dia
public String getCodi() { return codi; }
public int getCapacitat() { return capacitat; }
}Dues regles de compilació que has de conèixer:
- Cal implementar TOTS els mètodes abstractes de la interfície. Si te'n descuides un, el compilador diu
SalaReunions is not abstract and does not override abstract method getDiesPrestec() in Prestable. L'alternativa és declarar la classeabstract, i llavors l'obligació passa a les seves subclasses (lliçó 04-02). - Els mètodes implementats han de ser
public. Com que el mètode de la interfície és implícitamentpublic, reduir-ne la visibilitat en implementar-lo és un error: attempting to assign weaker access privileges.
L'@Override no és obligatori, però fes-lo servir sempre: és la mateixa xarxa de seguretat que vas aprendre a 03-05, i aquí detecta que has escrit estaDisponible() amb una n de més.
Una classe pot, a més, estendre una classe i implementar interfícies alhora, i l'ordre a la declaració és fix: primer extends, després implements.
- Implementació múltiple i el problema del diamant
Aquesta és la raó de ser de les interfícies. Una classe pot implementar tantes interfícies com vulgui, separades per comes:
Però només pot estendre una classe. Per què aquesta asimetria? La resposta s'anomena problema del diamant.
Imagina't que Java permetés herència múltiple de classes i que existissin aquestes dues:
class RecursFisic {
protected int codi = 100;
public String localitzar() { return "Prestatgeria " + codi; }
}
class RecursDigital {
protected int codi = 200;
public String localitzar() { return "Servidor " + codi; }
}
// AIXO NO EXISTEIX EN JAVA:
class LlibreHibrid extends RecursFisic, RecursDigital { }classDiagram
class Object
class RecursFisic {
+codi int
+localitzar() String
}
class RecursDigital {
+codi int
+localitzar() String
}
class LlibreHibrid
Object <|-- RecursFisic
Object <|-- RecursDigital
RecursFisic <|-- LlibreHibrid
RecursDigital <|-- LlibreHibrid
El dibuix té forma de rombe —d'aquí el nom— i planteja preguntes sense resposta única:
hibrid.localitzar()executa la versió física o la digital?hibrid.codival 100 o 200? O l'objecte té dos campscodi?- Si
RecursFisiciRecursDigitalhereten tots dos d'una mateixa classe base amb estat, aquest estat es desa una vegada o dues?
Els llenguatges que permeten herència múltiple (C++, per exemple) resolen això amb regles complexes: herència virtual, qualificació explícita de l'àmbit, ordre de linealització. Java va prendre la decisió oposada el 1995: una sola superclasse, i punt. La complexitat s'elimina d'arrel.
Les interfícies se n'escapen, del problema, perquè, en la seva forma original, no aporten estat ni implementació. Si deu interfícies declaren int getDiesPrestec();, la classe implementadora escriu un sol cos que satisfà les deu alhora. No hi ha ambigüitat possible: no hi ha dues versions entre les quals triar, n'hi ha zero.
| Conflicte | Herència múltiple de classes | Implementació múltiple d'interfícies |
|---|---|---|
| Estat duplicat | Sí: dos camps codi |
Impossible: no hi ha estat d'instància |
| Dos cossos per al mateix mètode | Sí: ambigüitat | No: la classe escriu l'únic cos |
| Constructors en cadena | Quin s'executa primer? | Les interfícies no tenen constructor |
Això va ser exactament cert fins a Java 8, quan van arribar els mètodes default i amb ells un petit diamant residual. El veuràs resolt a l'apartat 7.
- Una interfície és un TIPUS: polimorfisme per contracte
Aquest és l'apartat que dona valor real a tot l'anterior. Una interfície, tot i que no es pugui instanciar, és un tipus vàlid de Java: hi pots declarar variables, paràmetres, valors de retorn i arrays.
Prestable recurs = new SalaReunions("SALA-A", 12); // upcasting a la interficie
recurs.prestar();
System.out.println(recurs.estaDisponible()); // falseLa variable recurs no sap que hi ha una sala al darrere. Només coneix els quatre mètodes del contracte. És exactament el polimorfisme de 03-06 (el tipus declarat decideix què pots cridar, el tipus real decideix què s'executa), però ara el tipus declarat no és una superclasse, sinó un contracte que classes sense cap parentiu poden signar.
I aquí hi ha el benefici, en un mètode que serveix per a tot l'inventari de Nexus Software:
/** Informe de disponibilitat valid per a llibres, revistes, DVD i sales. */
public static void informarDisponibilitat(Prestable[] recursos) {
int lliures = 0;
for (Prestable p : recursos) {
System.out.printf(" %-12s termini %2d dies %s%n",
p.getClass().getSimpleName(),
p.getDiesPrestec(),
p.estaDisponible() ? "LLIURE" : "OCUPAT");
if (p.estaDisponible()) { lliures++; }
}
System.out.printf("Disponibles: %d de %d%n", lliures, recursos.length);
}Prestable[] inventari = {
new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
new Dvd("Refactoritzacio en directe", "DVD-0007", 95),
new SalaReunions("SALA-A", 12)
};
informarDisponibilitat(inventari);Llibre termini 15 dies LLIURE Dvd termini 3 dies LLIURE SalaReunions termini 1 dies LLIURE Disponibles: 3 de 3
Llibre i SalaReunions no comparteixen superclasse (més enllà d'Object), no comparteixen camps, no comparteixen res. I tot i així viatgen juntes en el mateix array i responen a la mateixa crida. Això és el que l'herència no et podia donar.
Se segueix fent servir un array perquè les col·leccions (
ArrayListi companyia) arriben al mòdul 5. És provisional, com ho ve sent des del mòdul 3.
- Els membres d'una interfície i els seus modificadors implícits
Les interfícies apliquen modificadors per defecte que no s'escriuen. Conèixer-los evita sorpreses.
| Membre | Modificadors implícits | Es poden ometre? | Des de |
|---|---|---|---|
| Mètode sense cos | public abstract |
Sí, i has d'ometre'ls | Java 1.0 |
| Camp | public static final |
Sí | Java 1.0 |
Mètode default |
public |
Sí (default és obligatori) |
Java 8 |
Mètode static |
public |
Sí (static és obligatori) |
Java 8 |
Mètode private |
cap (private explícit) |
No | Java 9 |
| Tipus imbricat (classe, interfície, enum) | public static |
Sí | Java 1.1 |
Dues conseqüències que sorprenen tothom:
Tot camp d'una interfície és una constant. Això:
no declara un camp d'instància, sinó una constant compartida a la qual s'accedeix com a Prestable.TERMINI_MAXIM_DIES. No li pots assignar cap altre valor, ni al constructor ni enlloc. Si proves TERMINI_MAXIM_DIES = 40; obtens cannot assign a value to final variable.
No existeixen els membres d'instància. Una interfície no desa mai dades per objecte. Si el teu disseny necessita que el contracte porti estat associat, el contracte no és el que busques: necessites una classe abstracta (04-02).
Antipatró: la "interfície de constants". Abans dels
enumera habitual crear una interfície només per agrupar constants i implementar-la per "heretar-les" sense qualificar. És una mala pràctica reconeguda: contamina l'API pública de la classe amb detalls d'implementació. Per a constants fes servir una classe final amb membresstatic final, o millor unenum(04-07).
- Mètodes
default: evolucionar una API sense trencar res
default: evolucionar una API sense trencar resFins a Java 7, afegir un mètode a una interfície publicada era una catàstrofe: cadascuna de les classes que la implementaven arreu del món deixava de compilar de cop. Pensa en la magnitud del problema: quan l'equip de Java va voler afegir forEach i stream a java.util.Collection el 2014, hi havia milions de classes implementant List fora del seu control.
La solució van ser els mètodes default: mètodes d'interfície amb cos, que les classes implementadores hereten de franc i poden sobreescriure si volen.
public interface Prestable {
boolean prestar();
boolean retornar();
boolean estaDisponible();
int getDiesPrestec();
/**
* Dies que queden de termini. Implementacio per defecte suficient
* per a la majoria de recursos; els que tinguin regles propies la sobreescriuen.
*/
default int diesRestants(int diesTranscorreguts) {
return Math.max(0, getDiesPrestec() - diesTranscorreguts);
}
/** @return true si el termini s'ha superat. */
default boolean estaVencut(int diesTranscorreguts) {
return diesRestants(diesTranscorreguts) == 0 && diesTranscorreguts > 0;
}
}Afegir aquests dos mètodes no trenca ni SalaReunions ni Material: totes dues continuen compilant sense tocar-ne ni una línia i guanyen els dos mètodes nous.
Quatre claus sobre els default:
- Un
defaultnomés pot fer servir el mateix contracte. Fixa't quediesRestantscridagetDiesPrestec(), un mètode abstracte de la interfície. No pot accedir a camps, perquè no hi ha camps. - Es poden sobreescriure. Una classe que necessiti una altra fórmula escriu el seu propi
diesRestantsamb@Override, i la seva versió guanya per despatx dinàmic. - No són un substitut de la classe abstracta. Són un mecanisme d'evolució d'APIs, no una via per ficar lògica de negoci a les interfícies. Si et trobes escrivint
defaultllargs i amb molta lògica, revisa el disseny. Objectguanya sempre. No pots declarar undefaultper atoString,equalsohashCode: el compilador el rebutja. Les implementacions d'Objecttenen prioritat estructural sobre qualsevoldefault.
- Conflicte de
default i Interficie.super.metode()
default i Interficie.super.metode()Amb els default, el diamant torna en versió reduïda. Si una classe implementa dues interfícies que aporten el mateix default, hi ha ambigüitat real:
public interface Prestable {
default String descriureTermini() { return "Termini estandard de prestec"; }
}
public interface Reservable {
default String descriureTermini() { return "Termini de reserva per franges"; }
}
public class SalaReunions implements Prestable, Reservable { } // NO COMPILAerror: class SalaReunions inherits unrelated defaults for descriureTermini()
from types Prestable and ReservableJava no endevina: t'obliga a decidir. La classe ha de sobreescriure el mètode, i a dins pot invocar explícitament la versió d'una interfície concreta amb la sintaxi NomInterficie.super.metode():
public class SalaReunions implements Prestable, Reservable {
@Override
public String descriureTermini() {
// Opcio A: quedar-se amb una
return Reservable.super.descriureTermini();
// Opcio B: combinar-les
// return Prestable.super.descriureTermini() + " / " + Reservable.super.descriureTermini();
// Opcio C: escriure alguna cosa completament propia
// return "Sala reservable per mitges jornades";
}
}Les regles de resolució que aplica el compilador, en ordre:
- La classe guanya a la interfície. Un mètode heretat d'una superclasse té prioritat sobre qualsevol
default(class wins). - La interfície més específica guanya. Si
Reservable extends Prestablei totes dues defineixen eldefault, guanyaReservable. - Si no hi ha guanyador, error de compilació, i la classe ho ha de resoldre amb
Interficie.super.metode().
La diferència amb el diamant clàssic és decisiva: el conflicte es detecta en compilació i només afecta el comportament, mai l'estat. Mai no hi ha dues còpies d'un camp.
- Mètodes
static en interfícies
static en interfíciesJava 8 també va permetre mètodes static amb cos dins d'una interfície. Són utilitats lligades al contracte, que s'invoquen pel nom de la interfície i no s'hereten per les classes implementadores.
public interface Prestable {
int TERMINI_MAXIM_DIES = 30;
boolean prestar();
boolean retornar();
boolean estaDisponible();
int getDiesPrestec();
/** Comprova que un termini proposat respecta la politica de l'empresa. */
static boolean terminiValid(int dies) {
return dies > 0 && dies <= TERMINI_MAXIM_DIES;
}
/** Compta quants recursos de l'array estan lliures. */
static int comptarDisponibles(Prestable[] recursos) {
int total = 0;
for (Prestable p : recursos) {
if (p.estaDisponible()) { total++; }
}
return total;
}
}System.out.println(Prestable.terminiValid(15)); // true
System.out.println(Prestable.terminiValid(45)); // false
System.out.println(Prestable.comptarDisponibles(inventari)); // 3
// SalaReunions.terminiValid(15); // NO COMPILA: els static d'interficie no s'heretenLa seva utilitat: mantenir juntes la interfície i les seves utilitats, en lloc de crear una classe auxiliar a part. Abans de Java 8 això no era possible, i per això el JDK és ple de parelles com Collection/Collections o Path/Paths. Amb mètodes estàtics en interfícies, aquest desdoblament ja no cal: per això les APIs modernes ofereixen directament List.of(...), Comparator.comparing(...) o Path.of(...).
- Mètodes
private en interfícies (Java 9)
private en interfícies (Java 9)Amb uns quants default en una interfície apareix aviat codi duplicat entre ells. Treure'l a un mètode public el convertiria en part del contracte, que no és el que vols. Java 9 va permetre mètodes private en interfícies exactament per a això:
public interface Notificable {
String getCanalAvis();
String generarAvis(int diesTranscorreguts);
default String avisUrgent(int diesTranscorreguts) {
return capcalera("URGENT") + generarAvis(diesTranscorreguts);
}
default String avisRutinari(int diesTranscorreguts) {
return capcalera("INFO") + generarAvis(diesTranscorreguts);
}
/** Detall d'implementacio compartit: NO forma part del contracte. */
private String capcalera(String nivell) {
return "[" + nivell + " / " + getCanalAvis() + "] ";
}
}Hi ha dues variants:
private: es pot cridar des dels mètodesdefault(té accés athis).private static: es pot cridar des delsdefaulti des delsstatic, però no accedeix athis.
Cap de les dues és visible des de fora ni des de les classes implementadores. Són pur detall intern.
- Interfícies marcadores i
@FunctionalInterface
@FunctionalInterfaceUna interfície marcadora (marker interface) és una interfície sense cap mètode. No promet comportament: promet una propietat, que el compilador o la biblioteca comproven en temps d'execució.
Les tres del JDK que veuràs:
| Interfície marcadora | Què marca | On s'estudia |
|---|---|---|
java.io.Serializable |
L'objecte es pot convertir en bytes i desar-se | Mòdul 7 |
java.lang.Cloneable |
Object.clone() el pot copiar (desaconsellat, 03-09) |
— |
java.util.RandomAccess |
La llista permet accés indexat eficient | Mòdul 5 |
Avui les anotacions (@Deprecated, @Override, i les teves pròpies a 10-02) cobreixen bona part d'aquests casos amb més flexibilitat, però les marcadores hi continuen sent perquè tenen un avantatge que les anotacions no tenen: creen un tipus, i per tant el compilador les pot exigir en una signatura (void desar(Serializable objecte)).
Cas a part és @FunctionalInterface: no és una interfície marcadora, sinó una anotació que s'aplica a interfícies amb exactament un mètode abstracte i que habilita l'ús de lambdes. Es menciona aquí perquè reconeguis la paraula; el seu significat complet, el catàleg de java.util.function i les referències a mètodes són el contingut de la lliçó 04-06.
- Dissenyar amb interfícies: contracte, inversió de dependències i testabilitat
Les interfícies no són només un truc per esquivar l'herència múltiple. Són l'eina central de disseny de Java, i aquestes tres idees expliquen per què.
Programar contra el contracte, no contra la implementació. Declara sempre amb el tipus més general que et serveixi:
Prestable recurs = new SalaReunions("SALA-A", 12); // be: depens del contracte
SalaReunions sala = new SalaReunions("SALA-A", 12); // nomes si necessites getCapacitat()Amb la primera forma, canviar demà a una altra implementació costa una línia. Amb la segona, costa revisar tot el que fa servir la variable.
Inversió de dependències, en una frase. Els mòduls importants (les regles de negoci) no han de dependre dels mòduls de detall (la base de dades, la consola, la xarxa): tots dos han de dependre d'una interfície definida pel mòdul important. A BiblioTech, GestorPrestecs no hauria de dependre de RebutConsola, sinó d'una interfície Rebut que RebutConsola implementa; així, canviar a RebutPdf no toca el gestor. Aquest és el mecanisme sobre el qual s'apuntalen Spring i la injecció de dependències del mòdul 11.
Testabilitat. Si GestorPrestecs depèn de la interfície Rebut, en una prova li pots passar una implementació falsa que només compti quantes vegades l'han cridada, sense imprimir res. Sense interfície, provar el gestor obliga a capturar System.out. Aquesta tècnica —els mocks— és JUnit i Mockito al mòdul 11, i la seva viabilitat es decideix aquí, en el moment en què tries dependre d'un contracte o d'una classe concreta.
- Interfície enfront de classe abstracta: l'avanç
La pregunta arriba inevitablement: si un default pot portar cos, en què es diferencia una interfície d'una classe abstracta? En allò essencial:
- Una interfície no té estat d'instància ni constructor, i se'n poden implementar unes quantes.
- Una classe abstracta sí que té camps, constructor i membres
protected, però només en pots estendre una.
Regla de partida: la interfície defineix què es pot fer; la classe abstracta comparteix com es fa i quines dades calen. La taula comparativa completa, amb les sis dimensions que importen i una regla pràctica de decisió, és el contingut de la lliçó 04-02, on a més veuràs que l'habitual no és triar, sinó combinar-les.
- BiblioTech:
Prestable, Notificable i SalaReunions
Prestable, Notificable i SalaReunionsApliquem-ho tot al projecte. Material té avui dos grups de responsabilitats barrejats: prestar-se i avisar. Els extraurem com a dos contractes independents.
Prestable
package com.nexussoftware.bibliotech.domini;
/** Contracte de tot recurs que Nexus Software presta als seus empleats. */
public interface Prestable {
/** Termini maxim que admet la politica de l'empresa. */
int TERMINI_MAXIM_DIES = 30;
boolean prestar();
boolean retornar();
boolean estaDisponible();
int getDiesPrestec();
/** Dies de termini que queden; 0 si ja ha vencut. */
default int diesRestants(int diesTranscorreguts) {
return Math.max(0, getDiesPrestec() - diesTranscorreguts);
}
/** @return true si el termini s'ha superat. */
default boolean estaVencut(int diesTranscorreguts) {
return diesTranscorreguts > getDiesPrestec();
}
static boolean terminiValid(int dies) {
return dies > 0 && dies <= TERMINI_MAXIM_DIES;
}
static int comptarDisponibles(Prestable[] recursos) {
int total = 0;
for (Prestable p : recursos) {
if (p.estaDisponible()) { total++; }
}
return total;
}
}Notificable
package com.nexussoftware.bibliotech.domini;
/** Contracte de tot allo sobre el qual BiblioTech pot emetre avisos. */
public interface Notificable {
/** Canal pel qual s'avisa: "correu", "xat", "telefon"... */
String getCanalAvis();
/** Text de l'avis per als dies transcorreguts indicats. */
String generarAvis(int diesTranscorreguts);
/** Avis amb prefix de nivell, construit sobre el contracte. */
default String avisUrgent(int diesTranscorreguts) {
return capcalera("URGENT") + generarAvis(diesTranscorreguts);
}
default String avisRutinari(int diesTranscorreguts) {
return capcalera("INFO") + generarAvis(diesTranscorreguts);
}
private String capcalera(String nivell) {
return "[" + nivell + " / " + getCanalAvis() + "] ";
}
}Material signa els dos contractes
El canvi a Material és d'una sola línia, perquè els mètodes ja existeixen des del mòdul 3:
public class Material implements Prestable, Notificable {
// ... constants, camps i constructor sense canvis ...
@Override public boolean prestar() { /* sense canvis */ }
@Override public boolean retornar() { /* sense canvis */ }
@Override public boolean estaDisponible() { return disponible; }
@Override public int getDiesPrestec() { return DIES_PRESTEC; }
@Override public String getCanalAvis() { return "correu"; }
@Override public String generarAvis(int diesTranscorreguts) { /* sense canvis */ }
// ... la resta igual ...
}Que la refactorització costi una línia és el millor senyal possible: significa que al mòdul 3 vas identificar bé les responsabilitats. L'única cosa nova és que ara aquestes responsabilitats tenen nom i són un tipus.
SalaReunions: l'avantatge enfront de l'herència
package com.nexussoftware.bibliotech.domini;
/** Sala de reunio de Nexus Software. Es reserva, pero NO es un material. */
public class SalaReunions implements Prestable {
private final String codi;
private final int capacitat;
private boolean lliure;
private String ocupadaPer;
public SalaReunions(String codi, int capacitat) {
this.codi = (codi == null || codi.isBlank()) ? "SALA-000" : codi.trim();
this.capacitat = Math.max(1, capacitat);
this.lliure = true;
this.ocupadaPer = null;
}
@Override
public boolean prestar() {
if (!lliure) {
System.out.println("AVIS: la sala " + codi + " ja esta reservada.");
return false;
}
lliure = false;
return true;
}
@Override
public boolean retornar() {
if (lliure) { return false; }
lliure = true;
ocupadaPer = null;
return true;
}
@Override public boolean estaDisponible() { return lliure; }
@Override public int getDiesPrestec() { return 1; }
/** Una sala no te multa: sobreescriu el default perque la seva regla es una altra. */
@Override
public boolean estaVencut(int diesTranscorreguts) {
return false;
}
public String getCodi() { return codi; }
public int getCapacitat() { return capacitat; }
}Fixa't en el que s'ha aconseguit:
SalaReunionsno hereta de res. No té títol, ni referència, ni multa, ni tarifa. El seu únic deute és el contractePrestable.- No implementa
Notificable, perquè no s'avisa de sales. Els contractes són independents: se signen per separat. - Sobreescriu un
default(estaVencut) perquè la seva regla de negoci difereix. - I tot i així, viatja en el mateix array que un
Llibrei respon ainformarDisponibilitat.
Amb herència això era impossible sense mentir: o bé SalaReunions extends Material (fals: no és un material i arrossegaria ISBN i multes), o bé duplicaves el codi de reserva.
classDiagram
class Prestable {
<<interface>>
+TERMINI_MAXIM_DIES int
+prestar() boolean
+retornar() boolean
+estaDisponible() boolean
+getDiesPrestec() int
+diesRestants(int) int
+estaVencut(int) boolean
}
class Notificable {
<<interface>>
+getCanalAvis() String
+generarAvis(int) String
+avisUrgent(int) String
+avisRutinari(int) String
}
class Material {
-titol String
-referencia String
-disponible boolean
+calcularMulta(int) double
+descriure() String
}
class Llibre
class Revista
class Dvd
class SalaReunions {
-codi String
-capacitat int
+getCapacitat() int
}
Prestable <|.. Material
Notificable <|.. Material
Prestable <|.. SalaReunions
Material <|-- Llibre
Material <|-- Revista
Material <|-- Dvd
Al diagrama, la línia discontínua és implements i la contínua és extends. Es llegeix d'un cop d'ull: dos contractes, dues famílies que els signen en distinta mesura, i cap parentiu forçat.
Errors Habituals i Consells
Creure que una interfície pot tenir camps d'instància. int comptador; dins d'una interfície no és un camp mutable: és public static final int comptador, i sense inicialitzador ni tan sols compila. Si necessites estat compartit entre implementacions, necessites una classe abstracta (04-02).
Reduir la visibilitat en implementar. Si escrius boolean prestar() sense public a la classe, el compilador falla: els mètodes d'interfície són public i no es poden restringir. És l'error més freqüent en la primera implementació.
Descuidar-se l'@Override. Sense l'anotació, escriure estaDisponibles() no dona error immediat: crea un mètode nou i el compilador es queixa molt més tard, amb un missatge sobre mètodes abstractes sense implementar. L'@Override assenyala el punt exacte de la fallada.
Fer servir la interfície com a bossa de constants. És un antipatró conegut. Per a constants agrupades fes servir un enum (04-07) o una classe final amb membres static final.
Omplir la interfície de mètodes default. Els default existeixen per evolucionar APIs, no per colar lògica de negoci al contracte. Un default de trenta línies és gairebé sempre una classe abstracta mal ubicada.
Interfícies massa grans. Si Prestable acumulés quinze mètodes, cap classe no la podria implementar sense mètodes buits. És preferible diverses interfícies petites i cohesives —Prestable, Notificable, Reservable— que no pas una de gegant. El principi s'anomena segregació d'interfícies i el formalitzaràs a 12-02.
Consell: anomena les interfícies per la capacitat. El conveni de Java afavoreix adjectius o participis (Prestable, Notificable, Comparable, Runnable, Serializable) enfront dels substantius, precisament perquè descriuen el que una cosa sap fer. I evita els prefixos tipus IPrestable: no són convenció en Java.
Consell: declara amb el tipus del contracte. Prestable recurs = ... en lloc de SalaReunions sala = ... sempre que no necessitis els mètodes propis de la classe. És la pràctica que fa possible canviar la implementació demà.
Exercicis
Exercici 1: la interfície Catalogable
Crea una interfície Catalogable a com.nexussoftware.bibliotech.domini amb:
- Una constant
String PREFIX_FITXA = "FITXA-". - Dos mètodes abstractes:
String getReferencia()iString getTitol(). - Un mètode
default String generarFitxa()que retorniFITXA-<referencia>: <titol>. - Un mètode
static boolean referenciaValida(String ref)que retornitruesi la referència no és nul·la i té almenys 5 caràcters.
Després fes que Material la implementi (comprova que no cal escriure ni un mètode nou) i prova generarFitxa() amb "Java Eficaç".
Exercici 2: resoldre un conflicte de default
Crea dues interfícies, Prestable i Llogable, totes dues amb un default String politica() que retorni textos diferents ("Prestec gratuit per a empleats" i "Lloguer amb tarifa diaria"). Crea una classe ProjectorPortatil que implementi les dues. Comprova que no compila, i resol-ho amb Interficie.super.metode() retornant la concatenació de totes dues polítiques.
Exercici 3: un inventari heterogeni
Escriu un mètode static void resumInventari(Prestable[] recursos) que recorri l'array i mostri, per a cada recurs: el seu nom de classe, si està disponible, i el seu termini. Al final ha d'imprimir quants són Notificable (fent servir instanceof) i el total de disponibles amb Prestable.comptarDisponibles. Prova'l amb dos llibres, un DVD i dues sales.
Solucions
Solució 1
package com.nexussoftware.bibliotech.domini;
/** Contracte de tot element que apareix al cataleg de BiblioTech. */
public interface Catalogable {
// public static final implicit
String PREFIX_FITXA = "FITXA-";
// public abstract implicits
String getReferencia();
String getTitol();
/**
* Fitxa de cataleg. Nomes fa servir el mateix contracte: per aixo pot
* tenir cos sense accedir a cap camp.
*/
default String generarFitxa() {
return PREFIX_FITXA + getReferencia() + ": " + getTitol();
}
/** Utilitat associada al contracte; NO s'hereta per les classes. */
static boolean referenciaValida(String ref) {
return ref != null && ref.trim().length() >= 5;
}
}Material només canvia a la declaració:
public class Material implements Prestable, Notificable, Catalogable {
// getReferencia() i getTitol() JA EXISTEIXEN des de 03-07: res a afegir
}Catalogable c = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
System.out.println(c.generarFitxa());
System.out.println(Catalogable.referenciaValida("978-0000000001"));
System.out.println(Catalogable.referenciaValida("123"));L'important de l'exercici: implementar la interfície no ha costat ni una línia de cos. Quan els mètodes ja existeixen amb la signatura correcta, signar el contracte és declaratiu. I generarFitxa() arriba de franc a Llibre, Revista i Dvd alhora.
Solució 2
public interface Prestable2 {
default String politica() { return "Prestec gratuit per a empleats"; }
}
public interface Llogable {
default String politica() { return "Lloguer amb tarifa diaria"; }
}Sense sobreescriure, l'error és:
error: class ProjectorPortatil inherits unrelated defaults for politica()
from types Prestable2 and LlogableLa resolució:
public class ProjectorPortatil implements Prestable2, Llogable {
private final String codi;
public ProjectorPortatil(String codi) { this.codi = codi; }
/**
* El compilador obliga a decidir. Aqui es combinen totes dues politiques:
* el projector es gratuit per a empleats, pero es lloga a externs.
*/
@Override
public String politica() {
return Prestable2.super.politica() + "; " + Llogable.super.politica();
}
public String getCodi() { return codi; }
}Fixa't en la sintaxi Prestable2.super.politica(): és l'única manera d'invocar explícitament el default d'una interfície concreta, i només és vàlida si la classe implementa directament aquella interfície. És el mecanisme amb què Java conserva la implementació múltiple sense el problema del diamant: el conflicte no es resol per regles ocultes, el resol el programador, i només afecta el comportament, mai l'estat.
Solució 3
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.domini.*;
public class InventariApp {
/** Resum valid per a qualsevol recurs prestable, sigui del tipus que sigui. */
public static void resumInventari(Prestable[] recursos) {
System.out.println("=== INVENTARI DE RECURSOS PRESTABLES ===");
int notificables = 0;
for (Prestable p : recursos) {
System.out.printf(" %-15s %-10s termini %2d dies%n",
p.getClass().getSimpleName(),
p.estaDisponible() ? "LLIURE" : "OCUPAT",
p.getDiesPrestec());
// instanceof amb patro de tipus (03-06): comprova i converteix
if (p instanceof Notificable n) {
notificables++;
System.out.printf(" canal d'avis: %s%n", n.getCanalAvis());
}
}
System.out.printf("Recursos notificables: %d de %d%n", notificables, recursos.length);
System.out.printf("Disponibles ara: %d de %d%n",
Prestable.comptarDisponibles(recursos), recursos.length);
}
public static void main(String[] args) {
Prestable[] inventari = {
new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994),
new Dvd("Refactoritzacio en directe", "DVD-0007", 95),
new SalaReunions("SALA-A", 12),
new SalaReunions("SALA-B", 4)
};
inventari[0].prestar(); // Marta Ruiz s'emporta Java Eficac
inventari[3].prestar(); // i reserva la SALA-A
resumInventari(inventari);
}
}=== INVENTARI DE RECURSOS PRESTABLES ===
Llibre OCUPAT termini 15 dies
canal d'avis: correu
Llibre LLIURE termini 15 dies
canal d'avis: correu
Dvd LLIURE termini 3 dies
canal d'avis: telefon
SalaReunions OCUPAT termini 1 dies
SalaReunions LLIURE termini 1 dies
Recursos notificables: 3 de 5
Disponibles ara: 3 de 5Tres observacions sobre la solució:
- El mètode no menciona ni una sola classe concreta. Només coneix
PrestableiNotificable. Afegir demàEquipInformatic implements Prestableno obliga a tocar ni una línia deresumInventari. instanceofamb patró distingeix els que a més signenNotificable, i a la mateixa línia declara la variablenja convertida. No és una olor de disseny com elsswitchsobre el tipus de 03-06: aquí no es tria comportament per tipus, es comprova una capacitat opcional.Prestable.comptarDisponibless'invoca sobre la interfície, no sobre un objecte. És un mètodestaticd'interfície: utilitat i contracte viuen al mateix fitxer.
Conclusió
Ja tens a la mà l'eina central del disseny en Java. Saps que una interfície és un contracte de comportament sense estat, que es declara amb interface i se signa amb implements, i que expressa una relació de "pot fer" enfront de l'"és un" de l'herència. Entens per què Java permet implementar tantes interfícies com vulguis però només estendre una classe: el problema del diamant, amb la seva ambigüitat d'estat i d'implementació, desapareix quan el contracte no aporta ni camps ni cossos. Saps que una interfície és un tipus, i n'has vist la conseqüència pràctica en un array on un Llibre i una SalaReunions —sense cap parentiu— responen a la mateixa crida.
Domines els membres d'una interfície i els seus modificadors implícits: public abstract als mètodes, public static final als camps, sense excepció. Coneixes els mètodes default i, sobretot, per a què es van afegir: permetre que el JDK evolucionés les seves interfícies sense trencar milions de classes; i saps resoldre el seu únic conflicte possible amb Interficie.super.metode(). Saps que els mètodes static mantenen les utilitats al costat del contracte —per això avui existeix List.of(...) on abans calia la classe Collections— i que els private de Java 9 permeten compartir codi entre default sense ampliar el contracte. Reconeixes les interfícies marcadores com Serializable (mòdul 7) i saps que @FunctionalInterface és la porta a les lambdes, que obriràs a 04-06.
I per damunt de la sintaxi, te'n portes tres criteris de disseny: programa contra el contracte, inverteix les dependències perquè les regles de negoci no depenguin dels detalls, i recorda que la testabilitat del teu codi al mòdul 11 es decideix avui, cada vegada que tries dependre d'una interfície o d'una classe concreta. BiblioTech ja té els seus contractes, Prestable i Notificable, i una SalaReunions que demostra que es pot prestar sense ser un material.
Queda una escletxa evident. Material continua sent una classe instanciable: res no impedeix escriure new Material("Alguna cosa", "REF-1", true) i obtenir un objecte sense tipus, sense autor, sense número i sense durada, que retorna "Material" a getTipus() i aplica terminis genèrics. És un objecte que no hauria d'existir, i el seu getTipus() i el seu getDiesPrestec() no són res més que valors de farciment que les subclasses sobreescriuen sempre. A la lliçó 04-02, Classes Abstractes, tancaràs aquella porta: Material passarà a ser abstract, els seus mètodes de farciment es convertiran en mètodes abstractes que obliguen cada suport a declarar les seves regles, i descobriràs que una classe abstracta ofereix el que una interfície no pot —estat, constructor i codi compartit— i per què el disseny professional gairebé mai no tria entre totes dues, sinó que les combina.
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
