classDiagram
class CompteBancari {
-String titular
-double saldo
+CompteBancari(String, double)
+ingressar(double) void
+retirar(double) boolean
+getSaldo() double
}
12 Desenvolupament de classes
Objectius
- Definir una classe pròpia amb els seus atributs i mètodes.
- Aplicar l’encapsulació amb els modificadors de visibilitat (
private,public). - Escriure constructors (amb i sense paràmetres) i entendre el constructor per defecte.
- Fer servir la paraula clau
this. - Escriure getters i setters amb validació.
- Redefinir els mètodes
toString()iequals()heretats d’Object. - Comprendre la diferència entre membres d’instància i estàtics en una classe pròpia.
Fins ara hem vist com fer servir objectes de classes que ja existien (Scanner, Random, String). El següent pas és crear les teves pròpies classes. Deixarem de ser només usuari d’objectes per convertir-te en dissenyador d’objectes.
12.1 Què és una classe?
Una classe és el codi que descriu com seran els objectes d’un tipus: quines dades guarden (atributs) i què saben fer (mètodes).
Escriure una classe és omplir el motlle del qual, més tard, en trauràs tants objectes com vulguis amb new.
L’estructura mínima d’una classe en Java és:
public class Llibre {
// 1) Atributs: l'estat de cada objecte
String titol;
String autor;
int anyPublicacio;
// 2) Mètodes: el comportament
void mostrar() {
System.out.println(titol + " (" + autor + ", " + anyPublicacio + ")");
}
}Amb aquesta classe ja podem crear i usar objectes des d’una altra classe amb main:
public class Programa {
public static void main(String[] args) {
Llibre ll = new Llibre();
ll.titol = "La plaça del diamant";
ll.autor = "Mercè Rodoreda";
ll.anyPublicacio = 1962;
ll.mostrar(); // La plaça del diamant (Mercè Rodoreda, 1962)
}
}En Java, cada classe public ha d’anar en un fitxer amb exactament el mateix nom i extensió .java. La classe Llibre ha de viure a Llibre.java.
S’han de respectar també les majúscules: els noms de classe s’escriuen en PascalCase (primera lletra de cada paraula en majúscula).
Mini-repte. Escriu l’esquelet d’una classe Rectangle amb dos atributs (base i alcada, de tipus double) i un mètode void mostrar() que no faci res encara.
12.2 Encapsulació i visibilitat
La classe anterior té un problema greu. Qualsevol part del programa pot escriure directament ll.anyPublicacio = -3000; i deixar l’objecte en un estat sense sentit. Les dades estan exposades i desprotegides.
L’encapsulació és el principi de la POO que diu: amaga les dades internes i controla com s’hi accedeix.
La idea és protegir l’estat de l’objecte perquè no es pugui corrompre des de fora.
No pots ficar la mà a la caixa forta del banc per treure diners. Interactues a través d’una interfície controlada (la pantalla i el teclat del caixer), que comprova el teu PIN i que no treguis més del compte. L’encapsulació fa el mateix: amaga els “diners” (les dades) i només deixa operar-hi a través de mètodes que validen.
Per aconseguir-ho fem servir els modificadors de visibilitat davant de cada membre:
| Modificador | Qui hi pot accedir |
|---|---|
private |
Només des de dins de la mateixa classe |
public |
Des de qualsevol classe |
protected |
La mateixa classe, les subclasses i el mateix paquet |
| (cap) | Les classes del mateix paquet (visibilitat “de paquet”) |
La recepta bàsica de l’encapsulació és:
Atributs
private, mètodes d’accéspublic.
public class Llibre {
private String titol; // Ningú de fora hi arriba directament
private String autor;
private int anyPublicacio;
}Ara ll.titol = "..." des de fora no compila. Bé: hem tancat la porta. Al següent apartat obrirem finestres controlades (getters i setters).
public
És temptador posar public a tot per estalviar-se problemes de compilació. Fer-ho anul·la l’encapsulació i torna el programa fràgil: qualsevol pot deixar un objecte en un estat invàlid. Per defecte, atributs sempre private.
Mini-repte. Reescriu la teva classe Rectangle perquè base i alcada siguin private.
12.3 Getters i setters
Si els atributs són private, com els llegim i modifiquem des de fora? Amb mètodes públics especialitzats:
- Un getter retorna el valor d’un atribut. Es diu
getNom()oisNom()per a booleans. - Un setter modifica el valor d’un atribut. Es diu
setNom(valor)i és el lloc ideal per validar abans d’acceptar el canvi.
public class Llibre {
private String titol;
private int anyPublicacio;
public String getTitol() {
return titol;
}
public void setTitol(String nouTitol) {
this.titol = nouTitol;
}
public int getAnyPublicacio() {
return anyPublicacio;
}
public void setAnyPublicacio(int any) {
// Validació: no acceptem anys fora del rang establert
if (any >= 1450 && any <= 2100) {
this.anyPublicacio = any;
} else {
System.out.println("Any no vàlid, s'ignora: " + any);
}
}
}El gran avantatge: el setAnyPublicacio filtra valors dolents. L’estat de l’objecte queda protegit encara que qui l’usa s’equivoqui.
Si un atribut no ha de canviar mai després de crear l’objecte (per exemple, l’ISBN d’un llibre), no li posis setter. Deixa’l només amb getter (i assigna’l al constructor).
Menys portes obertes, menys errors.
Mini-repte. Afegeix a Rectangle un setter setBase(double b) que només accepti valors positius.
12.4 Constructors
Crear un objecte i després omplir-lo atribut per atribut amb els mètodes getter és pesat i propens a oblits o errors.
El constructor permet donar tots els valors inicials en el moment de crear l’objecte.
Un constructor es diu igual que la classe i no té tipus de retorn.
public class Llibre {
private String titol;
private String autor;
private int anyPublicacio;
// Constructor amb paràmetres
public Llibre(String titol, String autor, int anyPublicacio) {
this.titol = titol;
this.autor = autor;
this.anyPublicacio = anyPublicacio;
}
}Ara la creació és neta i garanteix que l’objecte neix complet:
Llibre ll = new Llibre("La plaça del diamant", "Mercè Rodoreda", 1962);12.4.1 El constructor per defecte
Si no escrius cap constructor, Java en crea un de buit sense paràmetres (el constructor per defecte), que és el que feies servir amb new Llibre().
En el moment que escrius qualsevol constructor propi, Java ja no defineix el constructor buit. Si després fas new Llibre() sense haver-lo definit, no compilarà:
Llibre ll = new Llibre(); // ERROR si només has definit
// el constructor amb 3 paràmetresSolució: si necessites els dos, defineix-los tots dos.
12.4.2 Sobrecàrrega de constructors
Pots tenir diversos constructors a la mateixa classe sempre que tinguin paràmetres diferents (en nombre o tipus). Es diu sobrecàrrega.
public class Llibre {
private String titol;
private String autor;
private int anyPublicacio;
// Constructor complet
public Llibre(String titol, String autor, int anyPublicacio) {
this.titol = titol;
this.autor = autor;
this.anyPublicacio = anyPublicacio;
}
// Constructor curt: reaprofita el complet amb this(...)
public Llibre(String titol, String autor) {
this(titol, autor, 2025); // any per defecte
}
}Fixa’t en this(...): crida un altre constructor de la mateixa classe per no repetir codi.
Mini-repte. Escriu un constructor per a Rectangle que rebi base i alcada, i un segon constructor sense paràmetres que creï un quadrat 1×1 reutilitzant el primer amb this(...).
12.5 La paraula clau this
Has vist this diverses vegades. this és una referència a l’objecte actual: l’objecte sobre el qual s’està executant el mètode o el constructor.
El seu ús més habitual és desambiguar quan un paràmetre es diu igual que un atribut:
public void setTitol(String titol) {
this.titol = titol; // this.titol = atribut ; titol = paràmetre
}Sense this, titol = titol assignaria el paràmetre a si mateix i l’atribut es quedaria buit: un error silenciós molt típic.
Quan una persona diu “aquest cotxe és meu”, el “meu” es refereix a qui parla. this és el “meu” de l’objecte: quan un mètode diu this.titol, vol dir “el títol d’aquest objecte, el que està executant ara mateix el mètode”.
Usos de this:
this.atribut→ distingir atribut de paràmetre.this(...)→ cridar un altre constructor de la mateixa classe.thissol → passar l’objecte actual com a argument a un altre mètode.
this al setter
public void setPreu(double preu) {
preu = preu; // NO fa res útil: assigna el paràmetre a si mateix!
}L’atribut preu de l’objecte no canvia. Ha de ser this.preu = preu;.
12.6 Mètodes: el comportament de la classe
A part dels getters i setters, una classe té mètodes que representen les accions pròpies del domini.
Aquests mètodes operen sobre l’estat de l’objecte (els seus atributs).
public class CompteBancari {
private String titular;
private double saldo;
public CompteBancari(String titular, double saldoInicial) {
this.titular = titular;
this.saldo = saldoInicial;
}
public void ingressar(double quantitat) {
if (quantitat > 0) {
saldo += quantitat;
}
}
public boolean retirar(double quantitat) {
if (quantitat > 0 && quantitat <= saldo) {
saldo -= quantitat;
return true; // Operació feta
}
return false; // Fons insuficients
}
public double getSaldo() {
return saldo;
}
}Fixa’t que dins d’un mètode pots accedir directament als atributs (saldo) sense escriure this, perquè no hi ha ambigüitat.
- i +
Al diagrama de dalt, el símbol - marca els membres privats i el + els públics. És la notació estàndard UML, que veurem més endavant.
Mini-repte. Afegeix a Rectangle dos mètodes: double area() i double perimetre().
12.7 Membres estàtics en una classe pròpia
Ja hem utilitzat membres static de classes com Math. Ara pots posar-ne a les teves. Un membre static pertany a la classe, no a cada objecte: se’n comparteix una sola còpia entre tots.
Un ús molt típic és comptar quants objectes s’han creat o definir constants:
public class Soci {
public static final double QUOTA_ANUAL = 25.0; // Constant compartida
private static int totalSocis = 0; // Comptador compartit
private int numeroSoci;
private String nom;
public Soci(String nom) {
this.nom = nom;
totalSocis++; // Cada nou soci incrementa el total
this.numeroSoci = totalSocis; // i rep un número correlatiu
}
public static int getTotalSocis() {
return totalSocis;
}
}Ús:
Soci a = new Soci("Aina");
Soci b = new Soci("Bru");
System.out.println(Soci.getTotalSocis()); // 2 (es crida amb el nom de la classe)
System.out.println(Soci.QUOTA_ANUAL); // 25.0this en un mètode estàtic
Un mètode static no s’executa sobre cap objecte concret, així que no pot usar this ni accedir a atributs d’instància. getTotalSocis() pot llegir totalSocis (que és static), però no podria llegir nom (que és d’instància).
Mini-repte. Afegeix a Rectangle una constant static final amb el nombre de costats (4).
12.8 Redefinició del mètode toString()
Tota classe en Java penja d’una classe base anomenada Object. S’hereten alguns mètodes que convé redefinir.
El primer és toString(), que retorna una representació de l’objecte en forma de text. Per defecte és inútil:
Llibre ll = new Llibre("La plaça del diamant", "Mercè Rodoreda", 1962);
System.out.println(ll); // Llibre@1b6d3586 ← poc útil!Aquest Llibre@1b6d3586 és el nom de la classe i un codi intern. Redefinim toString() perquè digui alguna cosa útil:
@Override
public String toString() {
return titol + " — " + autor + " (" + anyPublicacio + ")";
}Ara:
System.out.println(ll); // La plaça del diamant — Mercè Rodoreda (1962)
System.out.println crida toString() automàticament. L’anotació @Override indica que estem reescrivint un mètode heretat; la veurem més endavant amb més profunditat.
Mini-repte. Escriu el toString() de Rectangle que retorni, per exemple, "Rectangle 3.0×2.0".
12.9 Redefinir equals()
El segon mètode heretat clau és equals(), que decideix si dos objectes són “iguals”.
Aquí hi ha un parany important. L’operador == entre objectes compara referències (si són el mateix objecte a la memòria), no el contingut:
Llibre a = new Llibre("La plaça del diamant", "Mercè Rodoreda", 1962);
Llibre b = new Llibre("La plaça del diamant", "Mercè Rodoreda", 1962);
System.out.println(a == b); // Fals. Són dos objectes diferents
System.out.println(a.equals(b)); // Per defecte també false.Per defecte, equals() fa el mateix que ==. Si volem que dos llibres amb el mateix contingut es considerin iguals, hem de redefinir equals():
@Override
public boolean equals(Object obj) {
if (this == obj) return true; // Mateix objecte
if (obj == null || getClass() != obj.getClass()) return false;
Llibre altre = (Llibre) obj; // Càsting segur
return anyPublicacio == altre.anyPublicacio
&& titol.equals(altre.titol)
&& autor.equals(altre.autor);
}Ara a.equals(b) retorna true perquè comparem els continguts.
String amb ==
Aquest és, potser, l’error més comú entre principiants:
String s1 = teclat.nextLine(); // L'usuari escriu "hola"
if (s1 == "hola") { ... } // gairebé sempre FALSE!== compara si són el mateix objecte, no el mateix text. Per comparar el contingut de textos s’usa SEMPRE .equals():
if (s1.equals("hola")) { ... } // Correcteequals() i hashCode() van de bracet
Per conveni, si redefineixes equals() també hauries de redefinir hashCode(). És imprescindible perquè els objectes funcionin bé dins de col·leccions com HashSet o HashMap. L’IDE els pot generar tots dos alhora automàticament.
Mini-repte. Explica per què "5" == "5" pot donar true però new String("5") == "5" dona false. (Pista: reserva de textos vs objecte nou.)
12.10 Posem-ho tot junt
Aquesta és la classe Llibre completa, ben encapsulada, amb tot el que hem vist:
public class Llibre {
// --- Atributs (privats) ---
private String titol;
private String autor;
private int anyPublicacio;
private boolean prestat;
// --- Constructor ---
public Llibre(String titol, String autor, int anyPublicacio) {
this.titol = titol;
this.autor = autor;
setAnyPublicacio(anyPublicacio); // reaprofitem la validació
this.prestat = false; // un llibre nou no està prestat
}
// --- Getters i setters ---
public String getTitol() { return titol; }
public String getAutor() { return autor; }
public int getAnyPublicacio() { return anyPublicacio; }
public boolean isPrestat() { return prestat; }
public void setAnyPublicacio(int any) {
if (any >= 1450 && any <= 2100) {
this.anyPublicacio = any;
}
}
// --- Comportament del domini ---
public void prestar() {
if (!prestat) {
prestat = true;
} else {
System.out.println("Ja estava prestat!");
}
}
public void retornar() {
prestat = false;
}
// --- Mètodes heretats redefinits ---
@Override
public String toString() {
String estat = prestat ? "prestat" : "disponible";
return titol + " — " + autor + " (" + anyPublicacio + ") [" + estat + "]";
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Llibre altre = (Llibre) obj;
return anyPublicacio == altre.anyPublicacio
&& titol.equals(altre.titol)
&& autor.equals(altre.autor);
}
}12.11 Resum
- Una classe defineix els atributs (estat) i els mètodes (comportament) dels seus objectes.
- L’encapsulació protegeix l’estat: atributs
private, accés a través de mètodespublic. - Els getters llegeixen i els setters modifiquen atributs; els setters són el lloc per validar.
- Els constructors inicialitzen l’objecte en crear-lo. En definir-ne un de propi es perd el constructor per defecte. Es poden sobrecarregar i encadenar amb
this(...). thisés la referència a l’objecte actual; serveix per desambiguar atributs de paràmetres.- Els membres
staticpertanyen a la classe i es comparteixen entre tots els objectes; les constants es fanstatic final. - Convé redefinir
toString()(representació en text) iequals()(igualtat per contingut). Recorda: per comparar textos s’usa.equals(), mai==.
12.12 Per practicar
Classe
Rectanglecompleta. Recull totes les peces dels mini-reptes: atributs privatsbaseialcada, dos constructors, getters/setters amb validació (només positius),area(),perimetre(), la constantNOMBRE_COSTATSitoString().Classe
Termometre. Atribut privattemperatura(double). Mètodespujar(double),baixar(double)i un getter. El setter no ha de deixar baixar de −273.15.Comptador d’instàncies. Amplia
Rectangleamb un comptadorstaticque digui quants rectangles s’han creat. Prova-ho creant-ne tres i imprimint el total.Detecta l’error. Aquest setter no funciona. Digues per què i corregeix-lo:
public void setAlcada(double alcada) {
alcada = alcada;
}equals()a la pràctica. Crea dos objectesRectanglede 3×2 per separat i comprova què donen==i.equals()abans i després de redefinirequals().Disseny propi. Dissenya des de zero una classe
CompteBancariamb número, titular i saldo, que no permeti descoberts. Escriu unmainque en creï un, hi faci ingressos i retirades, i imprimeixi l’objecte ambtoString().