La lección anterior terminó con una frustración concreta: cuando BiblioTechApp detecta un dato inválido, se rinde y termina en lugar de volver a preguntar; y cuando termina de procesar una devolución, se acaba el programa, aunque Marta Ruiz traiga cuatro libros en la mochila. Las condicionales enseñaron a tu programa a decidir; los bucles le enseñan a repetir. Son, junto con los condicionales, la mitad del flujo de control, y probablemente la construcción que más veces escribirás en tu vida profesional. En esta lección verás las tres formas clásicas de bucle en Java, la anatomía interna de cada una, los patrones fundamentales que resuelven el 90 % de los casos reales (contar, acumular, buscar, validar, ofrecer un menú) y las trampas que convierten un bucle en un cuelgue o en un consumo desbocado de memoria.

Contenido

  1. Qué es un bucle y qué partes tiene
  2. El bucle while
  3. El bucle do-while
  4. El bucle for
  5. Tabla comparativa: cuándo usar cada uno
  6. El bucle for-each: existe, pero llega más tarde
  7. Bucles anidados: la tabla de multas de BiblioTech
  8. Patrones fundamentales: contador, acumulador y búsqueda con bandera
  9. Bucle de validación de entrada
  10. Bucle de menú (adelanto del proyecto)
  11. Bucles infinitos: intencionados y accidentales
  12. Coste de las iteraciones y la trampa de concatenar String
  13. Errores Comunes y Consejos
  14. Ejercicios

  1. Qué es un bucle y qué partes tiene

Un bucle (o loop) es una estructura que ejecuta repetidamente un bloque de código mientras se cumpla una condición. Cada pasada completa por el bloque se llama iteración.

Todo bucle, sea de la forma que sea, tiene cuatro elementos. Identificarlos siempre —incluso cuando el lenguaje no te obligue a escribirlos juntos— es la mejor defensa contra los bucles infinitos:

Elemento Función Si falta o está mal...
Inicialización Prepara las variables de control antes de empezar El bucle arranca con basura o no compila
Condición Se evalúa antes (o después) de cada iteración; decide si sigue Bucle infinito o bucle que no se ejecuta nunca
Cuerpo El trabajo que se repite El bucle no hace nada útil
Actualización Modifica la variable de control para acercarse al fin Bucle infinito garantizado

El esquema mental:

flowchart TD
    I["Inicializacion"] --> C{"Condicion"}
    C -- "true" --> B["Cuerpo"]
    B --> U["Actualizacion"]
    U --> C
    C -- "false" --> F["Continuar tras el bucle"]

  1. El bucle while

while es el bucle más elemental: repite mientras la condición sea verdadera. Comprueba la condición antes de cada iteración, así que puede ejecutarse cero veces.

while (condicion) {
    // cuerpo
}

Anatomía diseccionada con un contador de devoluciones procesadas:

int devolucionesProcesadas = 0;          // 1. INICIALIZACIÓN (fuera del bucle)

while (devolucionesProcesadas < 3) {     // 2. CONDICIÓN (se comprueba ANTES de entrar)
    System.out.println("Procesando devolucion numero " + (devolucionesProcesadas + 1));
                                          // 3. CUERPO
    devolucionesProcesadas++;             // 4. ACTUALIZACIÓN (dentro del cuerpo)
}

System.out.println("Total procesadas: " + devolucionesProcesadas);

Salida:

Procesando devolucion numero 1
Procesando devolucion numero 2
Procesando devolucion numero 3
Total procesadas: 3

Traza paso a paso, que es como debes leer siempre un bucle la primera vez:

Iteración devolucionesProcesadas al entrar < 3 Imprime Al salir
1 0 true "…numero 1" 1
2 1 true "…numero 2" 2
3 2 true "…numero 3" 3
3 false fin

En este bucle, la actualización está dentro del cuerpo y es tu responsabilidad no olvidarla. Ese es el gran riesgo de while: si borras el devolucionesProcesadas++, la condición nunca cambia y el programa se queda colgado imprimiendo la misma línea para siempre.

while brilla cuando no sabes cuántas iteraciones habrá, porque el número depende de algo externo (lo que escriba el usuario, si se ha encontrado algo, si queda trabajo pendiente):

final double MULTA_MAXIMA = 20.0;
final double TARIFA_DIARIA = 0.25;

double multaAcumulada = 0.0;
int dias = 0;

// ¿Cuantos dias de retraso hacen falta para llegar al tope?
while (multaAcumulada < MULTA_MAXIMA) {
    dias++;
    multaAcumulada += TARIFA_DIARIA;
}

System.out.printf("Con %d dias de retraso se alcanza el tope de %.2f EUR%n",
                  dias, multaAcumulada);
// Con 80 dias de retraso se alcanza el tope de 20,00 EUR

  1. El bucle do-while

do-while es idéntico a while salvo en un punto decisivo: comprueba la condición al final, por lo que el cuerpo se ejecuta al menos una vez, pase lo que pase.

do {
    // cuerpo: se ejecuta SIEMPRE al menos una vez
} while (condicion);      // ¡ojo al punto y coma final!

El punto y coma tras el while es obligatorio; es el único sitio de Java donde un while lo lleva.

flowchart TD
    I["Inicializacion"] --> B["Cuerpo (siempre al menos una vez)"]
    B --> U["Actualizacion"]
    U --> C{"Condicion"}
    C -- "true" --> B
    C -- "false" --> F["Continuar tras el bucle"]

La diferencia práctica se ve comparando ambos con una condición falsa desde el principio:

