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

  1. Què és una interfície: un contracte sense estat
  2. Sintaxi i implements
  3. Implementació múltiple i el problema del diamant
  4. Una interfície és un TIPUS: polimorfisme per contracte
  5. Els membres d'una interfície i els seus modificadors implícits
  6. Mètodes default: evolucionar una API sense trencar res
  7. Conflicte de default i Interficie.super.metode()
  8. Mètodes static en interfícies
  9. Mètodes private en interfícies (Java 9)
  10. Interfícies marcadores i @FunctionalInterface
  11. Dissenyar amb interfícies: contracte, inversió de dependències i testabilitat
  12. Interfície enfront de classe abstracta: l'avanç
  13. BiblioTech: Prestable, Notificable i SalaReunions
  14. Errors Habituals i Consells
  15. Exercicis

  1. 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 un Material (herència). Llibre es 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

  1. Sintaxi i implements

Una 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:

  1. 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 classe abstract, i llavors l'obligació passa a les seves subclasses (lliçó 04-02).
  2. Els mètodes implementats han de ser public. Com que el mètode de la interfície és implícitament public, 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.

public class Llibre extends Material implements Comparable<Llibre> { ... }

  1. 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:

public class Material implements Prestable, Notificable {
    // ha de complir els dos contractes
}

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.codi val 100 o 200? O l'objecte té dos camps codi?
  • Si RecursFisic i RecursDigital hereten 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.

  1. 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());         // false

La 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 (ArrayList i companyia) arriben al mòdul 5. És provisional, com ho ve sent des del mòdul 3.

  1. 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 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 Java 1.1

Dues conseqüències que sorprenen tothom:

Tot camp d'una interfície és una constant. Això:

public interface Prestable {
    int TERMINI_MAXIM_DIES = 30;     // realment: public static final int
}

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 enum era 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 membres static final, o millor un enum (04-07).

  1. Mètodes default: evolucionar una API sense trencar res

Fins 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 default només pot fer servir el mateix contracte. Fixa't que diesRestants crida getDiesPrestec(), 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 diesRestants amb @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 default llargs i amb molta lògica, revisa el disseny.
  • Object guanya sempre. No pots declarar un default per a toString, equals o hashCode: el compilador el rebutja. Les implementacions d'Object tenen prioritat estructural sobre qualsevol default.

  1. Conflicte de 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 COMPILA
error: class SalaReunions inherits unrelated defaults for descriureTermini()
       from types Prestable and Reservable

Java 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:

  1. La classe guanya a la interfície. Un mètode heretat d'una superclasse té prioritat sobre qualsevol default (class wins).
  2. La interfície més específica guanya. Si Reservable extends Prestable i totes dues defineixen el default, guanya Reservable.
  3. 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.

  1. Mètodes static en interfícies

Java 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'hereten

La 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(...).

  1. Mètodes 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ètodes default (té accés a this).
  • private static: es pot cridar des dels default i des dels static, però no accedeix a this.

Cap de les dues és visible des de fora ni des de les classes implementadores. Són pur detall intern.

  1. Interfícies marcadores i @FunctionalInterface

Una 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ó.

public interface Auditable { }   // sense metodes: nomes marca

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.

  1. 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.

  1. 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.

  1. BiblioTech: Prestable, Notificable i SalaReunions

Apliquem-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:

  • SalaReunions no hereta de res. No té títol, ni referència, ni multa, ni tarifa. El seu únic deute és el contracte Prestable.
  • 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 Llibre i respon a informarDisponibilitat.

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 cohesivesPrestable, 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() i String getTitol().
  • Un mètode default String generarFitxa() que retorni FITXA-<referencia>: <titol>.
  • Un mètode static boolean referenciaValida(String ref) que retorni true si 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"));
FITXA-978-0000000001: Java Eficac
true
false

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 Llogable

La 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; }
}
System.out.println(new ProjectorPortatil("PRO-01").politica());
Prestec gratuit per a empleats; Lloguer amb tarifa diaria

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 5

Tres observacions sobre la solució:

  1. El mètode no menciona ni una sola classe concreta. Només coneix Prestable i Notificable. Afegir demà EquipInformatic implements Prestable no obliga a tocar ni una línia de resumInventari.
  2. instanceof amb patró distingeix els que a més signen Notificable, i a la mateixa línia declara la variable n ja convertida. No és una olor de disseny com els switch sobre el tipus de 03-06: aquí no es tria comportament per tipus, es comprova una capacitat opcional.
  3. Prestable.comptarDisponibles s'invoca sobre la interfície, no sobre un objecte. És un mètode static d'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

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

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

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

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

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

© Copyright 2026. Tots els drets reservats