14  Herència i polimorfisme

Objectius

  • Entendre l’herència i distingir entre superclasse i subclasse.
  • Reutilitzar codi amb extends i cridar el pare amb super.
  • Sobreescriure mètodes amb @Override i saber quan usar final.
  • Definir classes i mètodes abstract i saber per què no es poden instanciar.
  • Declarar i implementar interfícies amb implements.
  • Aplicar el polimorfisme: tractar objectes diferents amb una mateixa referència.
  • Fer upcasting i downcasting amb seguretat gràcies a instanceof.

Fins ara hem construit classes i les hem relacionat entre elles. Ara farem un pas més: fer que unes classes heretin d’altres.

L’herència i el polimorfisme són el cor de la programació orientada a objectes i el que permet escriure codi flexible i reutilitzable.

14.1 Què és l’herència?

L’herència permet crear una classe nova a partir d’una que ja existeix, aprofitant-ne els atributs i mètodes i afegint-hi (o canviant-hi) el que calgui.

La classe original és la superclasse (o classe pare); la nova és la subclasse (o classe filla).

ConsellAnalogia: la família

Heretes trets dels teus pares (cognom o característiques genètiques) però també tens característiques pròpies.

En programació és igual: una subclasse hereta tot el que té el pare i hi afegeix les seves particularitats.

Un Gos és un Animal: té tot el que té un animal (menja, dorm) i, a més, borda.

La relació clau és “és un/una”: un Gos és un Animal, un Cotxe és un Vehicle.

Si pots dir “X és un Y”, potser X ha d’heretar de Y.

classDiagram
    Animal <|-- Gos
    Animal <|-- Gat
    class Animal {
        #String nom
        +menjar() void
        +ferSo() void
    }
    class Gos {
        +ferSo() void
        +buscarPilota() void
    }
    class Gat {
        +ferSo() void
    }

La punta de fletxa buida indica herència i apunta cap al pare.

En Java, una classe hereta d’una altra amb la paraula clau extends.

public class Animal {
    protected String nom;           // Protected: visible per a les subclasses

    public Animal(String nom) {
        this.nom = nom;
    }

    public void menjar() {
        System.out.println(nom + " està menjant.");
    }

    public void ferSo() {
        System.out.println(nom + " fa un so.");
    }
}
NotaEl modificador protected

Recorda els nivells de visibilitat: private (només la pròpia classe), public (tothom).

N’hi ha un d’intermedi molt útil amb herència: protected, que fa l’atribut visible per a la mateixa classe i les seves subclasses, però no per a la resta.

Amb super(...) cridem el constructor del pare, i amb super.metode() cridem un mètode del pare:

public class Gos extends Animal {

    public Gos(String nom) {
        super(nom);             // Crida el constructor d'Animal
    }

    public void buscarPilota() {
        System.out.println(nom + " busca la pilota.");   // Utilitza 'nom' heretat
    }
}

Un Gos ja pot menjar() sense que ho haguem programat: ho ha heretat.

Gos rex = new Gos("Rex");
rex.menjar();               // Heretat d'Animal → "Rex està menjant."
rex.buscarPilota();         // Propi de Gos → "Rex busca la pilota."
AlertaError típic: oblidar super(...) al constructor

Si la superclasse no té constructor buit, la subclasse ha de cridar super(...) amb els arguments adequats a la primera línia del seu constructor.

Si te’l deixes, Java intenta cridar super() (buit), no el troba i dona error de compilació.

14.2 Sobreescriure mètodes

Una subclasse pot redefinir un mètode heretat per donar-li el seu propi comportament.

En diem sobreescriptura (override). Marquem el mètode redefinit amb l’anotació @Override:

public class Gos extends Animal {

    public Gos(String nom) {
        super(nom);
    }

    @Override
    public void ferSo() {
        System.out.println(nom + " fa: Bup, bup!");     // Versió pròpia del Gos
    }
}

Ara rex.ferSo() escriu “Rex fa: Bup, bup!” en comptes del so genèric de l’Animal.

NotaPer què @Override és important

@Override no és obligatori, però és molt recomanable: li diu al compilador “vull redefinir un mètode del pare”.

Si t’equivoques en el nom o els paràmetres (per exemple, escrius ferSò() amb accent, o ferso()), el compilador et donarà error en veure que no hi ha cap mètode així al pare, en comptes de crear silenciosament un mètode nou que no s’executarà mai.

AlertaError típic: canviar la signatura sense adonar-se’n

Sobreescriure exigeix el mateix nom i els mateixos paràmetres. Si canvies els paràmetres, no sobreescrius: sobrecarregues (crees una versió nova diferent).