int diasRetraso = 0;

while (diasRetraso > 0) {
    System.out.println("WHILE: hay retraso");     // NO se imprime nunca
}

do {
    System.out.println("DO-WHILE: hay retraso");  // SÍ se imprime una vez
} while (diasRetraso > 0);

do-while es la herramienta natural para todo lo que hay que hacer al menos una vez y quizá repetir: pedir un dato al usuario, mostrar un menú, reintentar una operación. Es el bucle de "pregunta primero, decide después".

Scanner scanner = new Scanner(System.in);
String respuesta;

do {
    System.out.print("¿Registrar otra devolucion? (si/no): ");
    respuesta = scanner.nextLine().trim();
} while (!respuesta.equalsIgnoreCase("si") && !respuesta.equalsIgnoreCase("no"));

System.out.println("Has respondido: " + respuesta);

Fíjate en que respuesta se declara fuera del do. Si la declararas dentro del bloque, no existiría en la condición del while ni después del bucle. Es un error de compilación muy habitual al empezar.

  1. El bucle for

for agrupa los cuatro elementos del bucle en una sola línea, lo que hace imposible olvidar la actualización. Es el bucle de referencia cuando sabes cuántas veces hay que repetir.

for (inicializacion; condicion; actualizacion) {
    // cuerpo
}

Diseccionado término a término:

for (int i = 1; i <= 5; i++) {
    System.out.println("Iteracion " + i);
}
  • int i = 1inicialización. Se ejecuta una sola vez, antes de todo. La variable i solo existe dentro del bucle (fuera dará cannot find symbol), lo cual es una ventaja: no contamina el resto del método.
  • i <= 5condición. Se evalúa antes de cada iteración, igual que en while. Si es falsa de entrada, el cuerpo no se ejecuta nunca.
  • i++actualización. Se ejecuta después del cuerpo, antes de volver a comprobar la condición.
  • El cuerpo entre llaves.

El orden real de ejecución, que conviene tener grabado:

flowchart TD
    A["int i = 1 (una sola vez)"] --> B{"i <= 5"}
    B -- "true" --> C["Cuerpo"]
    C --> D["i++"]
    D --> B
    B -- "false" --> E["Salir del bucle"]

Aplicado a BiblioTech, para imprimir la escala de multas de la primera semana:

final double TARIFA_DIARIA = 0.25;

System.out.println("DIAS   MULTA");
for (int dia = 1; dia <= 7; dia++) {
    double multa = dia * TARIFA_DIARIA;
    System.out.printf("%4d  %6.2f EUR%n", dia, multa);
}
DIAS   MULTA
   1    0,25 EUR
   2    0,50 EUR
   3    0,75 EUR
   4    1,00 EUR
   5    1,25 EUR
   6    1,50 EUR
   7    1,75 EUR

Variantes que verás en código real:

// Cuenta atrás
for (int dias = 15; dias > 0; dias--) { ... }

// Saltos de dos en dos
for (int dia = 0; dia <= 30; dia += 5) { ... }

// Dos variables de control (separadas por coma)
for (int i = 0, j = 10; i < j; i++, j--) { ... }

// Todas las secciones son opcionales: esto es un bucle infinito válido
for (;;) { ... }

Un consejo de nomenclatura: i, j, k son aceptables para índices numéricos puros (es una convención universal heredada de las matemáticas), pero en cuanto la variable signifique algo, ponle nombre: dia, intento, numeroLibro. El código se lee muchas más veces de las que se escribe.

  1. Tabla comparativa: cuándo usar cada uno

Los tres bucles son intercambiables desde el punto de vista teórico: cualquier cosa que hagas con uno la puedes hacer con otro. La elección es una cuestión de comunicar la intención.

Criterio while do-while for
Condición evaluada Antes Después Antes
Iteraciones mínimas 0 1 0
¿Sabes cuántas veces? No No
Partes del bucle Dispersas Dispersas Juntas en la cabecera
Riesgo de olvidar la actualización Alto Alto Bajo
Uso típico "Mientras quede trabajo" Menús, validación de entrada Recorridos y repeticiones contadas
Ejemplo BiblioTech Acumular multa hasta el tope Pedir opción hasta que sea válida Tabla de multas del día 1 al 30

Regla práctica que puedes aplicar sin pensarlo mucho:

  1. ¿Repetir un número conocido de veces o recorrer índices? → for.
  2. ¿Repetir mientras se cumpla algo, quizá cero veces? → while.
  3. ¿Hacerlo al menos una vez y luego decidir? → do-while.

  1. El bucle for-each: existe, pero llega más tarde

Java tiene un cuarto bucle, el for mejorado o for-each, con esta sintaxis:

for (String titulo : catalogo) {
    System.out.println(titulo);
}

Se lee como "para cada titulo en catalogo". Es más corto y elimina toda la aritmética de índices, así que es el bucle preferido cuando aplica. Su limitación es que solo funciona sobre arrays y colecciones, y ninguna de las dos cosas existe todavía en tu caja de herramientas: los arrays llegan en la lección 05-01 y las colecciones en el resto del módulo 5.

Por eso aquí solo lo dejamos anotado. Cuando llegues al módulo 5 y tengas una lista real de libros, el for-each será tu bucle por defecto. Hasta entonces, while, do-while y for cubren todo lo que necesitas.

  1. Bucles anidados: la tabla de multas de BiblioTech

Un bucle anidado es un bucle dentro del cuerpo de otro. El bucle interno completa todas sus iteraciones por cada iteración del externo. Si el externo hace 4 vueltas y el interno 5, el cuerpo interno se ejecuta 4 × 5 = 20 veces.

