Al cerrar la lección anterior, BiblioTechApp ya recorría una cola de devoluciones, filtraba con continue, paraba con break y mantenía media docena de acumuladores. Y con esa complejidad llega inevitablemente el momento en que el programa compila, se ejecuta sin quejarse y da un número que no es el correcto. Ahí termina lo que se puede resolver releyendo el código y empieza la depuración. Depurar no es un truco ni un talento innato: es un método, y es probablemente la habilidad que más separa a un programador que avanza de otro que se atasca. Para alguien que aprende de forma autodidacta es todavía más crítico, porque no hay nadie al lado a quien preguntar "¿por qué me da 0?". Esta lección te enseña a ver el estado interno de tu programa: primero con trazas, después con el depurador del IDE, y siempre siguiendo un procedimiento que convierte la búsqueda a ciegas en una investigación ordenada.

Contenido

  1. Por qué leer el código no basta
  2. Los tres tipos de error y cuál se depura
  3. Depuración con trazas: System.out.println estratégico
  4. Qué imprimir en una traza y en qué formato
  5. El patrón de traza de entrada y salida de un bloque
  6. Por qué las trazas deben desaparecer (y qué las sustituye)
  7. El depurador del IDE: conceptos y arranque
  8. Puntos de interrupción y comandos de paso
  9. Variables, expresiones y watch
  10. Puntos de interrupción condicionales
  11. Leer una traza de pila (stack trace)
  12. El método sistemático de depuración
  13. Caso práctico: el resumen de multas que sale mal
  14. Errores Comunes y Consejos
  15. Ejercicios

  1. Por qué leer el código no basta

Cuando un programa no hace lo que esperas, la tentación es releerlo. El problema es que relees lo que creías haber escrito, no lo que escribiste. Tu cerebro completa lo que falta, salta el <= que debía ser < y no ve la declaración que está dentro del bucle en lugar de fuera.

La depuración funciona al revés: en lugar de razonar sobre lo que el código debería hacer, se observa lo que hace. Se mira el valor real de cada variable en cada momento y se compara con el valor esperado. En cuanto encuentras el primer punto donde lo real y lo esperado divergen, has localizado el bug: está entre ese punto y el anterior que sí era correcto.

Esa diferencia entre deducir y observar es toda la lección.

  1. Los tres tipos de error y cuál se depura

Tipo Cuándo aparece Quién avisa Ejemplo Cómo se resuelve
De compilación Al compilar javac o el IDE, con línea y mensaje Falta un ;, tipos incompatibles Leer el mensaje (lección 01-03)
De ejecución Al ejecutar La JVM detiene el programa con una traza de pila NumberFormatException al convertir "veinte" Se manejan en el módulo 6; aquí solo se leen
Lógico Nunca "aparece" Nadie. El programa funciona y da un resultado incorrecto El total de multas sale 0,00 € Depuración

Los errores lógicos son los peligrosos: no rompen nada, simplemente mienten. Un recibo con la multa mal calculada se imprime igual de bien que uno correcto. Y son exactamente el objetivo de esta lección.

  1. Depuración con trazas: System.out.println estratégico

La técnica más antigua y todavía una de las más usadas: imprimir el estado del programa en puntos escogidos. Se le llama printf debugging o depuración con trazas.

Sus ventajas son reales: no necesita herramientas, funciona en cualquier entorno, deja un registro que se puede leer entero de una pasada y sirve incluso donde el depurador no llega (código concurrente, procesos remotos). Su desventaja también: obliga a modificar el código, recompilar y volver a ejecutar en cada intento.

Una traza mal puesta no sirve de nada:

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

Al ejecutar verás aqui, entra y un número suelto, sin saber cuál de las cinco variables es ni en qué iteración estás. Una traza útil dice dónde está, qué variable muestra y cuánto vale:

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

Salida:

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

De un vistazo compruebas la progresión y detectas el punto exacto donde el número deja de ser el que esperabas.

  1. Qué imprimir en una traza y en qué formato

Reglas prácticas, ordenadas por utilidad:

  1. Imprime siempre el nombre de la variable junto a su valor. multa=9.25 es información; 9.25 es ruido.
  2. Marca la ubicación con una etiqueta entre corchetes. [validacion], [bucle], [resumen]. Cuando tengas quince líneas de traza, esa etiqueta es lo que te dice de dónde salió cada una.
  3. Incluye la variable de control del bucle. Sin n=3 no sabes en qué iteración ocurre el problema.
  4. Usa printf cuando el formato importe. Los double con muchos decimales son ilegibles: System.out.printf("[bucle] n=%d multa=%.2f%n", n, multa);
  5. Usa System.err para lo excepcional. Se muestra en rojo en la mayoría de IDE y va por un canal distinto, así que puedes separar la traza normal de la anómala.
  6. Traza también los boolean y las condiciones. Es habitual descubrir que la condición que creías cierta es falsa:
boolean pagada = (n % 2 == 0);
System.out.println("[filtro] n=" + n + " pagada=" + pagada
                   + " -> " + (pagada ? "SE OMITE" : "SE PROCESA"));
  1. Delimita el bucle por fuera. Imprimir el estado justo antes de entrar y justo después de salir te dice si el problema está en el bucle o en lo que viene antes o después:
System.out.println("[antes] total=" + totalPendiente + " revisadas=" + revisadas);
for (...) { ... }
System.out.println("[despues] total=" + totalPendiente + " revisadas=" + revisadas);

  1. El patrón de traza de entrada y salida de un bloque

Cuando un bloque de código transforma unos datos de entrada en unos de salida, el patrón más rentable es imprimir ambos extremos:

// ENTRADA del bloque de calculo
System.out.printf("[calculo:IN ] diasTranscurridos=%d DIAS_PRESTAMO=%d%n",
                  diasTranscurridos, DIAS_PRESTAMO);

int diasRetraso = diasTranscurridos - DIAS_PRESTAMO;
if (diasRetraso < 0) {
    diasRetraso = 0;
}
double multa = diasRetraso * TARIFA_DIARIA;
if (multa > MULTA_MAXIMA) {
    multa = MULTA_MAXIMA;
}

// SALIDA del bloque de calculo
System.out.printf("[calculo:OUT] diasRetraso=%d multa=%.2f%n", diasRetraso, multa);

Con eso puedes aplicar bisección: si la entrada es correcta y la salida no, el bug está dentro de ese bloque. Si la entrada ya viene mal, el bug está antes y no tiene sentido mirar aquí. Cada par de trazas divide el programa en dos y descarta una mitad. Es el mismo principio de la búsqueda binaria, y es la forma más rápida de acorralar un error en un programa largo.

Cuando llegues al módulo 3 y escribas métodos, este patrón se convertirá en el estándar: una traza al entrar con los parámetros recibidos y otra al salir con el valor devuelto.

  1. Por qué las trazas deben desaparecer (y qué las sustituye)

Las trazas son andamios, no arquitectura. Dejarlas en el código tiene consecuencias reales:

  • Ensucian la salida. El usuario de BiblioTech no debe ver [bucle] n=3 multa=20.0 en medio de su recibo.
  • Cuestan rendimiento. La escritura por consola es lentísima comparada con cualquier cálculo; miles de println en un bucle pueden multiplicar por diez el tiempo de ejecución.
  • Filtran información. En un sistema real, imprimir datos de usuarios o credenciales en la consola es un problema de seguridad.
  • No se pueden apagar. Un System.out.println está o no está; no hay término medio.

Por eso, en cuanto encuentres el bug, borra la traza. Y para la información que sí quieres conservar de forma permanente, existe el logging: un sistema con niveles (DEBUG, INFO, WARN, ERROR) que se pueden activar o desactivar por configuración sin tocar el código, con marca de tiempo, origen y destino configurable (consola, fichero, servidor central). Eso se estudia en la lección 06-07 (Estrategias de Manejo de Errores y Logging). Por ahora, la regla es: traza para investigar, borrar al terminar.

Un truco de transición mientras tanto, aplicable ya con lo que sabes:

final boolean DEPURAR = true;     // ponlo a false para silenciar todas las trazas

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

Una sola constante controla todas las trazas. Es una versión rudimentaria de lo que hace un sistema de logging, y sirve perfectamente para un programa de consola de este tamaño.

  1. El depurador del IDE: conceptos y arranque

Un depurador (debugger) es una herramienta que ejecuta tu programa bajo control: puede pausarlo en cualquier línea, mostrarte el valor de todas las variables en ese instante y avanzar instrucción a instrucción. No modifica el código y no requiere recompilar para cambiar lo que observas. Es, con diferencia, la forma más eficiente de investigar un error lógico.

Todos los IDE de Java (IntelliJ IDEA, Eclipse, VS Code con el Extension Pack for Java, NetBeans) incluyen uno, y todos funcionan con los mismos conceptos y prácticamente los mismos atajos, porque debajo usan la misma tecnología de la JVM.

Para arrancar en modo depuración:

  • IntelliJ IDEA: botón con el icono de insecto junto al de ejecutar, o Shift + F9.
  • Eclipse: menú Run > Debug As > Java Application, o F11.
  • VS Code: pestaña Run and Debug, o F5.

La diferencia con la ejecución normal es que, si hay algún punto de interrupción activo, el programa se detendrá al llegar a él y te devolverá el control.

  1. Puntos de interrupción y comandos de paso

Un punto de interrupción (breakpoint) es una marca en una línea concreta que le dice al depurador "para aquí". Se coloca haciendo clic en el margen izquierdo del editor, a la altura del número de línea; aparece un círculo rojo. Volviendo a hacer clic se quita.

Dónde ponerlos, en orden de utilidad:

  • En la primera línea del cuerpo de un bucle sospechoso.
  • En la línea donde se asigna la variable que sale mal.
  • Justo antes y justo después del bloque que quieres examinar.
  • En la rama de un if que crees que no se ejecuta (si el programa se detiene ahí, tu creencia era falsa; información valiosísima).

Una vez detenido, se avanza con estos comandos:

Comando IntelliJ Eclipse Qué hace
Step Over F8 F6 Ejecuta la línea actual completa y para en la siguiente. Si hay una llamada a un método, la ejecuta entera sin entrar
Step Into F7 F5 Igual, pero entra dentro del método llamado para depurarlo línea a línea
Step Out Shift + F8 F7 Termina de ejecutar el método actual y vuelve a quien lo llamó
Resume F9 F8 Continúa la ejecución normal hasta el siguiente punto de interrupción (o hasta el final)
Run to Cursor Alt + F9 Ctrl + R Ejecuta hasta la línea donde está el cursor, sin poner un punto de interrupción
Stop Ctrl + F2 Mata el proceso

En este módulo todo tu código está en main, así que Step Over es el comando que usarás el 95 % del tiempo: pulsándolo repetidamente recorres el programa línea a línea. Step Into cobrará sentido en el módulo 3, cuando tengas métodos propios. Un consejo sobre Step Into: si lo pulsas en una línea con System.out.println, acabarás dentro del código fuente de la biblioteca estándar de Java, que no es lo que querías; usa Step Over para las llamadas que no son tuyas.

Flujo típico de una sesión de depuración:

flowchart TD
    A["Colocar breakpoint en la linea sospechosa"] --> B["Arrancar en modo depuracion"]
    B --> C["El programa se detiene en el breakpoint"]
    C --> D["Leer la ventana de Variables"]
    D --> E{"¿Los valores son los esperados?"}
    E -- "si" --> F["Step Over: avanzar una linea"]
    F --> D
    E -- "no" --> G["Bug localizado entre la ultima<br/>comprobacion correcta y esta"]
    G --> H["Corregir y volver a ejecutar"]

  1. Variables, expresiones y watch

Cuando el programa está detenido, el IDE muestra un panel de Variables con todo lo que existe en ese punto: los parámetros, las variables locales y sus valores actuales. Es la fotografía del estado de tu programa.

En una parada dentro del bucle de revisión de BiblioTech verías algo así:

Variable Valor
args String[0]
TARIFA_DIARIA 0.25
MULTA_MAXIMA 20.0
n 3
diasRetraso 111
pagada false
multa 27.75
totalPendiente 9.25
revisadas 1

Solo con leer esa tabla ya sabes en qué iteración estás y si los acumuladores llevan lo que deben.

Dos herramientas complementarias:

Evaluación de expresiones. Permite escribir una expresión Java cualquiera y ver su resultado en el contexto actual, sin modificar el código. En IntelliJ es Alt + F8 (Evaluate Expression); en Eclipse, la vista Expressions o Ctrl + Shift + I sobre una selección. Es utilísimo para comprobar una condición antes de que se evalúe:

diasRetraso * TARIFA_DIARIA          -> 27.75
multa >= MULTA_MAXIMA                -> true
diasRetraso <= UMBRAL_LEVE           -> false
n % 2 == 0                           -> false
totalPendiente + multa               -> 29.25

Preguntarle al depurador "¿cuánto vale esta condición ahora mismo?" resuelve muchos bugs en treinta segundos.

Watch (expresiones vigiladas). Una expresión añadida a la lista de watches se reevalúa automáticamente en cada parada. Sirve para vigilar un acumulador a lo largo de todas las iteraciones sin buscarlo en la lista de variables. Añade totalPendiente y revisadas como watch y verás su evolución vuelta a vuelta.

  1. Puntos de interrupción condicionales

Un bucle de 500 iteraciones con un punto de interrupción normal te obliga a pulsar Resume 500 veces. Un punto de interrupción condicional solo detiene el programa cuando se cumple una condición que tú escribes.

Se configura haciendo clic derecho sobre el círculo rojo del punto de interrupción; aparece un campo Condition donde se escribe una expresión booleana Java válida en ese punto:

diasRetraso > 30

Con esa condición, el programa solo se detendrá en las iteraciones en las que el retraso supere los 30 días. Otras condiciones habituales en BiblioTech:

Condición Para qué sirve
n == 7 Ir directamente a la iteración problemática
diasRetraso > 30 Detenerse solo en los casos graves
multa >= MULTA_MAXIMA Ver exactamente cuándo se aplica el tope
totalPendiente > 50.0 Descubrir en qué punto el acumulado se dispara
titulo.equals("Refactorizacion") Detenerse solo en un libro concreto
!pagada && diasRetraso == 0 Cazar una combinación rara que sospechas

Es la diferencia entre revisar 500 paradas y revisar 3. Junto con la evaluación de expresiones, es la funcionalidad del depurador que más tiempo ahorra.

  1. Leer una traza de pila (stack trace)

Cuando ocurre un error de ejecución, la JVM detiene el programa e imprime en System.err una traza de pila: el listado de llamadas que estaban activas en ese momento. Todavía no sabes manejar errores —eso es el módulo 6—, pero sí debes saber leerlos, porque te van a salir desde ya.

Exception in thread "main" java.lang.NumberFormatException: For input string: "veinte"
	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)

Se lee así, y el orden es importante:

  • Línea 1: el hilo (main), el tipo de error (java.lang.NumberFormatException) y el mensaje (For input string: "veinte"). Esta línea ya suele decirte qué ha pasado: se intentó convertir a número un texto que no lo era.
  • Líneas siguientes: la pila de llamadas, de más reciente a más antigua. La primera es donde estalló; la última es dónde empezó todo.
  • La línea que te importa es la primera que menciona TU código: com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:23). Ahí tienes clase, método y número de línea. Las de arriba están dentro de java.base, la biblioteca estándar, y casi nunca son el problema.

Regla práctica: lee la primera línea para saber qué pasó y busca la primera línea con tu paquete para saber dónde. En los IDE, esas líneas son enlaces: un clic te lleva al código.

Los tres errores de ejecución que más verás en este módulo:

Error Causa típica en BiblioTech
NumberFormatException Integer.parseInt("veinte") o cadena vacía
NullPointerException Llamar a un método sobre una variable String que vale null
ArithmeticException: / by zero División entera por cero (con double no ocurre: da Infinity)

  1. El método sistemático de depuración

Tener herramientas no basta; hace falta procedimiento. Este funciona siempre y evita las tres horas de cambiar cosas al azar:

1. Reproducir. Encuentra unos datos de entrada con los que el fallo ocurra siempre. Un bug que no sabes reproducir no lo puedes arreglar ni verificar. Anota exactamente qué entradas usas: en BiblioTech, por ejemplo, "Marta Ruiz / 3 libros / 27, 10, 120 días".

2. Definir lo esperado. Escribe el resultado correcto antes de mirar el código. "El total debería ser 23,00 €". Sin esa referencia no puedes saber cuándo has terminado. Calcúlalo a mano si hace falta.

3. Aislar por bisección. Divide el programa por la mitad con una traza o un punto de interrupción y comprueba si el estado en ese punto es correcto. Si lo es, el bug está después; si no, está antes. Repite sobre la mitad que sobrevive. Con diez pasos se acorrala un bug en mil líneas.

4. Formular una hipótesis concreta. No "algo falla en el bucle", sino "sospecho que totalMultas se reinicia en cada iteración porque está declarado dentro del for". Una hipótesis debe ser comprobable.

5. Verificarla. Pon un punto de interrupción o una traza que confirme o descarte esa hipótesis exacta. Si la descarta, vuelve al paso 4 con otra. Nunca cambies el código para "probar a ver" antes de haber verificado: es la forma más eficaz de introducir un segundo bug encima del primero.

6. Corregir. Un solo cambio, el mínimo, y que ataque la causa y no el síntoma. Si el total sale mal, la solución no es sumarle una constante al final.

7. Comprobar. Vuelve a ejecutar con los datos del paso 1 y verifica el resultado del paso 2. Y después prueba otros juegos de datos, especialmente los límites: cero libros, cero días, valores en el borde exacto de cada condición. Una corrección que arregla un caso y rompe otros dos es peor que el bug original.

  1. Caso práctico: el resumen de multas que sale mal

Vamos con un bug real, del tipo que te va a pasar la semana que viene. El bibliotecario de Nexus Software procesa una tanda de tres devoluciones y el total le sale mal.

El código (con el bug):

package com.nexussoftware.bibliotech;

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

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

        String empleado = "Marta Ruiz";
        int devoluciones = 3;

        int librosConRetraso = 0;

        System.out.println("Procesando devoluciones de " + empleado);

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

            double totalMultas = 0.0;                        // <-- linea 22

            int diasTranscurridos = 15 + (n * 37) % 130;
            int diasRetraso = diasTranscurridos - DIAS_PRESTAMO;
            if (diasRetraso < 0) {
                diasRetraso = 0;
            }

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

            if (diasRetraso > 0) {
                librosConRetraso++;
            }
            totalMultas += multa;

            System.out.printf("  Libro %d: %d dias de retraso, %.2f EUR%n",
                              n, diasRetraso, multa);

            if (n == devoluciones) {
                System.out.printf("TOTAL: %.2f EUR (%d libros con retraso)%n",
                                  totalMultas, librosConRetraso);
            }
        }
    }
}

Paso 1: reproducir. Se ejecuta y siempre da lo mismo:

Procesando devoluciones de Marta Ruiz
  Libro 1: 37 dias de retraso, 9,25 EUR
  Libro 2: 74 dias de retraso, 18,50 EUR
  Libro 3: 111 dias de retraso, 20,00 EUR
TOTAL: 20,00 EUR (3 libros con retraso)

Paso 2: definir lo esperado. A mano: 9,25 + 18,50 + 20,00 = 47,75 €. El programa dice 20,00 €. Los importes individuales son correctos; el que falla es el total. Y llama la atención que 20,00 € sea exactamente la multa del último libro.