Amb @Override, el compilador t’avisa; sense, l’error passa desapercebut.

14.3 Aturar l’herència

Ae vegades volem el contrari de l’herència: impedir-la. La paraula clau final ho permet a tres nivells:

  • final en un atribut o variable: no es pot canviar el valor (ja ho vam veure amb les constants).
  • final en un mètode: no es pot sobreescriure en cap subclasse.
  • final en una classe: no se’n pot heretar (ningú pot fer-ne extends).
public final class Constants {                  // Ningú pot heretar de Constants
    public static final double IVA = 0.21;
}

public class Compte {
    public final void numeroCompte() { }        // Les subclasses NO poden canviar-lo
}
NotaQuan usar final

Fes servir final quan un comportament no ha de canviar mai per garantir seguretat o coherència.

La classe String de Java, per exemple, és final: ningú no en pot fer una versió modificada, i això la fa fiable.

14.4 Classes i mètodes abstractes

Què passa si un Animal genèric no hauria d’existir mai per si sol? No té sentit un animal “que no és cap animal concret”. Volem obligar que sempre sigui un Gos, un Gat, etc. Per a això existeixen les classes abstractes.

Una classe abstract:

  • No es pot instanciar (no pots fer new Animal(...)).
  • Pot tenir mètodes abstractes: mètodes declarats sense cos, que les subclasses estan obligades a implementar.
public abstract class Animal {
    protected String nom;

    public Animal(String nom) {
        this.nom = nom;
    }

    public void menjar() {                          // Mètode normal (amb cos)
        System.out.println(nom + " menja.");
    }

    public abstract void ferSo();                   // Mètode abstracte: sense contingut.
}

Ara cada subclasse ha de donar la seva versió de ferSo():

public class Gat extends Animal {
    public Gat(String nom) { super(nom); }

    @Override
    public void ferSo() {
        System.out.println(nom + " fa: Meu!");
    }
}


AlertaError típic: intentar instanciar una classe abstracta

Això dona error de compilació:

Animal a = new Animal("Bèstia");    // ERROR: Animal és abstract

Una classe abstracta és una plantilla incompleta: només pots instanciar-ne les subclasses concretes. Sí que pots, però, declarar una variable de tipus Animal (ho aprofitarem tot seguit amb el polimorfisme).

14.5 Interfícies

Una interfície va un pas més enllà: és un contracte de mètodes que una classe es compromet a implementar, però sense dir com.

Defineix què s’ha de saber fer, no com es fa.

public interface Nedador {
    void nedar();               // Sense cos: qui implementi Nedador ha de saber nedar
}

Una classe implementa una interfície amb implements, i ha d’escriure tots els seus mètodes:

public class Anec extends Animal implements Nedador {
    public Anec(String nom) { super(nom); }

    @Override
    public void ferSo() { System.out.println(nom + " fa: Nyec!"); }

    @Override
    public void nedar() { System.out.println(nom + " neda a l'estany."); }
}

La gran diferència respecte a l’herència és que una classe només pot heretar d’una superclasse (extends) però pot implementar moltes interfícies (implements).

Així un Anec pot ser alhora Animal i Nedador.

classDiagram
    class Nedador {
        <<interface>>
        +nedar() void
    }
    Animal <|-- Anec
    Nedador <|.. Anec
    class Anec {
        +ferSo() void
        +nedar() void
    }

Comparativa entre classe abstracta i interfície:

Aspecte Classe abstracta Interfície
Relació que expressa És un Sap fer o pot
Nombre que se’n pot tenir Només una (extends) diverses (implements)
Pot tenir atributs d’estat Només constants (static final)
Pot tenir codi als mètodes Sí (mètodes normals) Sí, amb default (avançat)
Constructor No
Quan usar-la Jerarquia amb codi comú Capacitat compartida entre classes no relacionades
ConsellAnalogia: DNI i carnets
  • Heretar d’una superclasse és com el teu DNI: en tens un de sol i defineix qui ets.
  • Implementar interfícies és com tenir carnets (de conduir, de biblioteca, de soci del gimnàs): en pots tenir molts i cadascun certifica una capacitat concreta.

14.6 Polimorfisme

Polimorfisme vol dir “moltes formes”.

És la capacitat de fer servir una referència del tipus del pare per apuntar a objectes de qualsevol subclasse, i que Java cridi automàticament la versió correcta del mètode segons l’objecte real.

Animal[] animals = {
    new Gos("Rex"),
    new Gat("Mixeta"),
    new Anec("Donald")
};

for (Animal a : animals) {      // Tots són Animal...
    a.ferSo();                  // ... però cadascú fa el SEU so!
}

