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
- Per què llegir el codi no n'hi ha prou
- Els tres tipus d'error i quin es depura
- Depuració amb traces:
System.out.printlnestratègic - Què imprimir en una traça i en quin format
- El patró de traça d'entrada i sortida d'un bloc
- Per què les traces han de desaparèixer (i què les substitueix)
- El depurador de l'IDE: conceptes i arrencada
- Punts d'interrupció i ordres de pas
- Variables, expressions i
watch - Punts d'interrupció condicionals
- Llegir una traça de pila (
stack trace) - El mètode sistemàtic de depuració
- Cas pràctic: el resum de multes que surt malament
- Errors Habituals i Consells
- Exercicis
- 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çó.
- 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çó.
- Depuració amb traces:
System.out.println estratègic
System.out.println estratègicLa 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:
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.
- Què imprimir en una traça i en quin format
Regles pràctiques, ordenades per utilitat:
- Imprimeix sempre el nom de la variable al costat del seu valor.
multa=9.25és informació;9.25és soroll. - 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. - Inclou-hi la variable de control del bucle. Sense
n=3no saps en quina iteració passa el problema. - Fes servir
printfquan el format importi. Elsdoubleamb molts decimals són il·legibles:System.out.printf("[bucle] n=%d multa=%.2f%n", n, multa); - Fes servir
System.errper 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. - Traça també els
booleani 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"));- 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);
- 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.
- 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.0al mig del seu rebut. - Costen rendiment. L'escriptura per consola és lentíssima comparada amb qualsevol càlcul; milers de
printlnen 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.printlnhi é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.
- 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, oF11. - VS Code: pestanya
Run and Debug, oF5.
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.
- 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
ifque 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"]
- Variables, expressions i
watch
watchQuan 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.
- 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:
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.
- Llegir una traça de pila (
stack trace)
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 dejava.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) |
- 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.
- 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 sí 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.
- 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:
- Col·loca un punt d'interrupció a la línia de l'acumulador.
- Configura'l amb la condició
diesRetard > 30i anota en quines iteracions s'atura. - Canvia-la per
multa >= MULTA_MAXIMAi anota el primer dia en què s'atura. - Afegeix
totalAcumulatidiesRetardcom a expressions watch i descriu com evolucionen. - Fes servir l'avaluació d'expressions per calcular, sense tocar el codi, quant valdria
totalAcumulat / na 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:
Solució 2
Pas 1: reproduir. Dades: devolucions = 4, retards simulats n * 10. El programa imprimeix sempre:
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.
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:
-
Punt d'interrupció sense condició. El programa s'atura 200 vegades. Inviable a mà: és justament l'escenari que motiva les condicions.
-
Condició
diesRetard > 30. S'atura per primera vegada ambn = 31i 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 == 0atura només a 40, 60, 80, 100… -
Condició
multa >= MULTA_MAXIMA. La primera aturada és an = 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. -
Watches. Amb
totalAcumulatidiesRetardvigilats es veu l'evolució a cada aturada:totalAcumulatcreix 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. -
Avaluació d'expressions. Aturant-se a
n = 100i avaluanttotalAcumulat / ns'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:
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
- 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