Paso 3: aislar. El total se calcula dentro del bucle, así que el bucle es el sospechoso. Colocamos un punto de interrupción en la línea totalMultas += multa; y arrancamos en modo depuración. Ventana de variables en cada parada:

Parada n multa totalMultas antes de la suma totalMultas esperado antes
1.ª 1 9.25 0.0 0.0 ✔
2.ª 2 18.5 0.0 9.25 ✘
3.ª 3 20.0 0.0 27.75 ✘

En la segunda parada, totalMultas vale 0.0 cuando debería valer 9.25. Ese es el punto exacto donde lo real y lo esperado divergen.

Paso 4: hipótesis. Si el acumulado se pierde entre una iteración y la siguiente, es que la variable se está recreando. Y en efecto: double totalMultas = 0.0; está dentro del bucle (línea 22). En cada vuelta se declara una variable nueva, inicializada a cero, y la anterior desaparece al cerrar la llave.

Paso 5: verificar. Se coloca un punto de interrupción en la propia línea 22, con la condición n > 1. El programa se detiene ahí en la segunda iteración: la línea se ejecuta en cada vuelta. Hipótesis confirmada. (Sin punto de interrupción condicional habría que parar en cada iteración; con él, se va directo a la interesante.)

Paso 6: corregir. El cambio mínimo es mover la declaración fuera del bucle, junto al otro acumulador:

int librosConRetraso = 0;
double totalMultas = 0.0;          // <-- movida FUERA del bucle

for (int n = 1; n <= devoluciones; n++) {
    // ... ya no se declara aqui ...
}

Paso 7: comprobar.

Procesando devoluciones de Marta Ruiz
  Libro 1: 37 dias de retraso, 9,25 EUR
  Libro 2: 74 dias de retraso, 18,50 EUR
  Libro 3: 111 dias de retraso, 20,00 EUR
TOTAL: 47,75 EUR (3 libros con retraso)

Coincide con lo calculado a mano. Y ahora se prueban los límites: con devoluciones = 0 el bucle no entra y no se imprime ningún total, porque el if (n == devoluciones) está dentro del bucle. Eso es un segundo bug, más sutil, que la primera prueba no revelaba. La corrección adecuada es sacar el resumen fuera del bucle, que es además donde debe estar conceptualmente:

for (int n = 1; n <= devoluciones; n++) {
    // ... calculo e impresion de cada libro ...
}

// Resumen FUERA del bucle: se imprime siempre, incluso con cero devoluciones.
System.out.printf("TOTAL: %.2f EUR (%d libros con retraso)%n",
                  totalMultas, librosConRetraso);

Este caso reúne los dos errores de bucle más frecuentes: el acumulador declarado dentro y el resumen calculado dentro. Y demuestra el paso 7: si te hubieras quedado en "ya funciona", el bug del caso vacío seguiría ahí.

Variante para practicar: el error por uno. Cambia la condición del bucle a n <= devoluciones + 1 y observa qué ocurre. El programa procesará un libro fantasma con datos calculados fuera de rango. Localiza el fallo mirando el valor de n en la última parada del punto de interrupción; verás n = 4 cuando esperabas n = 3. Ese es el bug off-by-one que anunciaba la lección 02-02, cazado en diez segundos con el depurador y en media hora releyendo el código.

  1. Errores Comunes y Consejos

1. Cambiar código al azar. "Voy a probar con < en lugar de <= a ver si sale." A veces acierta y es lo peor que puede pasar, porque no habrás entendido nada y el bug volverá. Primero hipótesis, luego verificación, luego cambio.

2. Depurar sin saber qué resultado esperas. Si no has calculado a mano lo correcto, no sabrás reconocerlo cuando lo veas.

3. Trazas sin contexto. System.out.println(multa); en tres sitios distintos produce tres números y ninguna información.

4. Dejar las trazas en el código. Antes de dar por terminado, busca System.out.println en tu fichero y elimina lo que sea andamio.

5. Poner el punto de interrupción después del punto de fallo. Si la variable ya está mal cuando paras, colócalo antes. El objetivo es capturar el momento en que se estropea.

6. Ignorar la traza de pila y volver a ejecutar. El mensaje y el número de línea están ahí, gratis. Léelos.

7. Step Into en llamadas a la biblioteca estándar. Acabas navegando por el código fuente de Integer sin motivo. Usa Step Over para todo lo que no hayas escrito tú.

8. Depurar el fichero equivocado. Si has hecho cambios y no recompilas (o el IDE no lo hace automáticamente), estarás depurando la versión anterior. Si las líneas y los valores no cuadran, sospecha de esto.

9. Consejo: rubber duck debugging. Explícale el problema en voz alta, línea a línea, a un pato de goma, a una planta o a un compañero imaginario. Suena ridículo y funciona: la mayoría de los bugs se descubren en el momento de verbalizar "y aquí sumo la multa al total, que se declara… ah". Obligarte a explicar rompe la lectura automática que mencionaba el apartado 1.

