En tancar la lliçó anterior, BiblioTechApp ja recorria una cua de devolucions, filtrava amb continue, s'aturava amb break i mantenia mitja dotzena d'acumuladors. I amb aquesta complexitat arriba inevitablement el moment en què el programa compila, s'executa sense queixar-se i dona un número que no és el correcte. Aquí acaba el que es pot resoldre rellegint el codi i comença la depuració. Depurar no és un truc ni un talent innat: és un mètode, i és probablement l'habilitat que més separa un programador que avança d'un altre que s'encalla. Per a algú que aprèn de manera autodidacta és encara més crític, perquè no hi ha ningú al costat a qui preguntar "per què em dona 0?". Aquesta lliçó t'ensenya a veure l'estat intern del teu programa: primer amb traces, després amb el depurador de l'IDE, i sempre seguint un procediment que converteix la cerca a cegues en una investigació ordenada.

Contingut

  1. Per què llegir el codi no n'hi ha prou
  2. Els tres tipus d'error i quin es depura
  3. Depuració amb traces: System.out.println estratègic
  4. Què imprimir en una traça i en quin format
  5. El patró de traça d'entrada i sortida d'un bloc
  6. Per què les traces han de desaparèixer (i què les substitueix)
  7. El depurador de l'IDE: conceptes i arrencada
  8. Punts d'interrupció i ordres de pas
  9. Variables, expressions i watch
  10. Punts d'interrupció condicionals
  11. Llegir una traça de pila (stack trace)
  12. El mètode sistemàtic de depuració
  13. Cas pràctic: el resum de multes que surt malament
  14. Errors Habituals i Consells
  15. Exercicis

  1. Per què llegir el codi no n'hi ha prou

Quan un programa no fa el que esperes, la temptació és rellegir-lo. El problema és que rellegeixes el que creies haver escrit, no el que vas escriure. El teu cervell completa el que falta, salta el <= que havia de ser < i no veu la declaració que és dins del bucle en lloc de fora.

La depuració funciona a l'inrevés: en lloc de raonar sobre el que el codi hauria de fer, s'observa el que fa. Es mira el valor real de cada variable en cada moment i es compara amb el valor esperat. Tan bon punt trobes el primer punt on el real i l'esperat divergeixen, has localitzat l'error: és entre aquest punt i l'anterior que sí que era correcte.

Aquesta diferència entre deduir i observar és tota la lliçó.

  1. Els tres tipus d'error i quin es depura

Tipus Quan apareix Qui avisa Exemple Com es resol
De compilació En compilar javac o l'IDE, amb línia i missatge Falta un ;, tipus incompatibles Llegir el missatge (lliçó 01-03)
D'execució En executar La JVM atura el programa amb una traça de pila NumberFormatException en convertir "vint" Es gestionen al mòdul 6; aquí només es llegeixen
Lògic No "apareix" mai Ningú. El programa funciona i dona un resultat incorrecte El total de multes surt 0,00 € Depuració

Els errors lògics són els perillosos: no trenquen res, simplement menteixen. Un rebut amb la multa mal calculada s'imprimeix igual de bé que un de correcte. I són exactament l'objectiu d'aquesta lliçó.

  1. Depuració amb traces: System.out.println estratègic

La tècnica més antiga i encara una de les més emprades: imprimir l'estat del programa en punts escollits. Se'n diu printf debugging o depuració amb traces.

Els seus avantatges són reals: no necessita eines, funciona en qualsevol entorn, deixa un registre que es pot llegir sencer d'una passada i serveix fins i tot on el depurador no arriba (codi concurrent, processos remots). El seu desavantatge també: obliga a modificar el codi, recompilar i tornar a executar a cada intent.

Una traça mal posada no serveix de res:

System.out.println("aqui");
System.out.println("entra");
System.out.println(multa);

En executar veuràs aqui, entra i un número solt, sense saber quina de les cinc variables és ni en quina iteració ets. Una traça útil diu on és, quina variable mostra i quant val:

System.out.println("[bucle] n=" + n + " diesRetard=" + diesRetard
                   + " multa=" + multa + " total=" + totalPendent);

Sortida:

[bucle] n=1 diesRetard=37 multa=9.25 total=9.25
[bucle] n=2 diesRetard=74 multa=18.5 total=27.75
[bucle] n=3 diesRetard=111 multa=20.0 total=47.75

D'un cop d'ull comproves la progressió i detectes el punt exacte on el número deixa de ser el que esperaves.

  1. Què imprimir en una traça i en quin format

Regles pràctiques, ordenades per utilitat:

  1. Imprimeix sempre el nom de la variable al costat del seu valor. multa=9.25 és informació; 9.25 és soroll.
  2. Marca la ubicació amb una etiqueta entre claudàtors. [validacio], [bucle], [resum]. Quan tinguis quinze línies de traça, aquesta etiqueta és el que et diu d'on va sortir cadascuna.
  3. Inclou-hi la variable de control del bucle. Sense n=3 no saps en quina iteració passa el problema.
  4. Fes servir printf quan el format importi. Els double amb molts decimals són il·legibles: System.out.printf("[bucle] n=%d multa=%.2f%n", n, multa);
  5. Fes servir System.err per a l'excepcional. Es mostra en vermell a la majoria d'IDE i va per un canal diferent, així que pots separar la traça normal de l'anòmala.
  6. Traça també els boolean i les condicions. És habitual descobrir que la condició que creies certa és falsa:
