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

  1. Anatomia d'un mètode
  2. El valor de retorn i return
  3. Mètodes void
  4. Pas de paràmetres: Java sempre passa per valor
  5. Què implica això amb primitius
  6. Què implica això amb referències (i per què confon tothom)
  7. Sobrecàrrega de mètodes
  8. Mètodes static enfront de mètodes d'instància
  9. La referència this
  10. Varargs: nombre variable d'arguments
  11. Àmbit de les variables i shadowing
  12. Bones pràctiques de disseny de mètodes
  13. Recursió: un mètode que es crida a si mateix
  14. Refactorització guiada de BiblioTech
  15. Errors Habituals i Consells
  16. Exercicis

  1. Anatomia d'un mètode

Aquesta és l'estructura completa d'un mètode Java:

public double calcularMulta(int diesRetard) {
    return diesRetard * TARIFA_DIARIA;
}
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'argument 5).

Cridar un mètode d'instància es fa sempre a través d'un objecte:

double multa = prestec.calcularMulta(5);

  1. El valor de retorn i return

return 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 double pot retornar un int (promoció automàtica), però un mètode int no pot retornar un double sense 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 return assolit 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.

  1. Mètodes void

Quan un mètode fa alguna cosa en lloc de calcular alguna cosa, el seu tipus de retorn és void:

public void prestar() {
    disponible = false;
}

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.

  1. 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:

  1. Reassignar el paràmetre mai no afecta qui crida (ni amb primitius ni amb objectes).
  2. 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.

  1. 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:

Abans:   20
  Dins del metode: 999
Despres: 20

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:

diesTranscorreguts = incrementar(diesTranscorreguts);

  1. 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:

Despres de mutar:      TITOL CANVIAT
Despres de reassignar: TITOL CANVIAT

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
}

  1. 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
Tipus diferents de paràmetres
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:

  1. Coincidència exacta de tipus.
  2. Promoció de tipus primitius (intlongfloatdouble).
  3. Autoboxing/unboxing (intInteger, mòdul 1).
  4. 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 Object

Consell: 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.

  1. Mètodes static enfront de mètodes d'instància

La 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 No (no hi ha objecte)
Pot fer servir this No
Pot fer servir camps static
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
    }
}
error: non-static variable diesTranscorreguts cannot be referenced from a static context

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.

  1. La referència this

Dins 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:

public void notificar(RegistreAuditoria registre) {
    registre.anotar(this);   // "aqui soc jo"
}

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.

  1. 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");      // 2

Dins 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.

  1. À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
    }
}
new Prestec().demostrarShadowing(5);
// Sortida:
// 5
// 20

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.

  1. 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 main en són el contraexemple perfecte.
  • Noms verbals i precisos. calcularMulta, registrarDevolucio, estaDisponible. Evita processar, gestionar, fer o dades.
  • 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è és true?
// 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.

  1. 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 = 10

Cada 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).

  1. 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 this en 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 -1 per 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(): incrementa prestecsAcumulats en un. Si l'empleat ja és al límit MAX_PRESTECS_SIMULTANIS, no l'incrementa i avisa per consola.
  • registrarDevolucio(): decrementa prestecsAcumulats, mai per sota de zero.
  • potPrendrePrestat(): retorna true si 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);      // 2

Sortida:

M.R.
2
true
AVIS: Marta Ruiz ja te 3 prestecs (maxim 3).
3
2

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:

Prestec.simularEscala(5, 15, 20, 30, 60, 95, 100);
  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  GREU

Tres 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

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats