classDiagram
Animal <|-- Gos
Animal <|-- Gat
class Animal {
#String nom
+menjar() void
+ferSo() void
}
class Gos {
+ferSo() void
+buscarPilota() void
}
class Gat {
+ferSo() void
}
14 Herència i polimorfisme
Objectius
- Entendre l’herència i distingir entre superclasse i subclasse.
- Reutilitzar codi amb
extendsi cridar el pare ambsuper. - Sobreescriure mètodes amb
@Overridei saber quan usarfinal. - Definir classes i mètodes
abstracti 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).
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.
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.");
}
}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."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.
@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.
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:
finalen un atribut o variable: no es pot canviar el valor (ja ho vam veure amb les constants).finalen un mètode: no es pot sobreescriure en cap subclasse.finalen una classe: no se’n pot heretar (ningú pot fer-neextends).
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
}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!");
}
}Això dona error de compilació:
Animal a = new Animal("Bèstia"); // ERROR: Animal és abstractUna 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 | Sí | Només constants (static final) |
| Pot tenir codi als mètodes | Sí (mètodes normals) | Sí, amb default (avançat) |
| Constructor | Sí | No |
| Quan usar-la | Jerarquia amb codi comú | Capacitat compartida entre classes no relacionades |
- 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.
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 → AnimalEl 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 GosSi 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!
}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.protectedfa un membre visible per a les subclasses.- Sobreescriure (
@Override) redefineix un mètode heretat; l’anotació ajuda el compilador a detectar errors de signatura. finalimpedeix canviar un valor, sobreescriure un mètode o heretar d’una classe.- Una classe
abstractno 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
- Jerarquia de figures. Crea una classe abstracta
Figuraamb un mètode abstractearea(). Fes-ne subclassesCercle,RectangleiTriangle, cadascuna amb la seva fórmula. - Suma d’àrees polimòrfica. Amb la jerarquia anterior, guarda diverses figures en un array de
Figurai calcula la suma total d’àrees amb un sol bucle. - Empleats. Crea una superclasse
Empleati subclassesComercial(amb comissió) iDirectiu(amb bonus). Sobreescriu un mètodecalcularSou()a cadascuna. - Interfície
Descomptable. Defineix una interfície ambaplicarDescompte(double pct)i fes que diverses classes no relacionades (Producte,Servei) la implementin. - Contes amb
instanceof. Donat un array deFigura, compta quantes sónCerclefent servirinstanceofamb pattern matching. - Bloqueja l’herència. Marca una de les teves classes com a
finali comprova l’error que dona el compilador si intentes fer-neextends.