El caso clásico es generar una tabla de dos dimensiones. Nexus Software quiere una matriz de referencia con la multa según los días de retraso (filas) y el número de libros devueltos a la vez (columnas):

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;

        System.out.println("TABLA DE MULTAS DE BIBLIOTECH (EUR)");
        System.out.println("Filas: dias de retraso | Columnas: libros devueltos");
        System.out.println();

        // Cabecera de columnas
        System.out.printf("%8s", "DIAS\\LIB");
        for (int libros = 1; libros <= 4; libros++) {
            System.out.printf("%9d", libros);
        }
        System.out.println();

        // Bucle EXTERNO: recorre los dias de retraso de 5 en 5
        for (int dias = 5; dias <= 25; dias += 5) {

            System.out.printf("%8d", dias);

            // Bucle INTERNO: por cada fila, recorre las 4 columnas
            for (int libros = 1; libros <= 4; libros++) {

                double multa = dias * TARIFA_DIARIA * libros;
                if (multa > MULTA_MAXIMA) {
                    multa = MULTA_MAXIMA;          // tope por devolucion
                }
                System.out.printf("%9.2f", multa);
            }

            System.out.println();     // salto de linea al terminar la fila
        }
    }
}

Salida:

TABLA DE MULTAS DE BIBLIOTECH (EUR)
Filas: dias de retraso | Columnas: libros devueltos

DIAS\LIB        1        2        3        4
       5     1,25     2,50     3,75     5,00
      10     2,50     5,00     7,50    10,00
      15     3,75     7,50    11,25    15,00
      20     5,00    10,00    15,00    20,00
      25     6,25    12,50    18,75    20,00

Tres detalles importantes:

  1. Cada bucle tiene su propia variable de control (dias y libros). Reutilizar la misma en ambos es un error garantizado.
  2. El System.out.println() del final del bucle externo está fuera del interno: se ejecuta una vez por fila, no una vez por celda. Si lo metieras dentro, tendrías una columna vertical en lugar de una tabla.
  3. El tope de MULTA_MAXIMA se aplica dentro del bucle interno, celda a celda, y se nota en la esquina inferior derecha, donde 25 días × 4 libros daría 25,00 € pero queda en 20,00 €.

Cuidado con la explosión combinatoria: dos bucles anidados de 1.000 iteraciones cada uno son un millón de ejecuciones del cuerpo. Tres niveles, mil millones. El anidamiento es legítimo, pero cada nivel multiplica el coste.

  1. Patrones fundamentales: contador, acumulador y búsqueda con bandera

Casi todo lo que harás con bucles es una combinación de tres patrones. Merece la pena aprenderlos con nombre, porque los reconocerás en todo el código profesional que leas.

8.1 Contador

Una variable entera que se incrementa cuando ocurre algo. Sirve para responder "¿cuántos?".

int devolucionesConRetraso = 0;

for (int dias = 10; dias <= 30; dias += 5) {
    if (dias > 15) {                       // DIAS_PRESTAMO
        devolucionesConRetraso++;          // el contador solo sube si se cumple la condicion
    }
}
System.out.println("Devoluciones fuera de plazo: " + devolucionesConRetraso);  // 3

8.2 Acumulador

Una variable que va sumando valores. Sirve para responder "¿cuánto en total?". Se inicializa siempre fuera del bucle y a cero (o a "" para texto, aunque eso tiene matices que veremos en el apartado 12).

final double TARIFA_DIARIA = 0.25;
double totalMultas = 0.0;

for (int dias = 5; dias <= 25; dias += 5) {
    double multa = dias * TARIFA_DIARIA;
    totalMultas += multa;                  // acumulador
    System.out.printf("  %2d dias -> %.2f EUR%n", dias, multa);
}
System.out.printf("TOTAL: %.2f EUR%n", totalMultas);   // 18,75 EUR

El error clásico del acumulador es inicializarlo dentro del bucle. Si escribes double totalMultas = 0.0; como primera línea del cuerpo, se reinicia en cada vuelta y al final vale lo mismo que el último elemento. Es un bug tan frecuente que la lección 02-05 lo usa como caso práctico de depuración.

8.3 Búsqueda con bandera booleana

Una variable boolean que empieza en false y se pone a true cuando se encuentra lo buscado. Sirve para responder "¿existe alguno que…?".

final int UMBRAL_LEVE = 7;
boolean hayRetrasoGrave = false;          // bandera, inicializada a false

for (int dias = 1; dias <= 6; dias++) {
    if (dias > UMBRAL_LEVE) {
        hayRetrasoGrave = true;           // se levanta la bandera
    }
}

if (hayRetrasoGrave) {
    System.out.println("Hay al menos una devolucion con retraso grave.");
} else {
    System.out.println("Ninguna devolucion supera el umbral leve.");
}

Una vez levantada, la bandera no se vuelve a bajar: eso es lo que la hace fiable. Y en cuanto se levanta, seguir iterando suele ser innecesario; la forma de cortar el bucle en ese punto es break, que se estudia en detalle en la lección 02-04.

8.4 Los tres patrones juntos: una tanda de devoluciones

Este es el ejemplo que resume el módulo. Marta Ruiz llega al mostrador con varios libros y el bibliotecario los procesa uno a uno:

package com.nexussoftware.bibliotech;