10. Consejo: usa el control de versiones para poder volver atrás. Si haces git commit cuando el programa funciona, siempre puedes comparar (git diff) qué has tocado desde entonces y volver al estado bueno (git checkout) si te pierdes. Muchísimos bugs se localizan simplemente mirando qué cambió desde la última versión que funcionaba. Git se trata en profundidad en el módulo 12; con estos tres comandos ya obtienes casi todo el beneficio.

11. Consejo: reduce el caso. Si el bug ocurre con 100 devoluciones, intenta reproducirlo con 3. Un caso mínimo es infinitamente más fácil de depurar, y el propio proceso de reducirlo suele revelar la causa.

12. Consejo: descansa. Un bug que llevas dos horas buscando se encuentra en cinco minutos al día siguiente. No es folclore: la fijación mental es real y el descanso la rompe.

Ejercicios

Ejercicio 1: instrumentar un bucle con trazas

Toma este código, que debe contar cuántas devoluciones de una tanda superan el umbral de retraso leve, y añádele trazas siguiendo las reglas del apartado 4: etiqueta de ubicación, nombre y valor de cada variable relevante, y traza de entrada y salida del bucle. No modifiques la lógica todavía.

final int DIAS_PRESTAMO = 15;
final int UMBRAL_LEVE = 7;
int graves = 0;

for (int n = 1; n <= 5; n++) {
    int diasTranscurridos = 10 + (n * 23) % 60;
    int diasRetraso = diasTranscurridos - DIAS_PRESTAMO;
    if (diasRetraso > UMBRAL_LEVE) {
        graves++;
    }
}
System.out.println("Retrasos graves: " + graves);

Con las trazas puestas, ejecuta y responde: ¿el resultado es correcto? ¿Hay algún caso en el que diasRetraso salga negativo, y qué implica eso para el conteo?

Ejercicio 2: cazar el bug con el depurador

El siguiente programa debe calcular la multa media de una tanda de devoluciones. Da un resultado incorrecto. Localízalo con el depurador, siguiendo los siete pasos del método sistemático, y documenta por escrito: los datos de reproducción, el resultado esperado calculado a mano, en qué línea colocaste el punto de interrupción, qué viste en la ventana de variables, cuál era tu hipótesis y cuál fue la corrección.

final double TARIFA_DIARIA = 0.25;
int devoluciones = 4;
double sumaMultas = 0.0;
int contadas = 0;

for (int n = 1; n < devoluciones; n++) {
    int diasRetraso = n * 10;
    double multa = diasRetraso * TARIFA_DIARIA;
    sumaMultas += multa;
    contadas++;
}

double media = sumaMultas / contadas;
System.out.printf("Multa media de %d devoluciones: %.2f EUR%n", devoluciones, media);

Ejercicio 3: punto de interrupción condicional

Escribe un programa que recorra 200 días de retraso simulados y acumule la multa de cada uno con tope. Después:

  1. Coloca un punto de interrupción en la línea del acumulador.
  2. Configúralo con la condición diasRetraso > 30 y anota en qué iteraciones se detiene.
  3. Cámbiala por multa >= MULTA_MAXIMA y anota el primer día en que se detiene.
  4. Añade totalAcumulado y diasRetraso como expresiones watch y describe cómo evolucionan.
  5. Usa la evaluación de expresiones para calcular, sin tocar el código, cuánto valdría totalAcumulado / n en la parada.

Entrega el programa y una breve descripción de lo observado en cada paso.

Soluciones

Solución 1

package com.nexussoftware.bibliotech;

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

        final int DIAS_PRESTAMO = 15;
        final int UMBRAL_LEVE = 7;
        int graves = 0;

        // Traza de ENTRADA al bucle
        System.out.printf("[bucle:IN ] graves=%d DIAS_PRESTAMO=%d UMBRAL_LEVE=%d%n",
                          graves, DIAS_PRESTAMO, UMBRAL_LEVE);

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

            int diasTranscurridos = 10 + (n * 23) % 60;
            int diasRetraso = diasTranscurridos - DIAS_PRESTAMO;

            // Traza por iteracion: ubicacion + nombre=valor de todo lo relevante
            System.out.printf("[iter] n=%d transcurridos=%d retraso=%d supera=%b%n",
                              n, diasTranscurridos, diasRetraso,
                              diasRetraso > UMBRAL_LEVE);

            if (diasRetraso > UMBRAL_LEVE) {
                graves++;
                System.out.printf("[iter] n=%d -> graves incrementado a %d%n", n, graves);
            }
        }

        // Traza de SALIDA del bucle
        System.out.printf("[bucle:OUT] graves=%d%n", graves);

        System.out.println("Retrasos graves: " + graves);
    }
}

Salida:

[bucle:IN ] graves=0 DIAS_PRESTAMO=15 UMBRAL_LEVE=7
[iter] n=1 transcurridos=33 retraso=18 supera=true
[iter] n=1 -> graves incrementado a 1
[iter] n=2 transcurridos=56 retraso=41 supera=true
[iter] n=2 -> graves incrementado a 2
[iter] n=3 transcurridos=19 retraso=4 supera=false
[iter] n=4 transcurridos=42 retraso=27 supera=true
[iter] n=4 -> graves incrementado a 3
[iter] n=5 transcurridos=25 retraso=10 supera=true
[iter] n=5 -> graves incrementado a 4
[bucle:OUT] graves=4
Retrasos graves: 4

Respuestas. El conteo de 4 es correcto para los datos generados: las iteraciones 1, 2, 4 y 5 superan el umbral de 7 días y la 3 no. Con estos valores concretos diasRetraso nunca sale negativo, porque diasTranscurridos mínimo es 19 y siempre supera los 15 días de préstamo.

Pero la traza revela un riesgo latente: la fórmula 10 + (n * 23) % 60 puede producir valores por debajo de 15 (por ejemplo, con n = 10 da 10 + 230 % 60 = 10 + 50 = 60, pero con otras fórmulas o rangos sí ocurriría). El código no satura el retraso a cero, así que un diasTranscurridos menor que 15 daría un diasRetraso negativo. Para el conteo de graves da igual (un negativo nunca supera 7), pero si ese mismo diasRetraso alimentara el cálculo de la multa, la biblioteca pagaría al empleado. La lección: una traza no solo confirma el resultado, también expone suposiciones no escritas. La corrección es añadir el saturado a cero, como en el resto de BiblioTech:

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

Solución 2

Paso 1: reproducir. Datos: devoluciones = 4, retrasos simulados n * 10. El programa imprime siempre:

Multa media de 4 devoluciones: 5,00 EUR

Paso 2: lo esperado, a mano. Con 4 devoluciones, los retrasos deberían ser 10, 20, 30 y 40 días, es decir, multas de 2,50 + 5,00 + 7,50 + 10,00 = 25,00 €, y una media de 6,25 €. El programa dice 5,00 €.

Paso 3: aislar. Punto de interrupción en sumaMultas += multa;.

Paso 4: observar. Ventana de variables en cada parada:

Parada n diasRetraso multa sumaMultas tras sumar contadas
1.ª 1 10 2.5 2.5 1
2.ª 2 20 5.0 7.5 2
3.ª 3 30 7.5 15.0 3

Y no hay cuarta parada: el bucle termina. contadas vale 3, no 4. La media 15,0 / 3 = 5,00 € es aritméticamente correcta; lo que está mal es cuántas veces se ha iterado.

Paso 5: hipótesis. La condición del bucle es n < devoluciones con n empezando en 1. Eso recorre 1, 2, 3: tres iteraciones, no cuatro. Es un error por uno clásico, provocado por mezclar el convenio de empezar en 1 con la condición <, que está pensada para índices que empiezan en 0.

Verificación: un watch sobre n confirma que el último valor con el que entra al cuerpo es 3.

Paso 6: corregir. Empezando en 1, la condición correcta es <=:

package com.nexussoftware.bibliotech;

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

        final double TARIFA_DIARIA = 0.25;

        int devoluciones = 4;
        double sumaMultas = 0.0;
        int contadas = 0;

        // CORREGIDO: n empieza en 1, luego la condicion debe ser <=
        for (int n = 1; n <= devoluciones; n++) {
            int diasRetraso = n * 10;
            double multa = diasRetraso * TARIFA_DIARIA;
            sumaMultas += multa;
            contadas++;
        }

        // Guarda contra la division por cero: si no hay devoluciones, no hay media.
        if (contadas == 0) {
            System.out.println("No hay devoluciones que promediar.");
        } else {
            double media = sumaMultas / contadas;
            System.out.printf("Multa media de %d devoluciones: %.2f EUR%n",
                              contadas, media);
        }
    }
}

Paso 7: comprobar.

Multa media de 4 devoluciones: 6,25 EUR

Coincide con el cálculo manual. Y se prueban los límites: con devoluciones = 0 el bucle no entra, contadas vale 0 y la versión original habría hecho 0.0 / 0, que con double no lanza error pero produce NaN y una salida absurda (Multa media de 0 devoluciones: NaN EUR). La guarda añadida lo cubre. Observa también que el printf final ahora usa contadas en lugar de devoluciones: informar de lo que realmente se ha procesado, y no de lo que se pretendía procesar, es una defensa barata contra esta misma familia de errores.

Solución 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 DIAS_SIMULADOS = 200;

        double totalAcumulado = 0.0;
        int topadas = 0;

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

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

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

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

        System.out.printf("Total acumulado: %.2f EUR%n", totalAcumulado);
        System.out.printf("Dias con multa topada: %d%n", topadas);
    }
}