boolean pagada = (n % 2 == 0);
System.out.println("[filtre] n=" + n + " pagada=" + pagada
                   + " -> " + (pagada ? "S'OMET" : "ES PROCESSA"));
  1. Delimita el bucle per fora. Imprimir l'estat just abans d'entrar i just després de sortir et diu si el problema és al bucle o en el que ve abans o després:
System.out.println("[abans] total=" + totalPendent + " revisades=" + revisades);
for (...) { ... }
System.out.println("[despres] total=" + totalPendent + " revisades=" + revisades);

  1. El patró de traça d'entrada i sortida d'un bloc

Quan un bloc de codi transforma unes dades d'entrada en unes de sortida, el patró més rendible és imprimir tots dos extrems:

// ENTRADA del bloc de calcul
System.out.printf("[calcul:IN ] diesTranscorreguts=%d DIES_PRESTEC=%d%n",
                  diesTranscorreguts, DIES_PRESTEC);

int diesRetard = diesTranscorreguts - DIES_PRESTEC;
if (diesRetard < 0) {
    diesRetard = 0;
}
double multa = diesRetard * TARIFA_DIARIA;
if (multa > MULTA_MAXIMA) {
    multa = MULTA_MAXIMA;
}

// SORTIDA del bloc de calcul
System.out.printf("[calcul:OUT] diesRetard=%d multa=%.2f%n", diesRetard, multa);

Amb això pots aplicar la bisecció: si l'entrada és correcta i la sortida no, l'error és dins d'aquest bloc. Si l'entrada ja ve malament, l'error és abans i no té sentit mirar aquí. Cada parell de traces divideix el programa en dos i descarta una meitat. És el mateix principi de la cerca binària, i és la manera més ràpida d'acorralar un error en un programa llarg.

Quan arribis al mòdul 3 i escriguis mètodes, aquest patró es convertirà en l'estàndard: una traça en entrar amb els paràmetres rebuts i una altra en sortir amb el valor retornat.

  1. Per què les traces han de desaparèixer (i què les substitueix)

Les traces són bastides, no arquitectura. Deixar-les al codi té conseqüències reals:

  • Embruten la sortida. L'usuari de BiblioTech no ha de veure [bucle] n=3 multa=20.0 al mig del seu rebut.
  • Costen rendiment. L'escriptura per consola és lentíssima comparada amb qualsevol càlcul; milers de println en un bucle poden multiplicar per deu el temps d'execució.
  • Filtren informació. En un sistema real, imprimir dades d'usuaris o credencials a la consola és un problema de seguretat.
  • No es poden apagar. Un System.out.println hi és o no hi és; no hi ha terme mitjà.