Sortida:

Rex fa: Bup, bup!
Mixeta fa: Meu!
Donald fa: Nyec!

Encara que la variable a és de tipus Animal, Java mira quin objecte real hi ha a dins i executa la versió sobreescrita corresponent.

Això s’anomena vinculació dinàmica. La gràcia: pots afegir un Cavall nou i el bucle segueix funcionant sense modificar-lo.

NotaPer què és tan potent?

El polimorfisme et permet escriure codi que treballa amb el tipus general (Animal, Nedador, Figura…) sense saber ni preocupar-se de la subclasse concreta.

Menys if, més flexibilitat, i codi obert a ampliacions futures.

14.7 Upcasting i downcasting

Quan assignem un Gos a una variable Animal, fem un upcasting (pujar cap al pare).

És automàtic i sempre segur, perquè un gos sempre és un animal:

Animal a = new Gos("Rex");          // Upcasting implícit: Gos → Animal

El camí invers, tornar de Animal a Gos, és el downcasting (baixar cap al fill).

Cal escriure’l explícitament amb un cast i no sempre és segur:

Animal a = new Gos("Rex");
Gos g = (Gos) a;                    // Downcasting explícit: torna a ser Gos
g.buscarPilota();                   // Ara sí que podem cridar mètodes propis de Gos
AlertaError típic: downcasting a un tipus equivocat

Si l’objecte real no és del tipus al qual el converteixes, el programa falla en temps d’execució amb ClassCastException:

Animal a = new Gat("Mixeta");
Gos g = (Gos) a;                    // ERROR en execució: un Gat NO és un Gos!

Per evitar-ho, comprova abans el tipus real amb instanceof.

14.8 L’operador instanceof

instanceof comprova si un objecte és d’un tipus determinat (o d’una subclasse seva).

Retorna true o false, i és el guardià que fa segur el downcasting:

for (Animal a : animals) {
    a.ferSo();
    if (a instanceof Nedador) {             // Aquest animal sap nedar?
        Nedador n = (Nedador) a;
        n.nedar();
    }
}

Des de Java 16 hi ha una forma abreujada, el pattern matching, que combina la comprovació i el cast en un sol pas:

if (a instanceof Nedador n) {       // Si és Nedador, ja crea la variable 'n'
    n.nedar();                      // No cal el cast manual!
}
ConsellMini-repte

Crea una interfície Volador amb el mètode volar(). Fes que una classe Ocell (subclasse d’Animal) la implementi.

Després, recorre un array d’Animal i, amb instanceof, fes volar només els que sàpiguen fer-ho.

14.9 Resum

  • L’herència (extends) crea una subclasse que aprofita els atributs i mètodes d’una superclasse. Expressa la relació “és un/una”.
  • super(...) crida el constructor del pare; super.metode(), un mètode del pare. protected fa un membre visible per a les subclasses.
  • Sobreescriure (@Override) redefineix un mètode heretat; l’anotació ajuda el compilador a detectar errors de signatura.
  • final impedeix canviar un valor, sobreescriure un mètode o heretar d’una classe.
  • Una classe abstract no es pot instanciar i pot tenir mètodes abstractes que les subclasses han d’implementar.
  • Una interface és un contracte de capacitats; una classe pot implementar-ne moltes, però només heretar d’una superclasse.
  • El polimorfisme permet tractar objectes de subclasses diferents amb una referència del pare i que s’executi la versió correcta de cada mètode.
  • El upcasting (fill → pare) és automàtic i segur; el downcasting (pare → fill) és explícit i cal protegir-lo amb instanceof.

14.10 Per practicar

  1. Jerarquia de figures. Crea una classe abstracta Figura amb un mètode abstracte area(). Fes-ne subclasses Cercle, Rectangle i Triangle, cadascuna amb la seva fórmula.
  2. Suma d’àrees polimòrfica. Amb la jerarquia anterior, guarda diverses figures en un array de Figura i calcula la suma total d’àrees amb un sol bucle.
  3. Empleats. Crea una superclasse Empleat i subclasses Comercial (amb comissió) i Directiu (amb bonus). Sobreescriu un mètode calcularSou() a cadascuna.
  4. Interfície Descomptable. Defineix una interfície amb aplicarDescompte(double pct) i fes que diverses classes no relacionades (Producte, Servei) la implementin.
  5. Contes amb instanceof. Donat un array de Figura, compta quantes són Cercle fent servir instanceof amb pattern matching.
  6. Bloqueja l’herència. Marca una de les teves classes com a final i comprova l’error que dona el compilador si intentes fer-ne extends.