Lo que se observa en cada paso:

  1. Punto de interrupción sin condición. El programa se detiene 200 veces. Inviable a mano: es justo el escenario que motiva las condiciones.

  2. Condición diasRetraso > 30. Se detiene por primera vez con n = 31 y continúa parando en 32, 33, 34… hasta 200. Sigue siendo mucho, pero ya has saltado las 30 primeras iteraciones sin tocar el código. Para acotar más, diasRetraso > 30 && diasRetraso % 20 == 0 detiene solo en 40, 60, 80, 100…

  3. Condición multa >= MULTA_MAXIMA. La primera parada es en n = 80, porque 80 × 0,25 = 20,00 €, el primer día que alcanza el tope. Confirma sin cálculos el dato que ya conocías de la lección anterior.

  4. Watches. Con totalAcumulado y diasRetraso vigilados se ve la evolución en cada parada: totalAcumulado crece de forma cuadrática mientras la multa sube (cada día suma un poco más que el anterior) y pasa a crecer linealmente, exactamente 20,00 € por día, en cuanto se supera el día 80. Ese cambio de pendiente es el efecto del tope, visible en directo.

  5. Evaluación de expresiones. Deteniéndose en n = 100 y evaluando totalAcumulado / n se obtiene la multa media por día hasta ese punto: 1210,00 / 100 = 12,10 €, un valor que el programa no calcula en ningún sitio. Esa es la gran ventaja de la evaluación de expresiones: hacerle preguntas nuevas a un programa sin modificarlo ni recompilarlo.

Resultado final del programa, para contrastar:

Total acumulado: 3210,00 EUR
Dias con multa topada: 121

Verificación manual, que es el paso 2 del método aplicado a este ejercicio. El programa no se cree, se comprueba:

  • Días 1 a 79: no topados. La suma 1 + 2 + … + 79 vale 79 × 80 / 2 = 3160, y multiplicada por la tarifa de 0,25 € da 790,00 €.
  • Días 80 a 200: topados a 20,00 €. Son 200 − 80 + 1 = 121 días, es decir, 121 × 20,00 = 2420,00 €.
  • Total: 790,00 + 2420,00 = 3210,00 €, y topadas = 121. Ambos coinciden con la salida.

Si no hubieran coincidido, el procedimiento sería exactamente el mismo que en la solución 2: punto de interrupción condicional en n == 79, evaluar totalAcumulado y ver si vale 790,00 €. Si vale, el programa va bien hasta ahí y el error está en el cálculo manual; si no vale, el bug está en el código y ya lo tienes acorralado entre la iteración 1 y la 79. Cuando el papel y el programa discrepan, uno de los dos miente y hay que averiguar cuál: nunca des por bueno el resultado del programa solo porque es el que tienes delante.

Conclusión

Ya no dependes de releer el código para entender qué hace. Sabes distinguir los errores de compilación, de ejecución y lógicos, y que solo los terceros exigen depuración de verdad. Sabes escribir trazas útiles —con etiqueta de ubicación, nombre y valor de cada variable, delimitando la entrada y la salida de cada bloque— y por qué hay que borrarlas después, a la espera del logging del módulo 6. Sabes manejar el depurador del IDE: puntos de interrupción, step over, step into, step out y resume, la ventana de variables, las expresiones vigiladas, la evaluación de expresiones en caliente y, sobre todo, los puntos de interrupción condicionales, que convierten 200 paradas en 3. Sabes leer una traza de pila y quedarte con la línea que importa: la primera que menciona tu paquete. Y tienes un método de siete pasos —reproducir, definir lo esperado, aislar por bisección, formular hipótesis, verificar, corregir, comprobar— que sustituye la búsqueda a ciegas por una investigación.

En el caso práctico has cazado los dos bugs de bucle más habituales del oficio: el acumulador declarado dentro del bucle y el resumen calculado dentro de él, más la variante del error por uno. Todo con BiblioTechApp, que a estas alturas decide, repite, despacha opciones con switch, filtra con continue, para con break y ahora, además, puede ser inspeccionado por dentro.

Solo falta juntarlo todo. La siguiente lección, Proyecto: Menú Interactivo de BiblioTech, es la lección integradora del módulo: construirás por iteraciones la aplicación completa —menú en bucle con switch de flechas, validación de todas las entradas, registro de devoluciones, simulación de préstamos y un resumen de la sesión con estadísticas acumuladas— y terminarás enumerando con precisión qué sigue sin poder hacer y qué módulo lo resolverá.

Curso de Programación en Java

Módulo 1: Introducción a Java

Módulo 2: Flujo de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Programación Orientada a Objetos Avanzada

Módulo 5: Estructuras de Datos y Colecciones

Módulo 6: Manejo de Excepciones

Módulo 7: Entrada/Salida de Archivos

Módulo 8: Multihilo y Concurrencia

Módulo 9: Redes

Módulo 10: Temas Avanzados

Módulo 11: Frameworks y Librerías de Java

Módulo 12: Construcción de Aplicaciones del Mundo Real

© Copyright 2026. Todos los derechos reservados