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:
- La Pila (Stack): Emmagatzema les crides a mètodes i les variables locals. Les variables de tipus primitiu (
int, double, boolean) guarden el seu valor directament a la pila.
- El Munt (Heap): És l'espai de memòria dinàmica on viuen els objectes creats amb la paraula clau
new. Les variables de la pila no contenen l'objecte sencer, sinó només una referència (la seva adreça de memòria).
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