import java.util.Scanner;

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;
        final int UMBRAL_LEVE = 7;

        Scanner scanner = new Scanner(System.in);

        System.out.print("Empleado: ");
        String empleado = scanner.nextLine().trim();

        System.out.print("¿Cuantos libros devuelve? ");
        int numeroLibros = Integer.parseInt(scanner.nextLine().trim());

        // --- Variables de acumulacion de la tanda ---
        int librosConRetraso   = 0;      // contador
        double totalMultas     = 0.0;    // acumulador
        int mayorRetraso       = 0;      // maximo
        String libroMasRetrasado = "-";  // dato asociado al maximo
        boolean alcanzadoElTope = false; // bandera

        System.out.println();

        for (int libro = 1; libro <= numeroLibros; libro++) {

            System.out.printf("--- Libro %d de %d ---%n", libro, numeroLibros);

            System.out.print("  Titulo: ");
            String titulo = scanner.nextLine().trim();

            System.out.print("  Dias transcurridos: ");
            int diasTranscurridos = Integer.parseInt(scanner.nextLine().trim());

            int diasRetraso = diasTranscurridos - DIAS_PRESTAMO;
            if (diasRetraso < 0) {
                diasRetraso = 0;
            }

            double multa = diasRetraso * TARIFA_DIARIA;
            if (multa > MULTA_MAXIMA) {
                multa = MULTA_MAXIMA;
                alcanzadoElTope = true;                  // bandera
            }

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

            if (diasRetraso > mayorRetraso) {            // maximo + dato asociado
                mayorRetraso = diasRetraso;
                libroMasRetrasado = titulo;
            }

            String gravedad;
            if (diasRetraso == 0) {
                gravedad = "SIN RETRASO";
            } else if (diasRetraso <= UMBRAL_LEVE) {
                gravedad = "LEVE";
            } else {
                gravedad = "GRAVE";
            }

            System.out.printf("  -> %s | retraso %d dias | multa %.2f EUR%n%n",
                              gravedad, diasRetraso, multa);
        }

        System.out.println("=== RESUMEN DE LA TANDA ===");
        System.out.printf("%-24s %s%n",   "Empleado:", empleado);
        System.out.printf("%-24s %d%n",   "Libros devueltos:", numeroLibros);
        System.out.printf("%-24s %d%n",   "Con retraso:", librosConRetraso);
        System.out.printf("%-24s %.2f EUR%n", "Total a pagar:", totalMultas);
        System.out.printf("%-24s %s (%d dias)%n", "Mayor retraso:",
                          libroMasRetrasado, mayorRetraso);

        if (alcanzadoElTope) {
            System.out.println("AVISO: algun libro alcanzo la multa maxima.");
        }

        scanner.close();
    }
}

Ejemplo de sesión con Marta Ruiz y tres libros:

Empleado: Marta Ruiz
¿Cuantos libros devuelve? 3

--- Libro 1 de 3 ---
  Titulo: Java Efectivo
  Dias transcurridos: 27
  -> GRAVE | retraso 12 dias | multa 3,00 EUR

--- Libro 2 de 3 ---
  Titulo: Patrones de Diseno
  Dias transcurridos: 12
  -> SIN RETRASO | retraso 0 dias | multa 0,00 EUR

--- Libro 3 de 3 ---
  Titulo: Refactorizacion
  Dias transcurridos: 120
  -> GRAVE | retraso 105 dias | multa 20,00 EUR

=== RESUMEN DE LA TANDA ===
Empleado:                Marta Ruiz
Libros devueltos:        3
Con retraso:             2
Total a pagar:           23,00 EUR
Mayor retraso:           Refactorizacion (105 dias)
AVISO: algun libro alcanzo la multa maxima.

Compara esto con el final del módulo 1, donde procesar dos libros exigía duplicar treinta líneas y treinta variables numeradas. Ahora procesa tres, diez o mil libros con el mismo código.

Y aquí aparece la limitación que debes notar conscientemente. Al terminar el bucle, ¿qué sabes de los libros procesados? Solo lo que has ido acumulando en variables escalares: cuántos, cuánto suman, cuál fue el peor. Los datos individuales se han perdido: el título del libro 1 se sobrescribió con el del libro 2. No puedes imprimir el detalle al final, ni ordenar los libros por multa, ni buscar uno concreto. Para eso harían falta variables que guarden muchos valores a la vez, y eso son los arrays (lección 05-01) y las colecciones (resto del módulo 5). Guarda esa sensación: es exactamente la que justifica el módulo 5.

  1. Bucle de validación de entrada

Este es el patrón que resuelve la frustración con la que abría la lección. En lugar de terminar el programa cuando el dato es inválido, se vuelve a pedir.

La estructura canónica es un do-while, porque siempre hay que preguntar al menos una vez:

Scanner scanner = new Scanner(System.in);

int diasTranscurridos;
boolean entradaValida;

do {
    System.out.print("Dias transcurridos (0-3650): ");
    String entrada = scanner.nextLine().trim();

    diasTranscurridos = Integer.parseInt(entrada);
    entradaValida = diasTranscurridos >= 0 && diasTranscurridos <= 3650;

    if (!entradaValida) {
        System.out.println("  Valor fuera de rango. Intentalo de nuevo.");
    }

} while (!entradaValida);

System.out.println("Aceptado: " + diasTranscurridos + " dias.");

Sesión de ejemplo:

Dias transcurridos (0-3650): -5
  Valor fuera de rango. Intentalo de nuevo.
Dias transcurridos (0-3650): 99999
  Valor fuera de rango. Intentalo de nuevo.