Per això, tan bon punt trobis l'error, esborra la traça. I per a la informació que sí que vols conservar de manera permanent, existeix el logging: un sistema amb nivells (DEBUG, INFO, WARN, ERROR) que es poden activar o desactivar per configuració sense tocar el codi, amb marca de temps, origen i destinació configurable (consola, fitxer, servidor central). Això s'estudia a la lliçó 06-07 (Estratègies de Gestió d'Errors i Logging). De moment, la regla és: traça per investigar, esborrar en acabar.

Un truc de transició mentrestant, aplicable ja amb el que saps:

final boolean DEPURAR = true;     // posa'l a false per silenciar totes les traces

if (DEPURAR) {
    System.out.printf("[bucle] n=%d multa=%.2f%n", n, multa);
}

Una sola constant controla totes les traces. És una versió rudimentària del que fa un sistema de logging, i serveix perfectament per a un programa de consola d'aquesta mida.

  1. El depurador de l'IDE: conceptes i arrencada

Un depurador (debugger) és una eina que executa el teu programa sota control: pot pausar-lo en qualsevol línia, mostrar-te el valor de totes les variables en aquell instant i avançar instrucció a instrucció. No modifica el codi i no requereix recompilar per canviar el que observes. És, amb diferència, la manera més eficient d'investigar un error lògic.

Tots els IDE de Java (IntelliJ IDEA, Eclipse, VS Code amb l'Extension Pack for Java, NetBeans) n'inclouen un, i tots funcionen amb els mateixos conceptes i pràcticament les mateixes dreceres, perquè per sota fan servir la mateixa tecnologia de la JVM.

Per arrencar en mode depuració:

  • IntelliJ IDEA: botó amb la icona d'insecte al costat del d'executar, o Shift + F9.
  • Eclipse: menú Run > Debug As > Java Application, o F11.
  • VS Code: pestanya Run and Debug, o F5.

La diferència amb l'execució normal és que, si hi ha algun punt d'interrupció actiu, el programa s'aturarà en arribar-hi i et retornarà el control.

  1. Punts d'interrupció i ordres de pas

Un punt d'interrupció (breakpoint) és una marca en una línia concreta que li diu al depurador "atura't aquí". Es col·loca fent clic al marge esquerre de l'editor, a l'altura del número de línia; hi apareix un cercle vermell. Tornant a fer clic es treu.

On posar-los, en ordre d'utilitat:

  • A la primera línia del cos d'un bucle sospitós.
  • A la línia on s'assigna la variable que surt malament.
  • Just abans i just després del bloc que vols examinar.
  • A la branca d'un if que creus que no s'executa (si el programa s'hi atura, la teva creença era falsa; informació valuosíssima).

Un cop aturat, s'avança amb aquestes ordres:

Ordre IntelliJ Eclipse Què fa
Step Over F8 F6 Executa la línia actual completa i s'atura a la següent. Si hi ha una crida a un mètode, l'executa sencera sense entrar-hi
Step Into F7 F5 Igual, però entra dins del mètode cridat per depurar-lo línia a línia
Step Out Shift + F8 F7 Acaba d'executar el mètode actual i torna a qui l'ha cridat
Resume F9 F8 Continua l'execució normal fins al punt d'interrupció següent (o fins al final)
Run to Cursor Alt + F9 Ctrl + R Executa fins a la línia on és el cursor, sense posar-hi un punt d'interrupció
Stop Ctrl + F2 Mata el procés

En aquest mòdul tot el teu codi és al main, així que Step Over és l'ordre que faràs servir el 95 % del temps: prement-la repetidament recorres el programa línia a línia. Step Into prendrà sentit al mòdul 3, quan tinguis mètodes propis. Un consell sobre Step Into: si el prems en una línia amb System.out.println, acabaràs dins del codi font de la biblioteca estàndard de Java, que no és el que volies; fes servir Step Over per a les crides que no són teves.

Flux típic d'una sessió de depuració:

flowchart TD
    A["Collocar breakpoint a la linia sospitosa"] --> B["Arrencar en mode depuracio"]
    B --> C["El programa s'atura al breakpoint"]
    C --> D["Llegir la finestra de Variables"]
    D --> E{"Els valors son els esperats?"}
    E -- "si" --> F["Step Over: avancar una linia"]
    F --> D
    E -- "no" --> G["Error localitzat entre l'ultima<br/>comprovacio correcta i aquesta"]
    G --> H["Corregir i tornar a executar"]

  1. Variables, expressions i watch

Quan el programa està aturat, l'IDE mostra un panell de Variables amb tot el que existeix en aquell punt: els paràmetres, les variables locals i els seus valors actuals. És la fotografia de l'estat del teu programa.

En una aturada dins del bucle de revisió de BiblioTech veuries alguna cosa així:

Variable Valor
args String[0]
TARIFA_DIARIA 0.25
MULTA_MAXIMA 20.0
n 3
diesRetard 111
pagada false
multa 27.75
totalPendent 9.25
revisades 1

Només llegint aquesta taula ja saps en quina iteració ets i si els acumuladors porten el que han de portar.

Dues eines complementàries:

Avaluació d'expressions. Permet escriure una expressió Java qualsevol i veure'n el resultat en el context actual, sense modificar el codi. A IntelliJ és Alt + F8 (Evaluate Expression); a Eclipse, la vista Expressions o Ctrl + Shift + I sobre una selecció. És utilíssim per comprovar una condició abans que s'avaluï:

diesRetard * TARIFA_DIARIA           -> 27.75
multa >= MULTA_MAXIMA                -> true
diesRetard <= LLINDAR_LLEU           -> false
n % 2 == 0                           -> false
totalPendent + multa                 -> 29.25

Preguntar al depurador "quant val aquesta condició ara mateix?" resol molts errors en trenta segons.

Watch (expressions vigilades). Una expressió afegida a la llista de watches es reavalua automàticament a cada aturada. Serveix per vigilar un acumulador al llarg de totes les iteracions sense buscar-lo a la llista de variables. Afegeix totalPendent i revisades com a watch i en veuràs l'evolució volta a volta.

  1. Punts d'interrupció condicionals

Un bucle de 500 iteracions amb un punt d'interrupció normal t'obliga a prémer Resume 500 vegades. Un punt d'interrupció condicional només atura el programa quan es compleix una condició que tu escrius.

Es configura fent clic dret sobre el cercle vermell del punt d'interrupció; apareix un camp Condition on s'escriu una expressió booleana Java vàlida en aquell punt:

diesRetard > 30

Amb aquesta condició, el programa només s'aturarà a les iteracions en què el retard superi els 30 dies. Altres condicions habituals a BiblioTech:

Condició Per a què serveix
n == 7 Anar directament a la iteració problemàtica
diesRetard > 30 Aturar-se només en els casos greus
multa >= MULTA_MAXIMA Veure exactament quan s'aplica el sostre
totalPendent > 50.0 Descobrir en quin punt l'acumulat es dispara
titol.equals("Refactoritzacio") Aturar-se només en un llibre concret
!pagada && diesRetard == 0 Caçar una combinació rara que sospites

És la diferència entre revisar 500 aturades i revisar-ne 3. Juntament amb l'avaluació d'expressions, és la funcionalitat del depurador que més temps estalvia.

  1. Llegir una traça de pila (stack trace)

Quan es produeix un error d'execució, la JVM atura el programa i imprimeix a System.err una traça de pila: el llistat de crides que estaven actives en aquell moment. Encara no saps gestionar errors —això és el mòdul 6—, però sí que has de saber llegir-los, perquè te'n sortiran des d'ara mateix.

Exception in thread "main" java.lang.NumberFormatException: For input string: "vint"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:665)
	at java.base/java.lang.Integer.parseInt(Integer.java:781)
	at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:23)

Es llegeix així, i l'ordre és important:

  • Línia 1: el fil (main), el tipus d'error (java.lang.NumberFormatException) i el missatge (For input string: "vint"). Aquesta línia ja sol dir-te què ha passat: s'ha intentat convertir a número un text que no ho era.
  • Línies següents: la pila de crides, de més recent a més antiga. La primera és on va esclatar; l'última és on va començar tot.
  • La línia que t'importa és la primera que menciona EL TEU codi: com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:23). Allà tens classe, mètode i número de línia. Les de dalt són dins de java.base, la biblioteca estàndard, i gairebé mai no són el problema.

Regla pràctica: llegeix la primera línia per saber què ha passat i busca la primera línia amb el teu paquet per saber on. Als IDE, aquestes línies són enllaços: un clic et porta al codi.

Els tres errors d'execució que més veuràs en aquest mòdul:

Error Causa típica a BiblioTech
NumberFormatException Integer.parseInt("vint") o cadena buida
NullPointerException Cridar un mètode sobre una variable String que val null
ArithmeticException: / by zero Divisió entera per zero (amb double no passa: dona Infinity)

  1. El mètode sistemàtic de depuració

Tenir eines no n'hi ha prou; cal procediment. Aquest funciona sempre i evita les tres hores de canviar coses a l'atzar:

1. Reproduir. Troba unes dades d'entrada amb què la fallada es produeixi sempre. Un error que no saps reproduir no el pots arreglar ni verificar. Anota exactament quines entrades fas servir: a BiblioTech, per exemple, "Marta Ruiz / 3 llibres / 27, 10, 120 dies".

2. Definir el que s'espera. Escriu el resultat correcte abans de mirar el codi. "El total hauria de ser 23,00 €". Sense aquesta referència no pots saber quan has acabat. Calcula'l a mà si cal.

3. Aïllar per bisecció. Divideix el programa per la meitat amb una traça o un punt d'interrupció i comprova si l'estat en aquell punt és correcte. Si ho és, l'error és després; si no, és abans. Repeteix-ho sobre la meitat que sobreviu. Amb deu passos s'acorrala un error en mil línies.

4. Formular una hipòtesi concreta. No "alguna cosa falla al bucle", sinó "sospito que totalMultes es reinicia a cada iteració perquè està declarat dins del for". Una hipòtesi ha de ser comprovable.

5. Verificar-la. Posa un punt d'interrupció o una traça que confirmi o descarti aquesta hipòtesi exacta. Si la descarta, torna al pas 4 amb una altra. No canviïs mai el codi per "provar a veure" abans d'haver verificat: és la manera més eficaç d'introduir un segon error damunt del primer.

6. Corregir. Un sol canvi, el mínim, i que ataqui la causa i no el símptoma. Si el total surt malament, la solució no és sumar-li una constant al final.

7. Comprovar. Torna a executar amb les dades del pas 1 i verifica el resultat del pas 2. I després prova altres jocs de dades, especialment els límits: zero llibres, zero dies, valors a la vora exacta de cada condició. Una correcció que arregla un cas i en trenca dos més és pitjor que l'error original.

  1. Cas pràctic: el resum de multes que surt malament

Anem amb un error real, del tipus que et passarà la setmana vinent. El bibliotecari de Nexus Software processa una tanda de tres devolucions i el total li surt malament.

El codi (amb l'error):

package com.nexussoftware.bibliotech;

public class BiblioTechApp {
    public static void main(String[] args) {

        final int DIES_PRESTEC = 15;
        final double TARIFA_DIARIA = 0.25;
        final double MULTA_MAXIMA = 20.0;

        String empleat = "Marta Ruiz";
        int devolucions = 3;

        int llibresAmbRetard = 0;

        System.out.println("Processant devolucions de " + empleat);

        for (int n = 1; n <= devolucions; n++) {

            double totalMultes = 0.0;                        // <-- linia 22

            int diesTranscorreguts = 15 + (n * 37) % 130;
            int diesRetard = diesTranscorreguts - DIES_PRESTEC;
            if (diesRetard < 0) {
                diesRetard = 0;
            }

            double multa = diesRetard * TARIFA_DIARIA;
            if (multa > MULTA_MAXIMA) {
                multa = MULTA_MAXIMA;
            }

            if (diesRetard > 0) {
                llibresAmbRetard++;
            }
            totalMultes += multa;

            System.out.printf("  Llibre %d: %d dies de retard, %.2f EUR%n",
                              n, diesRetard, multa);

            if (n == devolucions) {
                System.out.printf("TOTAL: %.2f EUR (%d llibres amb retard)%n",
                                  totalMultes, llibresAmbRetard);
            }
        }
    }
}

Pas 1: reproduir. S'executa i sempre dona el mateix:

Processant devolucions de Marta Ruiz
  Llibre 1: 37 dies de retard, 9,25 EUR
  Llibre 2: 74 dies de retard, 18,50 EUR
  Llibre 3: 111 dies de retard, 20,00 EUR
TOTAL: 20,00 EUR (3 llibres amb retard)

Pas 2: definir el que s'espera. A mà: 9,25 + 18,50 + 20,00 = 47,75 €. El programa diu 20,00 €. Els imports individuals són correctes; el que falla és el total. I crida l'atenció que 20,00 € sigui exactament la multa de l'últim llibre.

Pas 3: aïllar. El total es calcula dins del bucle, així que el bucle és el sospitós. Col·loquem un punt d'interrupció a la línia totalMultes += multa; i arrenquem en mode depuració. Finestra de variables a cada aturada:

Aturada n multa totalMultes abans de la suma totalMultes esperat abans
1a 1 9.25 0.0 0.0 ✔
2a 2 18.5 0.0 9.25 ✘
3a 3 20.0 0.0 27.75 ✘

A la segona aturada, totalMultes val 0.0 quan hauria de valer 9.25. Aquest és el punt exacte on el real i l'esperat divergeixen.

Pas 4: hipòtesi. Si l'acumulat es perd entre una iteració i la següent, és que la variable s'està recreant. I efectivament: double totalMultes = 0.0; és dins del bucle (línia 22). A cada volta es declara una variable nova, inicialitzada a zero, i l'anterior desapareix en tancar la clau.

Pas 5: verificar. Es col·loca un punt d'interrupció a la mateixa línia 22, amb la condició n > 1. El programa s'hi atura a la segona iteració: la línia que s'executa a cada volta. Hipòtesi confirmada. (Sense punt d'interrupció condicional caldria aturar-se a cada iteració; amb ell, s'hi va directe.)

Pas 6: corregir. El canvi mínim és moure la declaració fora del bucle, al costat de l'altre acumulador:

int llibresAmbRetard = 0;
double totalMultes = 0.0;          // <-- moguda FORA del bucle

for (int n = 1; n <= devolucions; n++) {
    // ... ja no es declara aqui ...
}

Pas 7: comprovar.

Processant devolucions de Marta Ruiz
  Llibre 1: 37 dies de retard, 9,25 EUR
  Llibre 2: 74 dies de retard, 18,50 EUR
  Llibre 3: 111 dies de retard, 20,00 EUR
TOTAL: 47,75 EUR (3 llibres amb retard)

Coincideix amb el que s'ha calculat a mà. I ara es proven els límits: amb devolucions = 0 el bucle no hi entra i no s'imprimeix cap total, perquè l'if (n == devolucions) és dins del bucle. Això és un segon error, més subtil, que la primera prova no revelava. La correcció adequada és treure el resum fora del bucle, que és a més on ha de ser conceptualment:

for (int n = 1; n <= devolucions; n++) {
    // ... calcul i impressio de cada llibre ...
}

// Resum FORA del bucle: s'imprimeix sempre, fins i tot amb zero devolucions.
System.out.printf("TOTAL: %.2f EUR (%d llibres amb retard)%n",
                  totalMultes, llibresAmbRetard);

Aquest cas reuneix els dos errors de bucle més freqüents: l'acumulador declarat a dins i el resum calculat a dins. I demostra el pas 7: si t'haguessis quedat en "ja funciona", l'error del cas buit encara hi seria.

Variant per practicar: l'error per un. Canvia la condició del bucle a n <= devolucions + 1 i observa què passa. El programa processarà un llibre fantasma amb dades calculades fora de rang. Localitza la fallada mirant el valor de n a l'última aturada del punt d'interrupció; veuràs n = 4 quan esperaves n = 3. Aquest és l'error off-by-one que anunciava la lliçó 02-02, caçat en deu segons amb el depurador i en mitja hora rellegint el codi.

  1. Errors Habituals i Consells

1. Canviar codi a l'atzar. "Provaré amb < en lloc de <= a veure si surt." De vegades s'encerta i és el pitjor que pot passar, perquè no hauràs entès res i l'error tornarà. Primer hipòtesi, després verificació, després canvi.

2. Depurar sense saber quin resultat esperes. Si no has calculat a mà el correcte, no el sabràs reconèixer quan el vegis.

3. Traces sense context. System.out.println(multa); en tres llocs diferents produeix tres números i cap informació.

4. Deixar les traces al codi. Abans de donar-ho per acabat, busca System.out.println al teu fitxer i elimina el que sigui bastida.

5. Posar el punt d'interrupció després del punt de fallada. Si la variable ja està malament quan t'atures, col·loca'l abans. L'objectiu és capturar el moment en què s'espatlla.

6. Ignorar la traça de pila i tornar a executar. El missatge i el número de línia hi són, de franc. Llegeix-los.

7. Step Into en crides a la biblioteca estàndard. Acabes navegant pel codi font d'Integer sense motiu. Fes servir Step Over per a tot el que no hagis escrit tu.

8. Depurar el fitxer equivocat. Si has fet canvis i no recompiles (o l'IDE no ho fa automàticament), estaràs depurant la versió anterior. Si les línies i els valors no quadren, sospita d'això.

9. Consell: rubber duck debugging. Explica-li el problema en veu alta, línia a línia, a un ànec de goma, a una planta o a un company imaginari. Sona ridícul i funciona: la majoria dels errors es descobreixen en el moment de verbalitzar "i aquí sumo la multa al total, que es declara… ah". Obligar-te a explicar-ho trenca la lectura automàtica que esmentava l'apartat 1.

10. Consell: fes servir el control de versions per poder tornar enrere. Si fas git commit quan el programa funciona, sempre pots comparar (git diff) què has tocat des de llavors i tornar a l'estat bo (git checkout) si et perds. Moltíssims errors es localitzen simplement mirant què ha canviat des de l'última versió que funcionava. Git es tracta en profunditat al mòdul 12; amb aquestes tres ordres ja n'obtens gairebé tot el benefici.

11. Consell: redueix el cas. Si l'error es produeix amb 100 devolucions, intenta reproduir-lo amb 3. Un cas mínim és infinitament més fàcil de depurar, i el mateix procés de reduir-lo sol revelar-ne la causa.

12. Consell: descansa. Un error que fa dues hores que busques es troba en cinc minuts l'endemà. No és folklore: la fixació mental és real i el descans la trenca.

Exercicis

Exercici 1: instrumentar un bucle amb traces

Agafa aquest codi, que ha de comptar quantes devolucions d'una tanda superen el llindar de retard lleu, i afegeix-hi traces seguint les regles de l'apartat 4: etiqueta d'ubicació, nom i valor de cada variable rellevant, i traça d'entrada i sortida del bucle. No modifiquis encara la lògica.

final int DIES_PRESTEC = 15;
final int LLINDAR_LLEU = 7;
int greus = 0;

for (int n = 1; n <= 5; n++) {
    int diesTranscorreguts = 10 + (n * 23) % 60;
    int diesRetard = diesTranscorreguts - DIES_PRESTEC;
    if (diesRetard > LLINDAR_LLEU) {
        greus++;
    }
}
System.out.println("Retards greus: " + greus);

Amb les traces posades, executa i respon: el resultat és correcte? Hi ha algun cas en què diesRetard surti negatiu, i què implica això per al recompte?

Exercici 2: caçar l'error amb el depurador

El programa següent ha de calcular la multa mitjana d'una tanda de devolucions. Dona un resultat incorrecte. Localitza'l amb el depurador, seguint els set passos del mètode sistemàtic, i documenta per escrit: les dades de reproducció, el resultat esperat calculat a mà, en quina línia vas col·locar el punt d'interrupció, què vas veure a la finestra de variables, quina era la teva hipòtesi i quina va ser la correcció.

final double TARIFA_DIARIA = 0.25;
int devolucions = 4;
double sumaMultes = 0.0;
int comptades = 0;

for (int n = 1; n < devolucions; n++) {
    int diesRetard = n * 10;
    double multa = diesRetard * TARIFA_DIARIA;
    sumaMultes += multa;
    comptades++;
}

double mitjana = sumaMultes / comptades;
System.out.printf("Multa mitjana de %d devolucions: %.2f EUR%n", devolucions, mitjana);

Exercici 3: punt d'interrupció condicional

Escriu un programa que recorri 200 dies de retard simulats i acumuli la multa de cadascun amb sostre. Després:

  1. Col·loca un punt d'interrupció a la línia de l'acumulador.
  2. Configura'l amb la condició diesRetard > 30 i anota en quines iteracions s'atura.
  3. Canvia-la per multa >= MULTA_MAXIMA i anota el primer dia en què s'atura.
  4. Afegeix totalAcumulat i diesRetard com a expressions watch i descriu com evolucionen.
  5. Fes servir l'avaluació d'expressions per calcular, sense tocar el codi, quant valdria totalAcumulat / n a l'aturada.

Lliura el programa i una breu descripció del que has observat a cada pas.

Solucions

Solució 1

package com.nexussoftware.bibliotech;

public class BiblioTechApp {
    public static void main(String[] args) {

        final int DIES_PRESTEC = 15;
        final int LLINDAR_LLEU = 7;
        int greus = 0;

        // Traca d'ENTRADA al bucle
        System.out.printf("[bucle:IN ] greus=%d DIES_PRESTEC=%d LLINDAR_LLEU=%d%n",
                          greus, DIES_PRESTEC, LLINDAR_LLEU);

        for (int n = 1; n <= 5; n++) {

            int diesTranscorreguts = 10 + (n * 23) % 60;
            int diesRetard = diesTranscorreguts - DIES_PRESTEC;

            // Traca per iteracio: ubicacio + nom=valor de tot el rellevant
            System.out.printf("[iter] n=%d transcorreguts=%d retard=%d supera=%b%n",
                              n, diesTranscorreguts, diesRetard,
                              diesRetard > LLINDAR_LLEU);

            if (diesRetard > LLINDAR_LLEU) {
                greus++;
                System.out.printf("[iter] n=%d -> greus incrementat a %d%n", n, greus);
            }
        }

        // Traca de SORTIDA del bucle
        System.out.printf("[bucle:OUT] greus=%d%n", greus);

        System.out.println("Retards greus: " + greus);
    }
}

Sortida:

[bucle:IN ] greus=0 DIES_PRESTEC=15 LLINDAR_LLEU=7
[iter] n=1 transcorreguts=33 retard=18 supera=true
[iter] n=1 -> greus incrementat a 1
[iter] n=2 transcorreguts=56 retard=41 supera=true
[iter] n=2 -> greus incrementat a 2
[iter] n=3 transcorreguts=19 retard=4 supera=false
[iter] n=4 transcorreguts=42 retard=27 supera=true
[iter] n=4 -> greus incrementat a 3
[iter] n=5 transcorreguts=25 retard=10 supera=true
[iter] n=5 -> greus incrementat a 4
[bucle:OUT] greus=4
Retards greus: 4

Respostes. El recompte de 4 és correcte per a les dades generades: les iteracions 1, 2, 4 i 5 superen el llindar de 7 dies i la 3 no. Amb aquests valors concrets diesRetard no surt mai negatiu, perquè diesTranscorreguts mínim és 19 i sempre supera els 15 dies de préstec.

Però la traça revela un risc latent: la fórmula 10 + (n * 23) % 60 pot produir valors per sota de 15 (per exemple, amb n = 10 dona 10 + 230 % 60 = 10 + 50 = 60, però amb altres fórmules o rangs sí que passaria). El codi no satura el retard a zero, així que un diesTranscorreguts menor que 15 donaria un diesRetard negatiu. Per al recompte de greus tant se val (un negatiu no supera mai 7), però si aquest mateix diesRetard alimentés el càlcul de la multa, la biblioteca pagaria a l'empleat. La lliçó: una traça no només confirma el resultat, també exposa suposicions no escrites. La correcció és afegir-hi el saturat a zero, com a la resta de BiblioTech:

if (diesRetard < 0) {
    diesRetard = 0;
}

Solució 2

Pas 1: reproduir. Dades: devolucions = 4, retards simulats n * 10. El programa imprimeix sempre:

Multa mitjana de 4 devolucions: 5,00 EUR

Pas 2: el que s'espera, a mà. Amb 4 devolucions, els retards haurien de ser 10, 20, 30 i 40 dies, és a dir, multes de 2,50 + 5,00 + 7,50 + 10,00 = 25,00 €, i una mitjana de 6,25 €. El programa diu 5,00 €.

Pas 3: aïllar. Punt d'interrupció a sumaMultes += multa;.

Pas 4: observar. Finestra de variables a cada aturada:

Aturada n diesRetard multa sumaMultes després de sumar comptades
1a 1 10 2.5 2.5 1
2a 2 20 5.0 7.5 2
3a 3 30 7.5 15.0 3

I no hi ha quarta aturada: el bucle acaba. comptades val 3, no 4. La mitjana 15,0 / 3 = 5,00 € és aritmèticament correcta; el que està malament és quantes vegades s'ha iterat.

Pas 5: hipòtesi. La condició del bucle és n < devolucions amb n començant en 1. Això recorre 1, 2, 3: tres iteracions, no quatre. És un error per un clàssic, provocat per barrejar el conveni de començar en 1 amb la condició <, que està pensada per a índexs que comencen en 0.

Verificació: un watch sobre n confirma que l'últim valor amb què entra al cos és 3.

Pas 6: corregir. Començant en 1, la condició correcta és <=:

package com.nexussoftware.bibliotech;

public class BiblioTechApp {
    public static void main(String[] args) {

        final double TARIFA_DIARIA = 0.25;

        int devolucions = 4;
        double sumaMultes = 0.0;
        int comptades = 0;

        // CORREGIT: n comenca en 1, per tant la condicio ha de ser <=
        for (int n = 1; n <= devolucions; n++) {
            int diesRetard = n * 10;
            double multa = diesRetard * TARIFA_DIARIA;
            sumaMultes += multa;
            comptades++;
        }

        // Guarda contra la divisio per zero: si no hi ha devolucions, no hi ha mitjana.
        if (comptades == 0) {
            System.out.println("No hi ha devolucions per promitjar.");
        } else {
            double mitjana = sumaMultes / comptades;
            System.out.printf("Multa mitjana de %d devolucions: %.2f EUR%n",
                              comptades, mitjana);
        }
    }
}

Pas 7: comprovar.

Multa mitjana de 4 devolucions: 6,25 EUR

Coincideix amb el càlcul manual. I es proven els límits: amb devolucions = 0 el bucle no hi entra, comptades val 0 i la versió original hauria fet 0.0 / 0, que amb double no llança error però produeix NaN i una sortida absurda (Multa mitjana de 0 devolucions: NaN EUR). La guarda afegida ho cobreix. Observa també que el printf final ara fa servir comptades en lloc de devolucions: informar del que realment s'ha processat, i no del que es pretenia processar, és una defensa barata contra aquesta mateixa família d'errors.

Solució 3

package com.nexussoftware.bibliotech;

public class BiblioTechApp {
    public static void main(String[] args) {

        final double TARIFA_DIARIA = 0.25;
        final double MULTA_MAXIMA = 20.0;
        final int DIES_SIMULATS = 200;

        double totalAcumulat = 0.0;
        int diesAmbSostre = 0;

        for (int n = 1; n <= DIES_SIMULATS; n++) {

            int diesRetard = n;
            double multa = diesRetard * TARIFA_DIARIA;

            if (multa >= MULTA_MAXIMA) {
                multa = MULTA_MAXIMA;
                diesAmbSostre++;
            }

            totalAcumulat += multa;       // <-- BREAKPOINT AQUI
        }

        System.out.printf("Total acumulat: %.2f EUR%n", totalAcumulat);
        System.out.printf("Dies amb multa amb sostre: %d%n", diesAmbSostre);
    }
}

El que s'observa a cada pas:

  1. Punt d'interrupció sense condició. El programa s'atura 200 vegades. Inviable a mà: és justament l'escenari que motiva les condicions.

  2. Condició diesRetard > 30. S'atura per primera vegada amb n = 31 i continua aturant-se a 32, 33, 34… fins a 200. Continua sent molt, però ja t'has saltat les 30 primeres iteracions sense tocar el codi. Per acotar-ho més, diesRetard > 30 && diesRetard % 20 == 0 atura només a 40, 60, 80, 100…

  3. Condició multa >= MULTA_MAXIMA. La primera aturada és a n = 80, perquè 80 × 0,25 = 20,00 €, el primer dia que assoleix el sostre. Confirma sense càlculs la dada que ja coneixies de la lliçó anterior.

  4. Watches. Amb totalAcumulat i diesRetard vigilats es veu l'evolució a cada aturada: totalAcumulat creix de manera quadràtica mentre la multa puja (cada dia suma una mica més que l'anterior) i passa a créixer linealment, exactament 20,00 € per dia, tan bon punt se supera el dia 80. Aquest canvi de pendent és l'efecte del sostre, visible en directe.

  5. Avaluació d'expressions. Aturant-se a n = 100 i avaluant totalAcumulat / n s'obté la multa mitjana per dia fins a aquell punt: 1210,00 / 100 = 12,10 €, un valor que el programa no calcula enlloc. Aquest és el gran avantatge de l'avaluació d'expressions: fer preguntes noves a un programa sense modificar-lo ni recompilar-lo.

Resultat final del programa, per contrastar:

Total acumulat: 3210,00 EUR
Dies amb multa amb sostre: 121

Verificació manual, que és el pas 2 del mètode aplicat a aquest exercici. El programa no es creu, es comprova:

  • Dies 1 a 79: sense sostre. La suma 1 + 2 + … + 79 val 79 × 80 / 2 = 3160, i multiplicada per la tarifa de 0,25 € dona 790,00 €.
  • Dies 80 a 200: amb sostre a 20,00 €. Són 200 − 80 + 1 = 121 dies, és a dir, 121 × 20,00 = 2420,00 €.
  • Total: 790,00 + 2420,00 = 3210,00 €, i diesAmbSostre = 121. Tots dos coincideixen amb la sortida.

Si no haguessin coincidit, el procediment seria exactament el mateix que a la solució 2: punt d'interrupció condicional a n == 79, avaluar totalAcumulat i veure si val 790,00 €. Si hi val, el programa va bé fins allà i l'error és al càlcul manual; si no hi val, l'error és al codi i ja el tens acorralat entre la iteració 1 i la 79. Quan el paper i el programa discrepen, un dels dos menteix i cal esbrinar quin: no donis mai per bo el resultat del programa només perquè és el que tens al davant.

Conclusió

Ja no depens de rellegir el codi per entendre què fa. Saps distingir els errors de compilació, d'execució i lògics, i que només els tercers exigeixen depuració de debò. Saps escriure traces útils —amb etiqueta d'ubicació, nom i valor de cada variable, delimitant l'entrada i la sortida de cada bloc— i per què cal esborrar-les després, a l'espera del logging del mòdul 6. Saps manejar el depurador de l'IDE: punts d'interrupció, step over, step into, step out i resume, la finestra de variables, les expressions vigilades, l'avaluació d'expressions en calent i, sobretot, els punts d'interrupció condicionals, que converteixen 200 aturades en 3. Saps llegir una traça de pila i quedar-te amb la línia que importa: la primera que menciona el teu paquet. I tens un mètode de set passos —reproduir, definir el que s'espera, aïllar per bisecció, formular hipòtesis, verificar, corregir, comprovar— que substitueix la cerca a cegues per una investigació.

Al cas pràctic has caçat els dos errors de bucle més habituals de l'ofici: l'acumulador declarat dins del bucle i el resum calculat a dins, més la variant de l'error per un. Tot amb BiblioTechApp, que a hores d'ara decideix, repeteix, despatxa opcions amb switch, filtra amb continue, s'atura amb break i ara, a més, pot ser inspeccionat per dins.

Només falta ajuntar-ho tot. La lliçó següent, Projecte: menú interactiu de BiblioTech, és la lliçó integradora del mòdul: construiràs per iteracions l'aplicació completa —menú en bucle amb switch de fletxes, validació de totes les entrades, registre de devolucions, simulació de préstecs i un resum de la sessió amb estadístiques acumulades— i acabaràs enumerant amb precisió què continua sense poder fer i quin mòdul ho resoldrà.

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