En acabar la lliçó anterior els teus objectes eren caixes de dades: Llibre guarda un títol i un ISBN, però no sap fer absolutament res amb ells. Tota la intel·ligència de BiblioTech continua tancada a les dues-centes línies de main, que calcula dies de retard, aplica el sostre de la multa i classifica la gravetat com si els objectes no existissin. Aquesta lliçó aporta la meitat que falta: el comportament. Un mètode és una operació amb nom que agrupa un conjunt d'instruccions, rep dades, fa una feina i normalment retorna un resultat. És la unitat de descomposició del codi —el que impedeix que main creixi sense fi— i, en la POO, és la manera com un objecte exposa el que sap fer. En acabar, calcularMulta() no serà un bloc solt enmig d'un switch: serà una cosa que un Prestec sap fer amb si mateix.
Contingut
- Anatomia d'un mètode
- El valor de retorn i
return - Mètodes
void - Pas de paràmetres: Java sempre passa per valor
- Què implica això amb primitius
- Què implica això amb referències (i per què confon tothom)
- Sobrecàrrega de mètodes
- Mètodes
staticenfront de mètodes d'instància - La referència
this - Varargs: nombre variable d'arguments
- Àmbit de les variables i shadowing
- Bones pràctiques de disseny de mètodes
- Recursió: un mètode que es crida a si mateix
- Refactorització guiada de BiblioTech
- Errors Habituals i Consells
- Exercicis
- Anatomia d'un mètode
Aquesta és l'estructura completa d'un mètode Java:
| Part | A l'exemple | Què significa |
|---|---|---|
| Modificador d'accés | public |
Qui el pot cridar (03-07) |
| Altres modificadors | (cap) | static, final, abstract... |
| Tipus de retorn | double |
Quin tipus de valor retorna; void si no en retorna cap |
| Nom | calcularMulta |
Verb en lowerCamelCase |
| Llista de paràmetres | (int diesRetard) |
Dades d'entrada, amb tipus i nom |
| Cos | { return ...; } |
Les instruccions que s'executen |
Al conjunt nom + tipus de paràmetres en ordre se l'anomena signatura del mètode. La signatura d'aquest és calcularMulta(int). El tipus de retorn no forma part de la signatura, i això tindrà conseqüències a l'apartat 7.
Vocabulari que convé fixar per no confondre's mai més:
- Paràmetre: la variable declarada a la definició del mètode (
int diesRetard). - Argument: el valor concret que es passa en cridar-lo (
calcularMulta(5)passa l'argument5).
Cridar un mètode d'instància es fa sempre a través d'un objecte:
- El valor de retorn i
return
returnreturn fa dues coses alhora: lliura un valor a qui crida i acaba immediatament l'execució del mètode. El segon efecte el vas veure a la lliçó 02-04, aplicat al control de flux.
public String classificarGravetat(int diesRetard) {
if (diesRetard == 0) {
return "SENSE RETARD"; // surt aqui mateix
}
if (diesRetard <= LLINDAR_LLEU) {
return "LLEU";
}
return "GREU";
}Regles del compilador que has de conèixer:
- El valor retornat ha de ser compatible amb el tipus declarat. Un mètode
doublepot retornar unint(promoció automàtica), però un mètodeintno pot retornar undoublesense casting explícit. - Tots els camins han de retornar un valor. Si el compilador troba una ruta sense
return, falla amb "missing return statement". - El codi posterior a un
returnassolit sempre és codi inabastable i no compila.
Aquest mètode, per exemple, no compila:
public String classificar(int dies) {
if (dies > 0) {
return "AMB RETARD";
}
// ERROR: si dies <= 0 no hi ha return
}N'hi ha prou d'afegir un else o un return final per arreglar-ho. La segona forma —fer servir clàusules de guarda i deixar el cas general al final, sense else— és la que vas fer servir a 02-01 i la que es prefereix en codi professional.
- Mètodes
void
voidQuan un mètode fa alguna cosa en lloc de calcular alguna cosa, el seu tipus de retorn és void:
En un mètode void pots fer servir return; tot sol per sortir abans d'hora, però no pots retornar cap valor:
public void retornar() {
if (disponible) {
System.out.println("El llibre ja estava disponible.");
return; // sortida anticipada, sense valor
}
disponible = true;
}Un criteri útil de disseny, conegut com a separació entre consultes i ordres: un mètode o bé canvia l'estat (i retorna void) o bé respon una pregunta (i retorna un valor), però preferiblement no totes dues coses. Un mètode anomenat getMulta() que a més marqués el llibre com a retornat sorprendria qualsevol que el llegís.
- Pas de paràmetres: Java sempre passa per valor
Aquí hi ha un dels punts on més gent s'equivoca, fins i tot amb anys d'experiència. La regla és aquesta, i no té excepcions:
En Java, els arguments es passen sempre per valor. És a dir: en cridar un mètode, es copia el valor de l'argument al paràmetre.
El que despista és què s'està copiant en cada cas:
| Tipus d'argument | Què es copia | Conseqüència |
|---|---|---|
Primitiu (int, double, boolean...) |
El valor mateix | El mètode treballa amb una còpia independent |
Referència (String, Llibre, Prestec...) |
L'adreça, no l'objecte | El mètode arriba al mateix objecte, però amb la seva pròpia fletxa |
D'aquí les dues conseqüències que cal memoritzar:
- Reassignar el paràmetre mai no afecta qui crida (ni amb primitius ni amb objectes).
- Mutar l'objecte al qual apunta el paràmetre sí que afecta qui crida (perquè és el mateix objecte).
Els dos apartats següents ho demostren amb codi.
- Què implica això amb primitius
public static void intentarModificar(int dies) {
dies = 999; // nomes canvia la copia local
System.out.println(" Dins del metode: " + dies);
}
public static void main(String[] args) {
int diesTranscorreguts = 20;
System.out.println("Abans: " + diesTranscorreguts);
intentarModificar(diesTranscorreguts);
System.out.println("Despres: " + diesTranscorreguts);
}Sortida:
El paràmetre dies és una variable nova a la pila del mètode, inicialitzada amb una còpia de 20. Canviar-la no toca la variable original. Si un mètode ha de "modificar" un primitiu, l'única via és retornar el nou valor:
- Què implica això amb referències (i per què confon tothom)
Ara el cas interessant. Dos mètodes aparentment similars, amb resultats oposats:
public static void mutarLlibre(Llibre llibre) {
llibre.titol = "TITOL CANVIAT"; // muta l'objecte compartit
}
public static void reassignarLlibre(Llibre llibre) {
llibre = new Llibre(); // reapunta NOMES la fletxa local
llibre.titol = "ALTRE LLIBRE";
}
public static void main(String[] args) {
Llibre javaEficac = new Llibre();
javaEficac.titol = "Java Eficac";
mutarLlibre(javaEficac);
System.out.println("Despres de mutar: " + javaEficac.titol);
reassignarLlibre(javaEficac);
System.out.println("Despres de reassignar: " + javaEficac.titol);
}Sortida:
La segona crida no ha fet res visible. Vegem per què, a la memòria:
flowchart TB
subgraph P1["Pila de main"]
M["javaEficac = 0x1A"]
end
subgraph P2["Pila de reassignarLlibre"]
L1["llibre = 0x1A (en entrar)"]
L2["llibre = 0x9F (despres de new)"]
end
subgraph H["Heap"]
O1["0x1A : Llibre
titol = TITOL CANVIAT"]
O2["0x9F : Llibre
titol = ALTRE LLIBRE"]
end
M --> O1
L1 -.-> O1
L2 --> O2
En entrar a reassignarLlibre, el paràmetre llibre rep una còpia de l'adreça 0x1A: hi ha dues fletxes apuntant al mateix objecte. Quan el mètode fa llibre = new Llibre(), mou la seva pròpia fletxa al nou objecte 0x9F. La fletxa de main no s'ha tocat. En sortir del mètode, la pila local desapareix i l'objecte 0x9F queda inabastable.
A mutarLlibre, en canvi, no es toca cap fletxa: es fa servir el punt per modificar l'objecte al qual totes dues apunten, i per això el canvi es veu des de main.
La frase que ho resumeix tot: pots canviar el contingut de l'objecte que et passen, però no pots canviar a quin objecte apunta la variable de qui et crida.
Un corol·lari pràctic: String és immutable (mòdul 1), així que cap mètode no pot alterar la cadena que li passes. Això no funciona mai:
public static void embrutar(String text) {
text = text.toUpperCase(); // crea una altra cadena; qui crida no se n'assabenta
}
- Sobrecàrrega de mètodes
Sobrecarregar és declarar diversos mètodes amb el mateix nom i diferent llista de paràmetres a la mateixa classe. Java tria quin cridar segons els arguments.
public class Prestec {
/** Multa d'aquest prestec, fent servir els seus propis dies transcorreguts. */
public double calcularMulta() {
return calcularMulta(diesTranscorreguts);
}
/** Multa que correspondria a un nombre concret de dies transcorreguts. */
public double calcularMulta(int diesTranscorreguts) {
int retard = Math.max(0, diesTranscorreguts - DIES_PRESTEC);
return Math.min(retard * TARIFA_DIARIA, MULTA_MAXIMA);
}
/** Multa amb una tarifa diferent (per exemple, en una campanya especial). */
public double calcularMulta(int diesTranscorreguts, double tarifa) {
int retard = Math.max(0, diesTranscorreguts - DIES_PRESTEC);
return Math.min(retard * tarifa, MULTA_MAXIMA);
}
}Què distingeix una sobrecàrrega vàlida:
| Canvi | Sobrecàrrega vàlida? |
|---|---|
| Nombre diferent de paràmetres | Sí |
| Tipus diferents de paràmetres | Sí |
| Ordre diferent de tipus diferents | Sí ((int, String) enfront de (String, int)) |
| Només nom diferent dels paràmetres | No: error de compilació |
| Només tipus de retorn diferent | No: el retorn no forma part de la signatura |
Regles de resolució, que Java aplica en aquest ordre fins a trobar una coincidència:
- Coincidència exacta de tipus.
- Promoció de tipus primitius (
int→long→float→double). - Autoboxing/unboxing (
int↔Integer, mòdul 1). - Varargs (apartat 10), que sempre és l'última opció considerada.
public static void registrar(int dies) { System.out.println("versio int"); }
public static void registrar(double dies) { System.out.println("versio double"); }
public static void registrar(Object dies) { System.out.println("versio Object"); }
registrar(5); // "versio int" -> coincidencia exacta
registrar(5.0); // "versio double" -> coincidencia exacta
registrar(5L); // "versio double" -> long promociona a double abans que a ObjectConsell: la sobrecàrrega és útil quan les variants fan conceptualment el mateix amb dades diferents. Si dos mètodes amb el mateix nom fan coses diferents, posa'ls noms diferents.
- Mètodes
static enfront de mètodes d'instància
static enfront de mètodes d'instànciaLa diferència és la mateixa que vas veure amb els camps a 03-02:
| Aspecte | Mètode d'instància | Mètode static |
|---|---|---|
| Pertany a | Un objecte concret | La classe |
| Com es crida | prestec.calcularMulta() |
Prestec.esRetardLleu(5) |
| Pot fer servir camps d'instància | Sí | No (no hi ha objecte) |
Pot fer servir this |
Sí | No |
Pot fer servir camps static |
Sí | Sí |
| Quan s'ha de fer servir | L'operació depèn de l'estat de l'objecte | L'operació només depèn dels seus paràmetres |
Exemples del món real que ja coneixes: Math.max(a, b) i Integer.parseInt(text) són static perquè no necessiten cap estat; scanner.nextLine() és d'instància perquè depèn de l'estat d'aquell Scanner. I main és static perquè la JVM l'ha de poder cridar abans que existeixi cap objecte.
Aplicat a BiblioTech:
public class Prestec {
/** D'instancia: fa servir les dades d'AQUEST prestec. */
public int calcularDiesRetard() {
return Math.max(0, diesTranscorreguts - DIES_PRESTEC);
}
/** Estatic: pura funcio dels seus parametres, util sense tenir cap prestec. */
public static boolean esRetardLleu(int diesRetard) {
return diesRetard > 0 && diesRetard <= LLINDAR_LLEU;
}
}Error clàssic de principiant, i probablement el missatge del compilador que veuràs més vegades:
public class Prestec {
public int diesTranscorreguts;
public static int malament() {
return diesTranscorreguts; // ERROR
}
}La traducció és literal: un mètode static no està associat a cap objecte, així que la pregunta "els dies transcorreguts de qui?" no té resposta.
- La referència
this
thisDins d'un mètode d'instància, this és una referència a l'objecte sobre el qual s'ha cridat el mètode. Té tres usos:
a) Desambiguar quan un paràmetre es diu igual que un camp (l'ús més freqüent, i el que veuràs constantment en constructors):
public void establirDiesTranscorreguts(int diesTranscorreguts) {
this.diesTranscorreguts = diesTranscorreguts; // camp = parametre
}Sense this, diesTranscorreguts = diesTranscorreguts; assigna el paràmetre a si mateix i el camp no canvia. El compilador no es queixa; l'error és silenciós.
b) Cridar un altre mètode del mateix objecte (opcional, però de vegades més llegible):
public String generarResum() {
return this.classificarGravetat() + " - " + this.calcularMulta() + " EUR";
}c) Passar l'objecte actual a un altre mètode:
Fora d'aquests casos, escriure this. davant de tot és soroll. En Java l'estil dominant és ometre'l llevat que sigui necessari. A 03-04 apareixerà una quarta forma, this(...), per encadenar constructors.
- Varargs: nombre variable d'arguments
Els varargs permeten declarar un paràmetre que accepta zero, un o molts arguments del mateix tipus. S'escriuen amb tres punts:
public static void registrarEsdeveniments(String... esdeveniments) {
System.out.println("Esdeveniments rebuts: " + esdeveniments.length);
for (String esdeveniment : esdeveniments) {
System.out.println(" - " + esdeveniment);
}
}Crides vàlides:
registrarEsdeveniments(); // 0 esdeveniments
registrarEsdeveniments("Prestec registrat"); // 1
registrarEsdeveniments("Devolucio", "Multa aplicada"); // 2Dins del mètode, esdeveniments és un array (String[]), amb el seu length i el seu bucle for-each. Els arrays s'estudien a fons a la lliçó 05-01; aquí n'hi ha prou de saber que es recorren així.
Dues regles obligatòries:
- Només hi pot haver un paràmetre varargs per mètode.
- Ha de ser l'últim de la llista:
registrar(String empleat, String... esdeveniments)és vàlid; a l'inrevés, no compila.
Ja has fet servir varargs sense saber-ho: System.out.printf(String format, Object... args) és exactament això, i per això admet qualsevol nombre de valors després de la cadena de format.
- Àmbit de les variables i shadowing
L'àmbit (scope) d'una variable és la regió de codi on és visible. Resum:
| On es declara | Des d'on es veu | Quant viu |
|---|---|---|
| Camp d'instància | Tots els mètodes de la classe | El que visqui l'objecte |
Camp static |
Tots els mètodes, inclosos els static |
Tot el programa |
| Paràmetre | Només dins del seu mètode | L'execució del mètode |
| Variable local | Des de la seva declaració fins al } del seu bloc |
L'execució del bloc |
El shadowing passa quan una variable local o un paràmetre té el mateix nom que un camp: dins d'aquell àmbit, el nom es refereix a la variable local, i el camp queda "a l'ombra".
public class Prestec {
public int diesTranscorreguts = 20; // camp
public void demostrarShadowing(int diesTranscorreguts) { // parametre que enfosqueix
System.out.println(diesTranscorreguts); // el PARAMETRE
System.out.println(this.diesTranscorreguts); // el CAMP
}
}El shadowing no és un error —de fet és la pràctica habitual en constructors i setters, on el paràmetre es diu igual que el camp a propòsit—, però exigeix recordar this. en assignar.
- Bones pràctiques de disseny de mètodes
Aquests criteris distingeixen un mètode professional d'un d'improvisat:
- Un mètode, un propòsit. Si per descriure el que fa necessites la paraula "i", probablement són dos mètodes.
- Curts. No hi ha cap xifra màgica, però si un mètode no cap a la pantalla, costa raonar-hi. Els dos-cents de
mainen són el contraexemple perfecte. - Noms verbals i precisos.
calcularMulta,registrarDevolucio,estaDisponible. Evitaprocessar,gestionar,ferodades. - Convenció de noms booleans:
esX,estaX,teX.if (llibre.estaDisponible())es llegeix com una frase. - Pocs paràmetres. Amb més de tres o quatre, planteja't agrupar dades relacionades en un objecte. És, un altre cop, el problema de les variables soltes.
- Evita els paràmetres booleans.
generarInforme(true)no diu res en llegir-lo. Què éstrue?
// Malament: que significa aquest false?
imprimirRebut(prestec, false);
// Millor: dos metodes amb nom explicit
imprimirRebutComplet(prestec);
imprimirRebutResumit(prestec);- No barregis nivells d'abstracció. Un mètode que calcula regles de negoci no hauria de formatar a més amb
printf. És el tema central de la lliçó 03-08. - Documenta amb Javadoc el que no sigui obvi: què retorna, què espera i què passa en els casos límit.
- Recursió: un mètode que es crida a si mateix
Un mètode es pot cridar a si mateix. A això se l'anomena recursió, i tota recursió correcta té dues parts: un cas base que l'atura i un cas recursiu que s'acosta al cas base.
/** Suma 1 + 2 + ... + n de forma recursiva. */
public static int sumarFinsA(int n) {
if (n <= 0) { // cas base: atura la recursio
return 0;
}
return n + sumarFinsA(n - 1); // cas recursiu
}Traça de sumarFinsA(4):
sumarFinsA(4) = 4 + sumarFinsA(3)
= 4 + (3 + sumarFinsA(2))
= 4 + (3 + (2 + sumarFinsA(1)))
= 4 + (3 + (2 + (1 + sumarFinsA(0))))
= 4 + 3 + 2 + 1 + 0 = 10Cada crida pendent ocupa un marc a la pila. Si falta el cas base o no s'assoleix mai, la pila s'esgota i obtens un StackOverflowError, que reconeixeràs perquè la traça repeteix la mateixa línia centenars de vegades.
En el dia a dia, la recursió brilla amb estructures que s'imbriquen per naturalesa —arbres, directoris, expressions— i per a la resta sol ser preferible un bucle, que gasta menys memòria. La veuràs aplicada a la lliçó 05-09 (cerca binària) i al mòdul 7 (recórrer directoris).
- Refactorització guiada de BiblioTech
Arriba el moment d'aplicar tot això al projecte. Ho farem en dos passos, perquè així es fa a la vida real: primer extreure, després mudar.
Pas 1: extreure els càlculs de main a mètodes static
Aquest és el codi que avui viu dins del switch de main:
// --- ABANS: dins de main, enmig del case "1" ---
int diesRetard = diesTranscorreguts - DIES_PRESTEC;
if (diesRetard < 0) {
diesRetard = 0;
}
double multa = diesRetard * TARIFA_DIARIA;
if (multa >= MULTA_MAXIMA) {
multa = MULTA_MAXIMA;
}
String gravetat;
if (diesRetard == 0) {
gravetat = "SENSE RETARD";
} else if (diesRetard <= LLINDAR_LLEU) {
gravetat = "LLEU";
} else {
gravetat = "GREU";
}Primera millora: treure-ho a mètodes amb nom.
// --- DESPRES (pas 1): metodes static a BiblioTechApp ---
/**
* Calcula els dies de retard, saturats a zero.
* @param diesTranscorreguts dies des que es va fer el prestec
* @return 0 si encara es en termini; els dies d'exces en cas contrari
*/
private static int calcularDiesRetard(int diesTranscorreguts) {
return Math.max(0, diesTranscorreguts - DIES_PRESTEC);
}
/**
* Calcula la multa corresponent a un retard, aplicant el sostre legal.
* @param diesRetard dies de retard, mai negatius
* @return import en euros, mai superior a MULTA_MAXIMA
*/
private static double calcularMulta(int diesRetard) {
return Math.min(diesRetard * TARIFA_DIARIA, MULTA_MAXIMA);
}
/**
* Classifica la gravetat d'un retard segons la politica de Nexus Software.
* @param diesRetard dies de retard, mai negatius
* @return "SENSE RETARD", "LLEU" o "GREU"
*/
private static String classificarGravetat(int diesRetard) {
if (diesRetard == 0) return "SENSE RETARD";
if (diesRetard <= LLINDAR_LLEU) return "LLEU";
return "GREU";
}I el case "1" queda així:
int diesRetard = calcularDiesRetard(diesTranscorreguts);
double multa = calcularMulta(diesRetard);
String gravetat = classificarGravetat(diesRetard);Tres línies llegibles en lloc de vint. Fixa't en l'ús de Math.max i Math.min, que substitueixen els if de saturació: Math.max(0, x) és "mai per sota de zero" i Math.min(x, sostre) és "mai per damunt del sostre". Menys codi i menys llocs on equivocar-se.
Pas 2: mudar els mètodes a Prestec
El pas 1 millora la llegibilitat, però deixa les regles de negoci a la classe d'arrencada. I aquestes regles no són de l'aplicació: són del préstec. Aquest és el pas realment orientat a objectes.
package com.nexussoftware.bibliotech.domini;
/**
* Registre d'un prestec d'un llibre a un empleat de Nexus Software.
* Coneix les seves propies dades i sap aplicar-hi les regles de negoci.
*/
public class Prestec {
public static final int DIES_PRESTEC = 15;
public static final double TARIFA_DIARIA = 0.25;
public static final double MULTA_MAXIMA = 20.0;
public static final int LLINDAR_LLEU = 7;
public static int prestecsCreats = 0;
public Llibre llibre;
public String empleat;
public int diesTranscorreguts;
// ---------- Comportament d'instancia ----------
/** @return dies de retard d'AQUEST prestec, saturats a zero. */
public int calcularDiesRetard() {
return Math.max(0, diesTranscorreguts - DIES_PRESTEC);
}
/** @return multa d'AQUEST prestec en euros, amb el sostre aplicat. */
public double calcularMulta() {
return Math.min(calcularDiesRetard() * TARIFA_DIARIA, MULTA_MAXIMA);
}
/** @return "SENSE RETARD", "LLEU" o "GREU" segons el retard d'AQUEST prestec. */
public String classificarGravetat() {
int retard = calcularDiesRetard();
if (retard == 0) return "SENSE RETARD";
if (retard <= LLINDAR_LLEU) return "LLEU";
return "GREU";
}
/** @return true si el termini de 15 dies ja s'ha superat. */
public boolean estaVencut() {
return calcularDiesRetard() > 0;
}
/** @return true si la multa ha assolit el sostre de MULTA_MAXIMA. */
public boolean teMultaAlSostre() {
return calcularMulta() >= MULTA_MAXIMA;
}
/** @return dies que falten per vencer; 0 si ja ha vencut. */
public int diesRestants() {
return Math.max(0, DIES_PRESTEC - diesTranscorreguts);
}
// ---------- Utilitat estatica ----------
/**
* Indica si un retard donat es considera lleu, sense necessitat de tenir
* cap prestec construit. Util per a taules i simulacions.
*/
public static boolean esRetardLleu(int diesRetard) {
return diesRetard > 0 && diesRetard <= LLINDAR_LLEU;
}
}I ara, l'abans i el després del càlcul a main:
| Abans (mòdul 2) | Després (aquest mòdul) |
|---|---|
20 línies dins de case "1" |
prestec.calcularMulta() |
| Les regles es repeteixen a l'escala de multes | Existeixen una sola vegada, a Prestec |
Canviar la tarifa obliga a revisar tot main |
Es canvia una constant |
| Les dades van soltes per paràmetres | Viatgen dins de l'objecte |
| Només es pot calcular el préstec "en curs" | Es pot calcular qualsevol préstec |
Ús des de BiblioTechApp:
Prestec prestec = new Prestec();
prestec.llibre = refactoritzacio;
prestec.empleat = "Marta Ruiz";
prestec.diesTranscorreguts = 20;
Prestec.prestecsCreats++;
System.out.printf("Llibre: %s%n", prestec.llibre.titol);
System.out.printf("Empleat: %s%n", prestec.empleat);
System.out.printf("Retard: %d dies%n", prestec.calcularDiesRetard());
System.out.printf("Multa: %.2f EUR%n", prestec.calcularMulta());
System.out.printf("Gravetat: %s%n", prestec.classificarGravetat());
System.out.printf("Vencut: %b%n", prestec.estaVencut());Sortida:
Llibre: Refactoritzacio Empleat: Marta Ruiz Retard: 5 dies Multa: 1,25 EUR Gravetat: LLEU Vencut: true
Aquest és el moment en què BiblioTech es torna orientat a objectes de debò: main ja no calcula res, només pregunta. I observa que calcularMulta() crida internament calcularDiesRetard(): un mètode es pot recolzar en un altre del mateix objecte, evitant duplicar la lògica de saturació.
Afegim també comportament a Llibre, que fins ara era pura dada:
public class Llibre {
public static int llibresCreats = 0;
public String titol;
public String autor;
public String isbn;
public int anyPublicacio;
public boolean disponible;
/** @return true si l'exemplar es pot prestar ara mateix. */
public boolean estaDisponible() {
return disponible;
}
/** Marca l'exemplar com a prestat. Avisa si ja ho estava. */
public void prestar() {
if (!disponible) {
System.out.println("AVIS: '" + titol + "' ja estava prestat.");
return;
}
disponible = false;
}
/** Marca l'exemplar com a retornat. Avisa si ja era al prestatge. */
public void retornar() {
if (disponible) {
System.out.println("AVIS: '" + titol + "' ja estava disponible.");
return;
}
disponible = true;
}
/** @return descripcio llegible del llibre per a llistats. */
public String descriure() {
return titol + " - " + autor + " (" + anyPublicacio + "), ISBN " + isbn;
}
}Fixa't en prestar() i retornar(): són la raó de ser de l'encapsulament. Amb el camp públic, qualsevol pot escriure llibre.disponible = true; saltant-se l'avís; amb aquests mètodes, l'operació passa per un únic lloc que pot validar. La lliçó 03-07 tancarà aquesta porta definitivament.
Errors Habituals i Consells
- Oblidar
thisen un setter.diesTranscorreguts = diesTranscorreguts;compila, no fa res i no avisa. És dels errors més frustrants: l'IDE l'assenyala com a "assignació a si mateix", fes-li cas. - Creure que Java passa objectes per referència. No: passa la referència per valor. La diferència es nota exactament quan reassignes el paràmetre, com a l'apartat 6.
- Intentar fer servir camps d'instància des d'un mètode
static. El compilador dirà "non-static variable ... cannot be referenced from a static context". Pregunta't si el mètode hauria de ser d'instància o si li falta rebre l'objecte per paràmetre. - Sobrecarregar canviant només el tipus de retorn. No compila: la signatura no inclou el retorn.
- Mètodes que fan dues coses.
calcularIGuardarMulta()és un senyal d'alarma en el nom mateix. - Retornar codis numèrics màgics. Retornar
-1per indicar "no s'ha pogut calcular" obliga cada crida a conèixer aquell conveni. Mentre no disposis d'excepcions (mòdul 6) ni d'Optional(10-04), documenta el conveni amb Javadoc de manera explícita. - Consell: fes servir "extreure mètode" de l'IDE. IntelliJ (
Ctrl+Alt+M) i Eclipse (Alt+Shift+M) fan la refactorització del pas 1 automàticament i sense errors: selecciones el bloc, li dones nom i l'IDE dedueix els paràmetres i el retorn. - Consell: escriu primer la signatura i el Javadoc, després el cos. Obliga a decidir què entra i què surt abans de perdre's en la implementació.
Exercicis
Exercici 1: comportament a Empleat
Amplia la classe Empleat de la lliçó anterior amb aquests mètodes:
registrarPrestec(): incrementaprestecsAcumulatsen un. Si l'empleat ja és al límitMAX_PRESTECS_SIMULTANIS, no l'incrementa i avisa per consola.registrarDevolucio(): decrementaprestecsAcumulats, mai per sota de zero.potPrendrePrestat(): retornatruesi encara no ha assolit el màxim.getInicials(): retorna les inicials de l'empleat ("Marta Ruiz" → "M.R.").- Una versió sobrecarregada
registrarPrestec(int quantitat)que en registri diversos de cop respectant el límit.
Prova'ls amb Marta Ruiz fins que assoleixi el límit.
Exercici 2: demostrar el pas per valor
Escriu una classe DemoPasPerValor amb quatre mètodes static que rebin, respectivament: un int, un String, un Llibre que es muta i un Llibre que es reassigna. Cadascun ha d'imprimir el valor en entrar i en sortir. Des de main, crida'ls tots quatre i imprimeix l'estat abans i després de cada crida. Després respon per escrit: per què el cas del String es comporta com el de l'int i no com el del Llibre mutat?
Exercici 3: simulador d'escala de multes amb varargs
Escriu a Prestec un mètode static simularEscala(int... diesTranscorreguts) que rebi qualsevol nombre de valors de dies transcorreguts i imprimeixi una taula amb: dies transcorreguts, dies de retard, multa sense sostre, multa aplicada i gravetat. Ha de marcar amb <-- la primera fila on la multa assoleixi el sostre. Reutilitza els mètodes existents i no dupliquis cap regla de negoci. Prova'l amb simularEscala(5, 15, 20, 30, 60, 95, 100).
Solucions
Solució 1
package com.nexussoftware.bibliotech.domini;
public class Empleat {
public static final int MAX_PRESTECS_SIMULTANIS = 3;
public static int empleatsRegistrats = 0;
public String nom;
public String identificador;
public int prestecsAcumulats;
/** @return true si encara pot endur-se un altre llibre. */
public boolean potPrendrePrestat() {
return prestecsAcumulats < MAX_PRESTECS_SIMULTANIS;
}
/** Registra un prestec si el limit ho permet. */
public void registrarPrestec() {
if (!potPrendrePrestat()) {
System.out.printf("AVIS: %s ja te %d prestecs (maxim %d).%n",
nom, prestecsAcumulats, MAX_PRESTECS_SIMULTANIS);
return; // sortida anticipada
}
prestecsAcumulats++;
}
/**
* Registra diversos prestecs de cop. Versio sobrecarregada: mateixa
* operacio conceptual amb entrada diferent.
*/
public void registrarPrestec(int quantitat) {
for (int i = 0; i < quantitat; i++) {
registrarPrestec(); // reutilitza la logica i el limit
}
}
/** Registra una devolucio, sense baixar mai de zero. */
public void registrarDevolucio() {
prestecsAcumulats = Math.max(0, prestecsAcumulats - 1);
}
/** @return inicials del nom complet, per exemple "M.R." */
public String getInicials() {
String[] parts = nom.trim().split(" ");
StringBuilder sb = new StringBuilder(); // patro de la llico 02-02
for (String part : parts) {
if (!part.isEmpty()) {
sb.append(part.charAt(0)).append('.');
}
}
return sb.toString().toUpperCase();
}
}Prova:
Empleat marta = new Empleat();
marta.nom = "Marta Ruiz";
marta.identificador = "EMP-001";
System.out.println(marta.getInicials()); // M.R.
marta.registrarPrestec(2); // 2 prestecs
System.out.println(marta.prestecsAcumulats); // 2
System.out.println(marta.potPrendrePrestat()); // true
marta.registrarPrestec(); // 3
marta.registrarPrestec(); // AVIS: limit assolit
System.out.println(marta.prestecsAcumulats); // 3
marta.registrarDevolucio();
System.out.println(marta.prestecsAcumulats); // 2Sortida:
Claus: registrarPrestec(int) no duplica la comprovació del límit, delega en la versió sense paràmetres; i Math.max(0, ...) a la devolució evita un comptador negatiu sense necessitat d'un if.
Solució 2
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.domini.Llibre;
public class DemoPasPerValor {
static void ambPrimitiu(int dies) {
System.out.println(" entra: " + dies);
dies = 999;
System.out.println(" surt: " + dies);
}
static void ambString(String titol) {
System.out.println(" entra: " + titol);
titol = titol.toUpperCase(); // crea UNA ALTRA cadena; nomes canvia la copia local
System.out.println(" surt: " + titol);
}
static void mutantObjecte(Llibre llibre) {
System.out.println(" entra: " + llibre.titol);
llibre.titol = "MUTAT"; // toca l'objecte compartit
System.out.println(" surt: " + llibre.titol);
}
static void reassignantObjecte(Llibre llibre) {
System.out.println(" entra: " + llibre.titol);
llibre = new Llibre(); // mou NOMES la fletxa local
llibre.titol = "OBJECTE NOU";
System.out.println(" surt: " + llibre.titol);
}
public static void main(String[] args) {
int dies = 20;
System.out.println("1) Primitiu. Abans: " + dies);
ambPrimitiu(dies);
System.out.println(" Despres: " + dies); // 20
String titol = "Java Eficac";
System.out.println("2) String. Abans: " + titol);
ambString(titol);
System.out.println(" Despres: " + titol); // Java Eficac
Llibre llibre = new Llibre();
llibre.titol = "Patrons de Disseny";
System.out.println("3) Objecte mutat. Abans: " + llibre.titol);
mutantObjecte(llibre);
System.out.println(" Despres: " + llibre.titol); // MUTAT
Llibre altre = new Llibre();
altre.titol = "Refactoritzacio";
System.out.println("4) Objecte reassignat. Abans: " + altre.titol);
reassignantObjecte(altre);
System.out.println(" Despres: " + altre.titol); // Refactoritzacio
}
}Sortida:
1) Primitiu. Abans: 20 entra: 20 surt: 999 Despres: 20 2) String. Abans: Java Eficac entra: Java Eficac surt: JAVA EFICAC Despres: Java Eficac 3) Objecte mutat. Abans: Patrons de Disseny entra: Patrons de Disseny surt: MUTAT Despres: MUTAT 4) Objecte reassignat. Abans: Refactoritzacio entra: Refactoritzacio surt: OBJECTE NOU Despres: Refactoritzacio
Resposta a la pregunta. El String es comporta com l'int perquè és immutable: toUpperCase() no modifica la cadena original, en crea una de nova i la retorna. En assignar-la al paràmetre, només es mou la fletxa local, exactament igual que en el cas 4. No és que els String es passin d'una altra manera —es passen per valor de la referència, com qualsevol objecte—, és que no existeix cap forma de mutar-los, així que el cas 3 és impossible amb String. Aquest és un dels grans avantatges de la immutabilitat, i la raó per la qual a 03-07 veuràs que Llibre és un bon candidat a ser immutable també.
Solució 3
/**
* Imprimeix l'escala de multes per a una serie de valors de dies transcorreguts.
* @param diesTranscorreguts zero o mes valors a simular
*/
public static void simularEscala(int... diesTranscorreguts) {
System.out.println(" ESCALA DE MULTES DE BIBLIOTECH");
System.out.println(" ==================================================");
System.out.printf(" %8s %9s %12s %11s %-12s%n",
"TRANSC.", "RETARD", "SENSE SOSTRE", "APLICADA", "GRAVETAT");
boolean sostreJaMarcat = false; // patro bandera (llico 02-02)
for (int dies : diesTranscorreguts) {
// Es reutilitza la MATEIXA logica del domini: zero duplicacio de regles.
int retard = Math.max(0, dies - DIES_PRESTEC);
double senseSostre = retard * TARIFA_DIARIA;
double aplicada = Math.min(senseSostre, MULTA_MAXIMA);
String gravetat;
if (retard == 0) gravetat = "SENSE RETARD";
else if (esRetardLleu(retard)) gravetat = "LLEU";
else gravetat = "GREU";
String marca = "";
if (!sostreJaMarcat && aplicada >= MULTA_MAXIMA) {
marca = " <--";
sostreJaMarcat = true; // un cop aixecada, no torna a baixar
}
System.out.printf(" %8d %9d %12.2f %11.2f %-12s%s%n",
dies, retard, senseSostre, aplicada, gravetat, marca);
}
}Crida i sortida:
ESCALA DE MULTES DE BIBLIOTECH
==================================================
TRANSC. RETARD SENSE SOSTRE APLICADA GRAVETAT
5 0 0,00 0,00 SENSE RETARD
15 0 0,00 0,00 SENSE RETARD
20 5 1,25 1,25 LLEU
30 15 3,75 3,75 GREU
60 45 11,25 11,25 GREU
95 80 20,00 20,00 GREU <--
100 85 21,25 20,00 GREUTres detalls que mereixen atenció. Primer, el mètode és static perquè simula situacions hipotètiques: no hi ha cap préstec real al darrere, només números. Segon, reutilitza esRetardLleu(int), també static, sense duplicar el llindar. I tercer, simularEscala() sense arguments és una crida perfectament vàlida: imprimeix només la capçalera, perquè un varargs admet zero elements.
Un detall a discutir: aquest mètode barreja càlcul i presentació, justament el que desaconsella l'apartat 12. És acceptable en una utilitat de diagnòstic com aquesta, però recorda-ho: a la lliçó 03-08 veuràs per què en el codi de negoci aquesta barreja es paga cara.
Conclusió
Els teus objectes ja saben fer coses. Domines l'anatomia completa d'un mètode —modificadors, tipus de retorn, signatura, paràmetres, cos— i la diferència entre paràmetre i argument; saps quan un mètode retorna valor i quan és void, i per què convé que no faci totes dues coses. Has entès la regla que més confusió genera en Java: tot es passa per valor, inclosos els objectes, la referència dels quals es copia; d'aquí que mutar l'objecte rebut afecti qui crida i reassignar el paràmetre no. Saps sobrecarregar mètodes i coneixes les regles amb què Java tria la versió correcta, distingeixes mètodes static de mètodes d'instància i comprens per què un static no pot tocar l'estat d'un objecte que no té. Manegues this, els varargs, l'àmbit de les variables i el shadowing, i tens un catàleg de bones pràctiques per escriure mètodes que altres puguin llegir. Has vist la recursió amb el seu cas base i el seu risc de StackOverflowError.
I, sobretot, has fet la primera gran refactorització de BiblioTech: els càlculs de retard, multa i gravetat han sortit de main, s'han convertit en mètodes amb nom i després han mudat a la classe Prestec, que n'és la propietària legítima. main ja no calcula: pregunta.
Queda un cap solt evident. Els teus objectes continuen naixent buits: new Prestec() produeix un préstec sense llibre, sense empleat i amb zero dies, i cal omplir-lo camp a camp confiant a no oblidar-ne cap. A la lliçó següent, Constructors, tancaràs aquesta porta: aprendràs a exigir les dades imprescindibles en el moment de la creació, a sobrecarregar i encadenar constructors, a controlar l'ordre d'inicialització i a validar les dades perquè cap objecte de BiblioTech no pugui néixer en un estat invàlid.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