Dias transcurridos (0-3650): 27
Aceptado: 27 dias.

El mismo patrón vale para texto:

String empleado;
do {
    System.out.print("Empleado: ");
    empleado = scanner.nextLine().trim();
    if (empleado.isBlank()) {
        System.out.println("  El nombre no puede estar vacio.");
    }
} while (empleado.isBlank());

Y una variante prudente para producción: limitar el número de intentos, para que el programa no quede a merced de un usuario que se equivoca eternamente (o de un script automático mal hecho):

final int MAX_INTENTOS = 3;
int intentos = 0;
boolean valido = false;
int dias = 0;

while (!valido && intentos < MAX_INTENTOS) {
    intentos++;
    System.out.printf("Dias transcurridos (intento %d de %d): ", intentos, MAX_INTENTOS);
    dias = Integer.parseInt(scanner.nextLine().trim());
    valido = dias >= 0;
    if (!valido) {
        System.out.println("  Los dias no pueden ser negativos.");
    }
}

if (!valido) {
    System.err.println("Demasiados intentos fallidos. Operacion cancelada.");
} else {
    System.out.println("Aceptado: " + dias);
}

Sigue en pie la advertencia de la lección anterior: si el usuario escribe veinte, Integer.parseInt detiene el programa y ningún bucle lo salva. Ese agujero se cierra en el módulo 6.

  1. Bucle de menú (adelanto del proyecto)

El patrón que dará forma al proyecto final del módulo (lección 02-06) es un bucle que muestra opciones, lee la elección y actúa, hasta que el usuario decide salir.

Scanner scanner = new Scanner(System.in);
boolean salir = false;

while (!salir) {
    System.out.println();
    System.out.println("=== BIBLIOTECH - Nexus Software ===");
    System.out.println("  1. Registrar devolucion");
    System.out.println("  2. Simular prestamo");
    System.out.println("  0. Salir");
    System.out.print("Opcion: ");

    String opcion = scanner.nextLine().trim();

    if (opcion.equals("1")) {
        System.out.println("[Registrando devolucion...]");
    } else if (opcion.equals("2")) {
        System.out.println("[Simulando prestamo...]");
    } else if (opcion.equals("0")) {
        System.out.println("Hasta pronto.");
        salir = true;                       // la bandera corta el bucle
    } else {
        System.out.println("Opcion no reconocida.");
    }
}

scanner.close();

Aquí el bucle no cuenta iteraciones: se repite mientras el usuario no diga que se va. La bandera salir controla la condición. Es la aplicación directa del patrón de bandera del apartado 8.3.

En la lección 02-03 sustituirás esa escalera de if-else por un switch, mucho más legible para despachar opciones, y en la 02-06 montarás el programa completo.

  1. Bucles infinitos: intencionados y accidentales

Un bucle infinito es aquel cuya condición nunca se vuelve falsa. Existen dos categorías radicalmente distintas.

Intencionados. Se escriben a propósito y se abandonan desde dentro con break o return. Son idiomáticos en menús, servidores y bucles de eventos:

while (true) {
    String opcion = scanner.nextLine().trim();
    if (opcion.equals("0")) {
        break;                 // única salida, explícita
    }
    System.out.println("Has elegido: " + opcion);
}

while (true) y for (;;) son equivalentes; el primero se lee mejor. La condición para que un bucle infinito sea legítimo es que la salida sea evidente al leer el cuerpo.

Accidentales. Son bugs. Las tres causas, por frecuencia:

// 1. Falta la actualización
int i = 0;
while (i < 10) {
    System.out.println(i);
    // ¡falta i++! -> imprime 0 para siempre
}

// 2. La actualización aleja de la condición
for (int dias = 15; dias > 0; dias++) {   // deberia ser dias--
    System.out.println(dias);
}

// 3. La condición depende de algo que el cuerpo no cambia
boolean seguir = true;
while (seguir) {
    System.out.println("Procesando...");
    // nadie pone 'seguir' a false
}

Cómo salir de un bucle infinito colgado en la terminal: pulsa Ctrl + C. Eso envía una señal de interrupción al proceso Java y lo termina. Si lo has lanzado desde el IDE, hay un botón cuadrado rojo de "Stop" en la consola. Y si el bucle está imprimiendo millones de líneas y la terminal no responde, cierra la ventana; no habrás perdido nada, porque el programa no guarda estado todavía.

Cómo evitarlos, en orden de utilidad:

  1. Al escribir un while, escribe primero la actualización y luego el resto del cuerpo.
  2. Antes de ejecutar, pregúntate: ¿qué línea concreta hace que esta condición acabe siendo falsa? Si no sabes señalarla, tienes un bucle infinito.
  3. Durante el desarrollo, añade un límite de seguridad: if (iteraciones++ > 1_000_000) break;.

  1. Coste de las iteraciones y la trampa de concatenar String

Cada iteración cuesta tiempo. Un cuerpo que tarda 1 milisegundo, repetido un millón de veces, son 16 minutos. Dos consecuencias prácticas:

Saca del bucle todo lo que no cambia. Este cálculo se repite inútilmente:

// MAL: la multiplicación de las constantes se recalcula en cada vuelta
for (int dia = 1; dia <= 10_000; dia++) {
    double limite = TARIFA_DIARIA * 30 * 12;    // siempre el mismo valor
    ...
}

// BIEN: se calcula una vez
double limite = TARIFA_DIARIA * 30 * 12;
for (int dia = 1; dia <= 10_000; dia++) {
    ...
}

