← Índex del curs Exercicis

Unitat 5. Classes, Herència i Excepcions

Unitat Didàctica 5:

En aquesta unitat aprendràs:

Disseny de classes: atributs, mètodes, constructors i this
Model de memòria de la JVM: Pila (Stack) vs Munt (Heap) i pas de paràmetres
Encapsulació (private/public), mètodes estàtics i Records
Herència amb extends, constructors amb super() i sobreescriptura @Override
Polimorfisme dinàmic, classes abstractes, interfícies i classes segellades
Gestió d'errors i excepcions amb try-catch-finally, creació d'excepcions i asercions

Programació Orientada a Objectes: Disseny, Jerarquies i Robustesa

Fins ara hem treballat amb dades soltes i funcions bàsiques. La Programació Orientada a Objectes (POO) és una manera d'estructurar els programes organitzant el codi en classes que combinen tant les dades (atributs) com les operacions (mètodes) que les manipulen.

1. Conceptes Fonamentals: Classes i Instàncies

Una classe és el plànol o plantilla de disseny. Un objecte (o instància) és la realització concreta d'aquest plànol a la memòria del computador:

class Cotxe { // Camps o atributs (estat de l'objecte) String model; int velocitat; // Constructor per inicialitzar l'objecte public Cotxe(String model) { this.model = model; this.velocitat = 0; } // Mètode (comportament) public void accelerar(int increment) { this.velocitat += increment; } }

2. Com Funciona la Memòria: Pila (Stack) vs Heap

Per entendre realment com programa un ordinador, és fonamental conèixer els dos espais de memòria principals:

Variables primitives i pas per valor

Quan passem una variable primitiva a un mètode, Java en fa una còpia exacta del valor. Qualsevol modificació dins del mètode no afecta la variable original de qui l'ha cridat:

int numA = 98 + 1; int numB = numA; numB = 55; System.out.println(numA); // 99 System.out.println(numB); // 55
int numA = 98 + 1; int numB = numA; numB = 55; System.out.println(numA); // 99 System.out.println(numB); // 55
int numA = 98 + 1; int numB = numA; numB = 55; System.out.println(numA); // 99 System.out.println(numB); // 55
int numA = 98 + 1; int numB = numA; numB = 55; System.out.println(numA); // 99 System.out.println(numB); // 55
int numA = 98 + 1; int numB = numA; numB = 55; System.out.println(numA); // 99 System.out.println(numB); // 55
int numA = 98 + 1; int numB = numA; numB = 55; System.out.println(numA); // 99 System.out.println(numB); // 55
int numA = 98 + 1; int numB = numA; numB = 55; System.out.println(numA); // 99 System.out.println(numB); // 55

Variables de referència a la memòria

Quan declarem una variable d'un tipus d'objecte, aquesta variable guarda un apuntador (referència) a la zona del Heap on resideix l'objecte:

class Alumne { float nota; } Alumne aluA = new Alumne(); aluA.nota = 10; Alumne aluB = aluA; aluB.nota = 5; System.out.println(aluA.nota); System.out.println(aluB.nota);
class Alumne { float nota; } Alumne aluA = new Alumne(); aluA.nota = 10; Alumne aluB = aluA; aluB.nota = 5; System.out.println(aluA.nota); System.out.println(aluB.nota);
class Alumne { float nota; } Alumne aluA = new Alumne(); aluA.nota = 10; Alumne aluB = aluA; aluB.nota = 5; System.out.println(aluA.nota); System.out.println(aluB.nota);
class Alumne { float nota; } Alumne aluA = new Alumne(); aluA.nota = 10; Alumne aluB = aluA; aluB.nota = 5; System.out.println(aluA.nota); System.out.println(aluB.nota);
class Alumne { float nota; } Alumne aluA = new Alumne(); aluA.nota = 10; Alumne aluB = aluA; aluB.nota = 5; System.out.println(aluA.nota); System.out.println(aluB.nota);
class Alumne { float nota; } Alumne aluA = new Alumne(); aluA.nota = 10; Alumne aluB = aluA; aluB.nota = 5; System.out.println(aluA.nota); System.out.println(aluB.nota);
class Alumne { float nota; } Alumne aluA = new Alumne(); aluA.nota = 10; Alumne aluB = aluA; aluB.nota = 5; System.out.println(aluA.nota); System.out.println(aluB.nota);
class Alumne { float nota; } Alumne aluA = new Alumne(); aluA.nota = 10; Alumne aluB = aluA; aluB.nota = 5; System.out.println(aluA.nota); System.out.println(aluB.nota);

Pas de referències a mètodes

Quan passem un objecte com a paràmetre, el mètode rep una còpia de la referència, la qual apunta exactament al mateix objecte a la memòria Heap:

class Professor { void aprovar(float parametreNota) { parametreNota = 5; } } float nota = 0; Professor unProfessor = new Professor(); unProfessor.aprovar(nota); System.out.println(nota); // 0
class Professor { void aprovar(float parametreNota) { parametreNota = 5; } } float nota = 0; Professor unProfessor = new Professor(); unProfessor.aprovar(nota); System.out.println(nota); // 0
class Professor { void aprovar(float parametreNota) { parametreNota = 5; } } float nota = 0; Professor unProfessor = new Professor(); unProfessor.aprovar(nota); System.out.println(nota); // 0
class Professor { void aprovar(float parametreNota) { parametreNota = 5; } } float nota = 0; Professor unProfessor = new Professor(); unProfessor.aprovar(nota); System.out.println(nota); // 0
class Professor { void aprovar(float parametreNota) { parametreNota = 5; } } float nota = 0; Professor unProfessor = new Professor(); unProfessor.aprovar(nota); System.out.println(nota); // 0
class Professor { void aprovar(float parametreNota) { parametreNota = 5; } } float nota = 0; Professor unProfessor = new Professor(); unProfessor.aprovar(nota); System.out.println(nota); // 0
class Professor { void aprovar(float parametreNota) { parametreNota = 5; } } float nota = 0; Professor unProfessor = new Professor(); unProfessor.aprovar(nota); System.out.println(nota); // 0
class Professor { void aprovar(float parametreNota) { parametreNota = 5; } } float nota = 0; Professor unProfessor = new Professor(); unProfessor.aprovar(nota); System.out.println(nota); // 0

Modificació d'objectes referenciats

Atès que ambdues referències apunten al mateix objecte, si el mètode modifica les propietats internes de l'objecte, aquests canvis es mantenen quan el mètode finalitza:

class Alumne { float nota; } class Professor { void aprovar(Alumne parametreAlumne) { parametreAlumne.nota = 5; } } Alumne unAlumne = new Alumne(); Professor unProfessor = new Professor(); unProfessor.aprovar(unAlumne); System.out.println(unAlumne.nota); // 5
class Alumne { float nota; } class Professor { void aprovar(Alumne parametreAlumne) { parametreAlumne.nota = 5; } } Alumne unAlumne = new Alumne(); Professor unProfessor = new Professor(); unProfessor.aprovar(unAlumne); System.out.println(unAlumne.nota); // 5
class Alumne { float nota; } class Professor { void aprovar(Alumne parametreAlumne) { parametreAlumne.nota = 5; } } Alumne unAlumne = new Alumne(); Professor unProfessor = new Professor(); unProfessor.aprovar(unAlumne); System.out.println(unAlumne.nota); // 5
class Alumne { float nota; } class Professor { void aprovar(Alumne parametreAlumne) { parametreAlumne.nota = 5; } } Alumne unAlumne = new Alumne(); Professor unProfessor = new Professor(); unProfessor.aprovar(unAlumne); System.out.println(unAlumne.nota); // 5
class Alumne { float nota; } class Professor { void aprovar(Alumne parametreAlumne) { parametreAlumne.nota = 5; } } Alumne unAlumne = new Alumne(); Professor unProfessor = new Professor(); unProfessor.aprovar(unAlumne); System.out.println(unAlumne.nota); // 5
class Alumne { float nota; } class Professor { void aprovar(Alumne parametreAlumne) { parametreAlumne.nota = 5; } } Alumne unAlumne = new Alumne(); Professor unProfessor = new Professor(); unProfessor.aprovar(unAlumne); System.out.println(unAlumne.nota); // 5
class Alumne { float nota; } class Professor { void aprovar(Alumne parametreAlumne) { parametreAlumne.nota = 5; } } Alumne unAlumne = new Alumne(); Professor unProfessor = new Professor(); unProfessor.aprovar(unAlumne); System.out.println(unAlumne.nota); // 5
class Alumne { float nota; } class Professor { void aprovar(Alumne parametreAlumne) { parametreAlumne.nota = 5; } } Alumne unAlumne = new Alumne(); Professor unProfessor = new Professor(); unProfessor.aprovar(unAlumne); System.out.println(unAlumne.nota); // 5
class Alumne { float nota; } class Professor { void aprovar(Alumne parametreAlumne) { parametreAlumne.nota = 5; } } Alumne unAlumne = new Alumne(); Professor unProfessor = new Professor(); unProfessor.aprovar(unAlumne); System.out.println(unAlumne.nota); // 5

3. Encapsulació i Modificadors de Visibilitat

El principi d'encapsulació estableix que l'estat intern d'un objecte ha d'estar protegit d'alteracions directes i indegudes des de l'exterior. Per això declarem els atributs com a private i proporcionem mètodes públics d'accés (getters) i modificació controlada (setters):

class CompteBancari { private double saldo; // Protegit de modificacions directes public double getSaldo() { return this.saldo; } public void ingressar(double quantitat) { if (quantitat > 0) { this.saldo += quantitat; } } }

4. Membres Estàtics i Records Moderns

La paraula clau static indica que un atribut o mètode pertany a la classe en general i no a una instància particular.

A més, per a classes que únicament actuen com a contenidors immutables de dades, Java 26 ofereix els Records, que generen automàticament constructor, getters, equals, hashCode i toString en una sola línia:

public record Punt(int x, int y) {} // Ús: Punt p = new Punt(10, 20); // println(p.x()); // 10

El Concepte d'Herència (extends)

L'herència permet crear noves classes basades en classes existents, heretant-ne els seus atributs i mètodes per evitar duplicar codi:

// Superclasse base: public class Vehicle { protected String marca; protected int velocitatMaxima; public Vehicle(String marca, int velocitatMaxima) { this.marca = marca; this.velocitatMaxima = velocitatMaxima; } public void accelerar() { println(marca + " està accelerant."); } } // Subclasse filla: public class Cotxe extends Vehicle { private int nombrePortes; public Cotxe(String marca, int velocitatMaxima, int nombrePortes) { super(marca, velocitatMaxima); // Crida al constructor de la superclasse this.nombrePortes = nombrePortes; } @Override public void accelerar() { println(marca + " (cotxe de " + nombrePortes + " portes) accelera veloçment."); } }

Polimorfisme Dinàmic

El polimorfisme és la capacitat de tractar objectes de diferents subclasses com si fossin del tipus de la seva superclasse o interfície comuna, executant en temps d'execució la versió adequada del mètode sobreescrit:

void main() { List flota = List.of( new Cotxe("Toyota", 180, 5), new Cotxe("Tesla", 250, 4) ); for (Vehicle v : flota) { v.accelerar(); // S'executa la versió especialitzada de cada vehicle! } }

Classes Abstractes i Interfícies

Una classe abstracta és una classe que no es pot instanciar directament amb new i que defineix operacions que les subclasses estan obligades a implementar:

public abstract class Figura { protected String color; public Figura(String color) { this.color = color; } // Mètode abstracte (sense cos): public abstract double calcularArea(); } public class Cercle extends Figura { private double radi; public Cercle(String color, double radi) { super(color); this.radi = radi; } @Override public double calcularArea() { return Math.PI * radi * radi; } }

Interfícies (interface)

Una interfície defineix un contracte de comportament que qualsevol classe pot implementar amb implements, donant suport a implementació múltiple:

public interface Dibuixable { void dibuixar(); // Mètode default modern amb implementació per defecte: default void mostrarDescripcio() { println("Element gràfic dibuixable."); } }

Classes Segellades en Java Modern (sealed)

Les classes segellades permeten a una superclasse controlar i restringir de forma explícita quines altres classes poden heretar d'ella mitjançant la clàusula permits:

public abstract sealed class Forma permits Cercle, Rectangle, Triangle { // Només aquestes tres classes tenen autorització per heretar de Forma! }

Observa el diagrama de classes següent que representa una jerarquia de figures geomètriques amb una classe segellada (sealed):

classDiagram class Figura { <<sealed abstract>> +String color +calcularArea()* double } class Cercle { <<final>> +double radi +calcularArea() double } class Rectangle { <<final>> +double base +double alcada +calcularArea() double } Figura <|-- Cercle : permits Figura <|-- Rectangle : permits style Figura fill:#1e3a8a,stroke:#38bdf8,stroke-width:2px,color:#fff style Cercle fill:#0f172a,stroke:#34d399,stroke-width:1px,color:#fff style Rectangle fill:#0f172a,stroke:#34d399,stroke-width:1px,color:#fff

7. Gestió d'Errors i Excepcions

Quan programem, els errors inesperats (com intentar dividir per zero, llegir un fitxer que no existeix o rebre un text quan esperàvem un número) no haurien de fer caure sobtadament la nostra aplicació.

Captura d'excepcions amb try-catch-finally

Per gestionar aquests casos disposem del bloc try-catch-finally:

try { int resultat = 10 / 0; // Provocarà una ArithmeticException println("Resultat: " + resultat); } catch (ArithmeticException e) { println("Error matemàtic detectat: " + e.getMessage()); } finally { println("Aquest bloc s'executa sempre, hi hagi o no error."); }

Creació i llançament d'excepcions pròpies

Podem definir les nostres pròpies excepcions creant una classe que hereti de Exception o RuntimeException, i llançar-les amb la instrucció throw:

public class EdatInvalidaException extends Exception { public EdatInvalidaException(String missatge) { super(missatge); } } void registrarUsuari(int edat) throws EdatInvalidaException { if (edat < 0 || edat > 120) { throw new EdatInvalidaException("L'edat ha d'estar entre 0 i 120 anys: " + edat); } println("Usuari registrat amb èxit!"); }

Asercions per a depuració (assert)

Durant el desenvolupament podem utilitzar la sentència assert per comprovar condicions que mai haurien de ser falses si el codi funciona correctament:

int saldo = 100; assert saldo >= 0 : "El saldo mai pot ser negatiu";

Exercicis Pràctics i Qüestionaris

Posa a prova el que has après amb els projectes de la classe Persona, TextBox, jerarquia Animals, Workers, Shape2D i exercicis de gestió d'excepcions.

Exercicis