No concatenes String con + dentro de bucles largos. Recuerda del módulo 1 que String es inmutable: cada += no modifica el texto, sino que crea un objeto nuevo copiando todo el contenido anterior. En un bucle de n iteraciones eso significa n objetos y un coste que crece con el cuadrado de n.

// MAL: 10.000 objetos String intermedios, cada uno más largo que el anterior
String informe = "";
for (int dia = 1; dia <= 10_000; dia++) {
    informe += "Dia " + dia + "\n";
}

// BIEN: StringBuilder modifica un único buffer interno
StringBuilder informe = new StringBuilder();
for (int dia = 1; dia <= 10_000; dia++) {
    informe.append("Dia ").append(dia).append("\n");
}
System.out.println(informe.toString());

StringBuilder es una clase de la biblioteca estándar pensada exactamente para esto: append añade al final sin copiar todo, y toString() produce el String final una sola vez. Para bucles de tres o cuatro vueltas la diferencia es irrelevante y la concatenación normal es más legible; a partir de unos cientos de iteraciones, StringBuilder es obligatorio. En un caso real medido, construir 100.000 líneas pasa de decenas de segundos con += a unos pocos milisegundos con StringBuilder.

Una alternativa cuando lo único que quieres es mostrar el texto: imprimir dentro del bucle en lugar de acumularlo. System.out.println dentro de un bucle no tiene ese problema (aunque tampoco es gratis: la salida por consola es de las operaciones más lentas que existen).

Errores Comunes y Consejos

1. Olvidar la actualización. La causa número uno de bucles infinitos. En for es casi imposible; en while es el riesgo permanente.

2. Punto y coma después de la cabecera.

for (int i = 0; i < 5; i++);        // <-- bucle vacío
{
    System.out.println(i);          // ni siquiera compila: 'i' ya no existe
}

3. Error por uno (off-by-one). El bug más frecuente de la programación. < y <= no son intercambiables:

for (int i = 1; i < 5; i++)   // 1, 2, 3, 4  -> CUATRO vueltas
for (int i = 1; i <= 5; i++)  // 1, 2, 3, 4, 5 -> CINCO vueltas
for (int i = 0; i < 5; i++)   // 0, 1, 2, 3, 4 -> CINCO vueltas

Cuando dudes, escribe la traza de las tres primeras y la última iteración en un papel.

4. Inicializar el acumulador dentro del bucle. El resultado final acaba siendo el del último elemento en lugar de la suma. Inicializa siempre antes del for/while.

5. Declarar dentro del do una variable que necesitas en la condición. No compila: el ámbito del bloque termina en la llave de cierre.

6. Modificar la variable de control dentro del cuerpo de un for. Compila, pero convierte el bucle en un rompecabezas:

for (int i = 0; i < 10; i++) {
    if (algo) { i += 3; }     // legal, pero nadie lo espera al leer la cabecera
}

Si necesitas saltar iteraciones, usa continue (lección 02-04) o replantea el bucle.

7. Comparar double en la condición del bucle. for (double d = 0.0; d != 1.0; d += 0.1) no termina nunca, porque 0.1 no es representable exactamente en binario y la suma nunca vale 1.0 exacto. Usa < en lugar de !=, o cuenta con enteros y divide.

8. Consejo: nombra las variables de control por lo que significan. for (int intento = 1; intento <= MAX_INTENTOS; intento++) se entiende sin comentarios; for (int i = 1; i <= n; i++) obliga a mirar el cuerpo.

9. Consejo: mantén los cuerpos cortos. Si el cuerpo de tu bucle ocupa cincuenta líneas, cuesta ver dónde está la actualización y qué se acumula. Cuando aprendas métodos (lección 03-03), extraer el cuerpo será la solución natural.

Ejercicios

Ejercicio 1: escala de multas con for

Escribe un programa que imprima la escala oficial de multas de BiblioTech desde 1 hasta 30 días de retraso, de cinco en cinco, con tres columnas: días, multa sin tope y multa aplicada (con MULTA_MAXIMA). Añade una columna final con la etiqueta de gravedad (LEVE si los días son <= UMBRAL_LEVE, GRAVE en caso contrario). Al terminar, imprime cuántas de las filas mostradas superaban el tope, usando un contador.

Usa printf para alinear la tabla y las constantes TARIFA_DIARIA = 0.25, MULTA_MAXIMA = 20.0 y UMBRAL_LEVE = 7.

Ejercicio 2: validación robusta con do-while

Escribe un programa que pida al usuario el nombre de un empleado y sus días transcurridos, repitiendo cada pregunta hasta obtener un valor válido:

  • El nombre no puede estar en blanco ni tener menos de 3 caracteres.
  • Los días transcurridos deben estar entre 0 y 3650.

Cuando ambos datos sean válidos, calcula el retraso saturado a cero y la multa con tope, e imprime el recibo. Muestra en la salida cuántos intentos fallidos hubo en total (un contador que se incremente en cada rechazo).

Ejercicio 3: acumulación de una tanda con bucles anidados

Nexus Software quiere un informe de la sesión de un bibliotecario. El programa debe:

  1. Preguntar cuántos empleados ha atendido.
  2. Por cada empleado, preguntar su nombre y cuántos libros devuelve.
  3. Por cada libro, preguntar los días transcurridos y calcular su multa con tope.
  4. Mostrar, al terminar cada empleado, su subtotal de multas y cuántos libros traía con retraso.
  5. Mostrar, al final de todo, el total general de multas, el número total de libros procesados y el nombre del empleado con mayor multa acumulada.

Usa un bucle externo para los empleados y uno interno para sus libros. Después de resolverlo, responde por escrito: ¿qué te habría hecho falta para poder imprimir al final el detalle libro a libro de toda la sesión?

Soluciones

Solución 1

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 UMBRAL_LEVE = 7;

        int filasTopadas = 0;      // contador

        System.out.println("ESCALA OFICIAL DE MULTAS - BiblioTech");
        System.out.printf("%6s %12s %12s  %s%n", "DIAS", "SIN TOPE", "APLICADA", "GRAVEDAD");
        System.out.println("-".repeat(48));

        // Inicialización: dia = 1. Condición: <= 30. Actualización: +5.
        // Recorre 1, 6, 11, 16, 21, 26 -> seis filas.
        for (int dias = 1; dias <= 30; dias += 5) {

            double multaSinTope = dias * TARIFA_DIARIA;
            double multaAplicada = multaSinTope;

            if (multaAplicada > MULTA_MAXIMA) {
                multaAplicada = MULTA_MAXIMA;
                filasTopadas++;                 // el contador solo sube al topar
            }

            String gravedad;
            if (dias <= UMBRAL_LEVE) {
                gravedad = "LEVE";
            } else {
                gravedad = "GRAVE";
            }

            System.out.printf("%6d %12.2f %12.2f  %s%n",
                              dias, multaSinTope, multaAplicada, gravedad);
        }

        System.out.println("-".repeat(48));
        System.out.println("Filas que alcanzaron el tope: " + filasTopadas);
    }
}

Salida:

ESCALA OFICIAL DE MULTAS - BiblioTech
  DIAS     SIN TOPE     APLICADA  GRAVEDAD
------------------------------------------------
     1         0,25         0,25  LEVE
     6         1,50         1,50  LEVE
    11         2,75         2,75  GRAVE
    16         4,00         4,00  GRAVE
    21         5,25         5,25  GRAVE
    26         6,50         6,50  GRAVE
------------------------------------------------
Filas que alcanzaron el tope: 0

Observación interesante: con la tarifa actual, ninguna fila hasta 30 días llega al tope de 20 €, porque harían falta 80 días de retraso. El contador vale 0 y eso es correcto, no un bug. Comprueba que el programa se comporta como esperas cambiando el límite del bucle a dias <= 120.

Solución 2

package com.nexussoftware.bibliotech;

import java.util.Scanner;

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;

        Scanner scanner = new Scanner(System.in);
        int intentosFallidos = 0;

        // --- Bucle de validación del nombre ---
        String empleado;
        do {
            System.out.print("Empleado: ");
            empleado = scanner.nextLine().trim();

            if (empleado.length() < 3) {
                System.out.println("  El nombre debe tener al menos 3 caracteres.");
                intentosFallidos++;
            }
        } while (empleado.length() < 3);

        // --- Bucle de validación de los días ---
        int diasTranscurridos;
        boolean diasValidos;
        do {
            System.out.print("Dias transcurridos (0-3650): ");
            diasTranscurridos = Integer.parseInt(scanner.nextLine().trim());

            diasValidos = diasTranscurridos >= 0 && diasTranscurridos <= 3650;
            if (!diasValidos) {
                System.out.println("  Valor fuera del rango permitido.");
                intentosFallidos++;
            }
        } while (!diasValidos);

        // --- Cálculo, ya con datos garantizados válidos ---
        int diasRetraso = diasTranscurridos - DIAS_PRESTAMO;
        if (diasRetraso < 0) {
            diasRetraso = 0;
        }

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

        System.out.println();
        System.out.println("=== RECIBO DE DEVOLUCION ===");
        System.out.printf("%-20s %s%n",       "Empleado:", empleado);
        System.out.printf("%-20s %d%n",       "Dias transcurridos:", diasTranscurridos);
        System.out.printf("%-20s %d%n",       "Dias de retraso:", diasRetraso);
        System.out.printf("%-20s %.2f EUR%n", "Multa:", multa);
        System.out.printf("%-20s %d%n",       "Intentos fallidos:", intentosFallidos);

        scanner.close();
    }
}

Sesión de ejemplo:

Empleado:
  El nombre debe tener al menos 3 caracteres.
Empleado: JD
  El nombre debe tener al menos 3 caracteres.
Empleado: Diego Alonso
Dias transcurridos (0-3650): -3
  Valor fuera del rango permitido.
Dias transcurridos (0-3650): 27

=== RECIBO DE DEVOLUCION ===
Empleado:            Diego Alonso
Dias transcurridos:  27
Dias de retraso:     12
Multa:               3,00 EUR
Intentos fallidos:   3

Dos decisiones de diseño que conviene razonar:

  • empleado y diasTranscurridos se declaran fuera del do, porque se usan en la condición del while y después del bucle.
  • En el segundo bucle se usa la bandera diasValidos en lugar de repetir la condición compleja en el while. Repetirla funcionaría, pero duplicar una condición es duplicar un sitio donde equivocarse al cambiarla.

Solución 3

package com.nexussoftware.bibliotech;

import java.util.Scanner;

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;

        Scanner scanner = new Scanner(System.in);

        System.out.print("¿Cuantos empleados has atendido? ");
        int numeroEmpleados = Integer.parseInt(scanner.nextLine().trim());

        // Acumuladores globales de la sesión
        double totalGeneral = 0.0;
        int totalLibros = 0;
        double mayorMultaEmpleado = 0.0;
        String empleadoConMasMulta = "-";

        // ===== BUCLE EXTERNO: empleados =====
        for (int e = 1; e <= numeroEmpleados; e++) {

            System.out.printf("%n===== Empleado %d de %d =====%n", e, numeroEmpleados);
            System.out.print("Nombre: ");
            String empleado = scanner.nextLine().trim();

            System.out.print("Libros que devuelve: ");
            int numeroLibros = Integer.parseInt(scanner.nextLine().trim());

            // Acumuladores LOCALES del empleado: se reinician en cada vuelta
            // del bucle externo, que es justo lo que queremos aquí.
            double subtotal = 0.0;
            int librosConRetraso = 0;

            // ===== BUCLE INTERNO: libros de ESTE empleado =====
            for (int l = 1; l <= numeroLibros; l++) {

                System.out.printf("  Libro %d - dias transcurridos: ", l);
                int diasTranscurridos = Integer.parseInt(scanner.nextLine().trim());

                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++;
                }
                subtotal += multa;
                totalLibros++;              // acumulador global

                System.out.printf("    retraso %d dias -> %.2f EUR%n", diasRetraso, multa);
            }

            totalGeneral += subtotal;

            if (subtotal > mayorMultaEmpleado) {
                mayorMultaEmpleado = subtotal;
                empleadoConMasMulta = empleado;
            }

            System.out.printf("  SUBTOTAL %s: %.2f EUR (%d libros con retraso de %d)%n",
                              empleado, subtotal, librosConRetraso, numeroLibros);
        }

        System.out.println();
        System.out.println("========== INFORME DE LA SESION ==========");
        System.out.printf("%-26s %d%n",       "Empleados atendidos:", numeroEmpleados);
        System.out.printf("%-26s %d%n",       "Libros procesados:", totalLibros);
        System.out.printf("%-26s %.2f EUR%n", "Total recaudado:", totalGeneral);
        System.out.printf("%-26s %s (%.2f EUR)%n", "Mayor multa:",
                          empleadoConMasMulta, mayorMultaEmpleado);
        System.out.println("=========================================");

        scanner.close();
    }
}

Sesión abreviada con dos empleados:

¿Cuantos empleados has atendido? 2

===== Empleado 1 de 2 =====
Nombre: Marta Ruiz
Libros que devuelve: 2
  Libro 1 - dias transcurridos: 27
    retraso 12 dias -> 3,00 EUR
  Libro 2 - dias transcurridos: 10
    retraso 0 dias -> 0,00 EUR
  SUBTOTAL Marta Ruiz: 3,00 EUR (1 libros con retraso de 2)

===== Empleado 2 de 2 =====
Nombre: Nuria Vidal
Libros que devuelve: 1
  Libro 1 - dias transcurridos: 120
    retraso 105 dias -> 20,00 EUR
  SUBTOTAL Nuria Vidal: 20,00 EUR (1 libros con retraso de 1)

========== INFORME DE LA SESION ==========
Empleados atendidos:       2
Libros procesados:         3
Total recaudado:           23,00 EUR
Mayor multa:               Nuria Vidal (20,00 EUR)
=========================================

El punto clave del ejercicio es la distinción entre acumuladores globales y locales. totalGeneral y totalLibros se declaran antes del bucle externo y sobreviven a toda la sesión. subtotal y librosConRetraso se declaran dentro del bucle externo, de modo que se crean y se ponen a cero en cada empleado. Colocar mal cualquiera de esas declaraciones produce un informe silenciosamente incorrecto: si subtotal estuviera fuera, arrastraría las multas del empleado anterior.

Respuesta a la pregunta final: para imprimir al final el detalle libro a libro necesitarías guardar todos los datos leídos, no solo sus agregados. Con variables escalares es imposible: cada titulo y cada multa se sobrescriben en la siguiente iteración. Harían falta estructuras capaces de contener muchos valores bajo un solo nombre —un array (lección 05-01) o un ArrayList (lección 05-03)— y, para agrupar título, ISBN y multa como una unidad, una clase propia Libro (módulo 3). Ese es exactamente el camino que sigue el curso.

Conclusión

Tu programa ya repite. Has diseccionado los tres bucles clásicos: while, que comprueba antes y puede no ejecutarse nunca; do-while, que ejecuta al menos una vez y es la herramienta natural de la validación y los menús; y for, que agrupa inicialización, condición y actualización en una cabecera y es el bucle de las repeticiones contadas. Sabes que existe el for-each y por qué esperarás al módulo 5 para usarlo. Has anidado bucles para generar la tabla de multas de BiblioTech, y sobre todo has aprendido los cuatro patrones que resuelven casi todo: contador, acumulador, búsqueda con bandera y bucle de validación, más el bucle de menú que estructurará el proyecto del módulo. Y conoces los dos peligros permanentes: el bucle infinito accidental y la concatenación de String dentro de bucles largos.

BiblioTechApp procesa ahora tandas enteras de devoluciones, insiste hasta que el usuario introduce datos válidos y produce un resumen agregado de la sesión. Pero también has visto su techo: al acabar el bucle solo quedan los agregados; los datos individuales se han perdido porque no tienes dónde guardarlos. Esa carencia se resolverá en el módulo 5.

En la siguiente lección, Sentencias Switch, sustituirás la escalera de if-else del despachador de menú por una construcción diseñada específicamente para elegir entre muchos valores de una misma variable. Verás el switch clásico con su temible fall-through, el switch con flechas de Java 14 en adelante y el switch como expresión que devuelve un valor, y con ellos escribirás la primera versión del despachador de opciones de BiblioTech.

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