Esta es la lección integradora del módulo. Todo lo que has aprendido en las cinco anteriores —condicionales, bucles, switch, break y continue, y el método para depurar cuando algo no cuadra— se junta aquí en una única aplicación completa: la versión 2.0 de BiblioTechApp, el programa que Nexus Software usará en el mostrador de su biblioteca técnica. Ya no termina tras un solo préstamo: muestra un menú, atiende operaciones una tras otra, exige datos correctos antes de aceptarlos, acumula estadísticas de toda la sesión y se despide con un cierre ordenado cuando el bibliotecario decide irse. No lo escribiremos de una vez: lo construiremos en cinco iteraciones, cada una añadiendo una capa sobre la anterior, exactamente como se desarrolla software real. Y terminaremos con la parte más honesta de todas: la lista de lo que este programa sigue sin poder hacer, con el módulo del curso que resuelve cada carencia.

Contenido

  1. La especificación completa
  2. Iteración 1: el esqueleto del menú
  3. Iteración 2: la validación de todas las entradas
  4. Iteración 3: la lógica de la devolución
  5. Iteración 4: las estadísticas de la sesión
  6. Iteración 5: el pulido de la salida
  7. El código completo final, comentado
  8. Compilar y ejecutar
  9. Una sesión de prueba completa
  10. Limitaciones: qué no puede hacer y qué módulo lo resolverá
  11. Errores Comunes y Consejos
  12. Ejercicios

  1. La especificación completa

Como en la lección 01-07, se escribe primero qué debe hacer el programa y solo después cómo.

Nombre: BiblioTechApp (versión 2.0) Paquete: com.nexussoftware.bibliotech

Comportamiento esperado:

  1. Mostrar una cabecera con la empresa, el nombre y la versión de la aplicación.
  2. Mostrar en bucle un menú con cuatro opciones y no terminar hasta que el usuario elija salir:
1. Registrar devolucion
2. Simular prestamo
3. Ver resumen de la sesion
0. Salir
  1. Opción 1 (registrar devolución): pedir empleado, título, ISBN y días transcurridos; validar cada dato; calcular el retraso, la multa con tope, el estado y la gravedad; emitir un recibo formateado con una referencia única.
  2. Opción 2 (simular préstamo): pedir empleado y título; informar del plazo, de la tarifa que se aplicará y de cuándo se alcanzaría el tope.
  3. Opción 3 (resumen de la sesión): mostrar los acumulados de todo lo hecho desde que arrancó el programa.
  4. Opción 0 (salir): imprimir un cierre con el total recaudado y terminar.
  5. Cualquier otra entrada debe mostrar un aviso y volver al menú sin romper nada.

Reglas de negocio, las mismas desde el módulo 1:

Regla Valor Constante
Duración estándar del préstamo 15 días DIAS_PRESTAMO
Tarifa por día de retraso 0,25 € TARIFA_DIARIA
Multa máxima por devolución 20,00 € MULTA_MAXIMA
Umbral de retraso leve 7 días UMBRAL_LEVE

Reglas de validación:

Dato Condición para aceptarlo
Empleado Al menos 3 caracteres tras trim()
Título No vacío ni en blanco
ISBN Empieza por 978 y tiene exactamente 14 caracteres
Días transcurridos Solo dígitos, y valor entre 0 y 3650

Estadísticas de la sesión (todas en variables escalares, que es lo único que tienes):

Variable Qué guarda
devolucionesRegistradas Contador de devoluciones procesadas
devolucionesConRetraso Cuántas de ellas llegaron tarde
topesAlcanzados Cuántas alcanzaron MULTA_MAXIMA
prestamosSimulados Contador de préstamos
totalMultas Acumulador de importes
mayorRetraso Máximo de días de retraso visto
empleadoMayorRetraso Nombre asociado a ese máximo
libroMayorRetraso Título asociado a ese máximo

Restricciones técnicas: todo dentro de main, sin clases propias, sin arrays ni colecciones, sin excepciones. Solo lo aprendido hasta aquí.

El flujo completo del programa:

flowchart TD
    INI["Cabecera de bienvenida<br/>e inicializacion de contadores"] --> MENU["Mostrar menu"]
    MENU --> LEER["Leer opcion con nextLine()"]
    LEER --> SW{"switch (opcion)"}

    SW -- "1" --> V1["Validar empleado, titulo,<br/>ISBN y dias (bucles do-while)"]
    V1 --> C1["Calcular retraso, multa,<br/>estado y gravedad"]
    C1 --> E1["Actualizar estadisticas"]
    E1 --> R1["Imprimir recibo"] --> MENU

    SW -- "2" --> V2["Validar empleado y titulo"]
    V2 --> R2["Informar del plazo<br/>y la tarifa"] --> MENU

    SW -- "3" --> R3["Imprimir resumen<br/>de la sesion"] --> MENU

    SW -- "otra" --> ERR["Aviso: opcion no reconocida"] --> MENU

    SW -- "0" --> FIN["salir = true"]
    FIN --> CIERRE["Cierre con total recaudado<br/>y scanner.close()"]

  1. Iteración 1: el esqueleto del menú

Primero, el armazón: mostrar el menú, leer una opción, despacharla y repetir. Sin lógica de negocio, solo mensajes de sitio.

package com.nexussoftware.bibliotech;

import java.util.Scanner;

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

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

        while (true) {

            System.out.println();
            System.out.println("  1. Registrar devolucion");
            System.out.println("  2. Simular prestamo");
            System.out.println("  3. Ver resumen de la sesion");
            System.out.println("  0. Salir");
            System.out.print("  Opcion: ");

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

            switch (opcion) {
                case "1" -> System.out.println("  [registrar devolucion]");
                case "2" -> System.out.println("  [simular prestamo]");
                case "3" -> System.out.println("  [resumen de la sesion]");
                case "0" -> salir = true;
                default  -> System.out.println("  Opcion no reconocida.");
            }

            if (salir) {
                break;
            }
        }

        System.out.println("Sesion cerrada.");
        scanner.close();
    }
}

Tres decisiones de diseño que conviene entender antes de seguir, porque condicionan todo lo demás:

1. La opción se lee como String, no como int. Si usaras Integer.parseInt directamente y el usuario escribiera hola, el programa se detendría con un NumberFormatException. Leyéndola como texto, cualquier entrada rara cae limpiamente en el default.

2. La salida se controla con la bandera salir y un break situado en el cuerpo del bucle, no dentro del switch. Esta es la aplicación directa de la trampa que estudiaste en la lección 02-04: un break escrito dentro de una rama del switch —incluso con la sintaxis de flechas, donde case "0" -> { … break; } compila perfectamente— saldría del switch y no del while, y el menú sería imposible de abandonar. Al poner if (salir) { break; } después del switch, el break está directamente en el cuerpo del bucle y no hay ninguna ambigüedad.

3. El switch usa flechas. Sin break entre ramas, sin fall-through posible, y con las llaves delimitando el ámbito de cada rama, de modo que las variables declaradas en la opción 1 no chocan con las de la opción 2.

Pruébalo antes de continuar: debe recorrer el menú indefinidamente, avisar de las opciones desconocidas y cerrarse con 0. Si no se cierra, revisa el punto 2.

  1. Iteración 2: la validación de todas las entradas

Ahora se rellenan los huecos con la lectura de datos, aplicando el bucle de validación de la lección 02-02: preguntar, comprobar, y volver a preguntar mientras el dato no sirva.

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.");
    }
} while (empleado.length() < 3);

El caso interesante es el número. Recuerda que ni un if ni un bucle pueden evitar que Integer.parseInt("veinte") detenga el programa: el fallo ocurre dentro de la conversión. Pero sí puedes comprobar antes que la cadena está formada solo por dígitos, recorriéndola carácter a carácter con un for y charAt:

int diasTranscurridos = 0;
boolean diasValidos = false;

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

    // Comprobacion previa: ¿son todos los caracteres digitos?
    boolean soloDigitos = !entrada.isEmpty();
    for (int i = 0; i < entrada.length(); i++) {
        if (!Character.isDigit(entrada.charAt(i))) {
            soloDigitos = false;
            break;                  // en cuanto falla uno, no hace falta seguir
        }
    }

    if (!soloDigitos) {
        System.out.println("    Escribe un numero entero sin signos ni letras.");
        continue;                   // descarta esta vuelta y vuelve a preguntar
    }

    // Aqui ya es seguro convertir: parseInt no puede fallar.
    diasTranscurridos = Integer.parseInt(entrada);
    diasValidos = diasTranscurridos <= 3650;
    if (!diasValidos) {
        System.out.println("    Valor fuera de rango.");
    }

} while (!diasValidos);

Este fragmento condensa medio módulo: un do-while de validación, un for de recorrido, un break que corta la comprobación en cuanto encuentra un carácter no válido, un continue que descarta la iteración cuando la entrada no es numérica y una bandera booleana que gobierna la condición del bucle exterior.

Y observa un efecto secundario elegante: como el signo - no es un dígito, los días negativos quedan rechazados automáticamente por la misma comprobación. Aquella promesa de la lección 02-01 —"el programa ya no acepta días negativos"— queda cumplida sin una sola condición adicional.

Esta comprobación previa es una defensa artesanal. En el módulo 6 la sustituirás por un try-catch, que es la forma correcta y general de tratar entradas mal formadas.

  1. Iteración 3: la lógica de la devolución

Con los datos ya garantizados válidos, se aplican las reglas de negocio del módulo 1 y la escalera de decisión de la lección 02-01:

// 1. Retraso saturado a cero: nunca negativo.
int diasRetraso = diasTranscurridos - DIAS_PRESTAMO;
if (diasRetraso < 0) {
    diasRetraso = 0;
}

// 2. Descomposicion en semanas y dias sueltos (division entera y modulo).
int semanasRetraso = diasRetraso / 7;
int diasSueltos = diasRetraso % 7;

// 3. Multa con tope.
double multa = diasRetraso * TARIFA_DIARIA;
boolean topada = multa >= MULTA_MAXIMA;
if (topada) {
    multa = MULTA_MAXIMA;
}

// 4. Estado y gravedad: la tabla de decision de la leccion 02-01.
String estado;
String gravedad;
if (diasRetraso == 0) {
    estado = "EN PLAZO";
    gravedad = "NINGUNA";
} else {
    estado = "RETRASADO";
    if (topada) {
        gravedad = "MAXIMA";
    } else if (diasRetraso <= UMBRAL_LEVE) {
        gravedad = "LEVE";
    } else {
        gravedad = "GRAVE";
    }
}

Fíjate en el orden de la escalera interna: el tope se comprueba antes que el umbral leve, porque una multa topada es siempre la clasificación prioritaria, independientemente de los días. Es exactamente la tabla de decisión que construiste en la lección 02-01, y aquí se ve por qué merecía la pena escribirla antes que el código.

La referencia del recibo se compone con métodos de String del módulo 1 y el contador de la sesión, de modo que cada recibo de la sesión sea único:

String referencia = "DEV-" + isbn.substring(isbn.length() - 4)
                  + "-" + (devolucionesRegistradas + 1);
// Con ISBN 978-0000000001 y primera devolucion -> DEV-0001-1

  1. Iteración 4: las estadísticas de la sesión

Aquí entran los patrones de acumulación de la lección 02-02: contador, acumulador y máximo con dato asociado. Todas estas variables se declaran antes del while, porque deben sobrevivir a todas las vueltas del menú; si estuvieran dentro del bucle, se reiniciarían en cada operación (que es justo el bug que depuraste en la lección 02-05).

// FUERA del bucle del menu: sobreviven a toda la sesion.
int devolucionesRegistradas = 0;
int devolucionesConRetraso  = 0;
int topesAlcanzados         = 0;
int prestamosSimulados      = 0;
double totalMultas          = 0.0;
int mayorRetraso            = 0;
String empleadoMayorRetraso = "-";
String libroMayorRetraso    = "-";

Y la actualización, dentro de la rama de la devolución, después de calcular:

devolucionesRegistradas++;                 // contador
totalMultas += multa;                      // acumulador

if (diasRetraso > 0) {
    devolucionesConRetraso++;
}
if (topada) {
    topesAlcanzados++;
}

// Maximo CON dato asociado: se actualizan las tres variables a la vez.
if (diasRetraso > mayorRetraso) {
    mayorRetraso = diasRetraso;
    empleadoMayorRetraso = empleado;
    libroMayorRetraso = titulo;
}

El patrón del máximo con dato asociado merece atención: cuando encuentras un nuevo récord, tienes que actualizar todas las variables que lo describen en el mismo if. Olvidar una produce un resumen incoherente ("mayor retraso: 105 días, empleado: Marta Ruiz" cuando en realidad fue Diego Alonso). Y esa incomodidad —tres variables sueltas que en realidad son un solo dato— es el argumento exacto que abre el módulo 3: lo natural sería un objeto con tres campos.

En el resumen hay que protegerse de la división por cero, tal como aprendiste depurando:

double mediaMulta;
if (devolucionesRegistradas == 0) {
    mediaMulta = 0.0;                      // sin devoluciones no hay media
} else {
    mediaMulta = totalMultas / devolucionesRegistradas;
}

  1. Iteración 5: el pulido de la salida

La última iteración no añade funcionalidad: hace que el programa parezca profesional. Las técnicas son las del módulo 1, printf y String.repeat, aplicadas con criterio.

  • Anchos fijos con %-18s para que las etiquetas queden alineadas en columna.
  • %.2f para todos los importes. Un recibo que diga 3.0 EUR no es aceptable.
  • Separadores con "=".repeat(ANCHO) y una constante ANCHO para no repetir el número mágico por todas partes.
  • Sangría por niveles: el menú a dos espacios, los datos de un formulario a cuatro. Ayuda a entender la jerarquía de lo que se está leyendo sin ningún adorno gráfico.
  • Un aviso destacado solo cuando se alcanza el tope, en lugar de repetir información irrelevante en cada recibo.

  1. El código completo final, comentado

package com.nexussoftware.bibliotech;

import java.util.Scanner;

/**
 * BiblioTech 2.0 - Gestion de la biblioteca tecnica de Nexus Software.
 *
 * Version del modulo 2 (Flujo de Control): menu interactivo con validacion
 * de entradas y estadisticas de sesion. Todo el codigo vive en main y usa
 * unicamente variables escalares, condicionales y bucles.
 */
public class BiblioTechApp {

    public static void main(String[] args) {

        // ===== CONSTANTES: reglas de negocio y configuracion =====
        final String NOMBRE_EMPRESA = "Nexus Software";
        final String VERSION        = "2.0 (modulo 2)";
        final int    DIAS_PRESTAMO  = 15;
        final double TARIFA_DIARIA  = 0.25;
        final double MULTA_MAXIMA   = 20.0;
        final int    UMBRAL_LEVE    = 7;
        final int    DIAS_MAXIMOS   = 3650;
        final int    ANCHO          = 52;

        Scanner scanner = new Scanner(System.in);

        // ===== ESTADISTICAS DE LA SESION =====
        // Declaradas FUERA del bucle: deben sobrevivir a todas las operaciones.
        int    devolucionesRegistradas = 0;
        int    devolucionesConRetraso  = 0;
        int    topesAlcanzados         = 0;
        int    prestamosSimulados      = 0;
        double totalMultas             = 0.0;
        int    mayorRetraso            = 0;
        String empleadoMayorRetraso    = "-";
        String libroMayorRetraso       = "-";

        // ===== CABECERA =====
        System.out.println("=".repeat(ANCHO));
        System.out.println("  BIBLIOTECH " + VERSION);
        System.out.println("  Biblioteca tecnica de " + NOMBRE_EMPRESA);
        System.out.println("=".repeat(ANCHO));

        boolean salir = false;

        // ===== BUCLE PRINCIPAL DEL MENU =====
        while (true) {

            System.out.println();
            System.out.println("-".repeat(ANCHO));
            System.out.println("  MENU PRINCIPAL");
            System.out.println("-".repeat(ANCHO));
            System.out.println("  1. Registrar devolucion");
            System.out.println("  2. Simular prestamo");
            System.out.println("  3. Ver resumen de la sesion");
            System.out.println("  0. Salir");
            System.out.print("  Opcion: ");

            // Se lee como texto: cualquier entrada rara cae en el default.
            String opcion = scanner.nextLine().trim();

            switch (opcion) {

                // ---------- OPCION 1: REGISTRAR DEVOLUCION ----------
                case "1" -> {
                    System.out.println();
                    System.out.println("  --- REGISTRAR DEVOLUCION ---");

                    // Validacion del nombre del empleado
                    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.");
                        }
                    } while (empleado.length() < 3);

                    // Validacion del titulo
                    String titulo;
                    do {
                        System.out.print("  Titulo del libro: ");
                        titulo = scanner.nextLine().trim();
                        if (titulo.isBlank()) {
                            System.out.println("    El titulo no puede estar vacio.");
                        }
                    } while (titulo.isBlank());

                    // Validacion del ISBN: prefijo 978 y longitud exacta
                    String isbn;
                    do {
                        System.out.print("  ISBN (978-XXXXXXXXXX): ");
                        isbn = scanner.nextLine().trim();
                        if (!isbn.startsWith("978") || isbn.length() != 14) {
                            System.out.println("    ISBN no valido para esta biblioteca.");
                        }
                    } while (!isbn.startsWith("978") || isbn.length() != 14);

                    // Validacion de los dias: primero que sean digitos, luego el rango
                    int diasTranscurridos = 0;
                    boolean diasValidos = false;
                    do {
                        System.out.print("  Dias transcurridos (0-" + DIAS_MAXIMOS + "): ");
                        String entrada = scanner.nextLine().trim();

                        boolean soloDigitos = !entrada.isEmpty();
                        for (int i = 0; i < entrada.length(); i++) {
                            if (!Character.isDigit(entrada.charAt(i))) {
                                soloDigitos = false;
                                break;      // un caracter malo basta: no seguimos
                            }
                        }

                        if (!soloDigitos) {
                            System.out.println("    Escribe un numero entero sin signos ni letras.");
                            continue;       // vuelve a preguntar
                        }

                        diasTranscurridos = Integer.parseInt(entrada);
                        diasValidos = diasTranscurridos <= DIAS_MAXIMOS;
                        if (!diasValidos) {
                            System.out.println("    Valor fuera de rango.");
                        }
                    } while (!diasValidos);

                    // ----- Reglas de negocio -----
                    int diasRetraso = diasTranscurridos - DIAS_PRESTAMO;
                    if (diasRetraso < 0) {
                        diasRetraso = 0;                 // saturado a cero
                    }

                    int semanasRetraso = diasRetraso / 7;
                    int diasSueltos    = diasRetraso % 7;

                    double multa = diasRetraso * TARIFA_DIARIA;
                    boolean topada = multa >= MULTA_MAXIMA;
                    if (topada) {
                        multa = MULTA_MAXIMA;            // tope aplicado
                    }

                    String estado;
                    String gravedad;
                    if (diasRetraso == 0) {
                        estado = "EN PLAZO";
                        gravedad = "NINGUNA";
                    } else {
                        estado = "RETRASADO";
                        if (topada) {
                            gravedad = "MAXIMA";         // el tope manda sobre los dias
                        } else if (diasRetraso <= UMBRAL_LEVE) {
                            gravedad = "LEVE";
                        } else {
                            gravedad = "GRAVE";
                        }
                    }

                    String referencia = "DEV-" + isbn.substring(isbn.length() - 4)
                                      + "-" + (devolucionesRegistradas + 1);

                    // ----- Actualizacion de las estadisticas de sesion -----
                    devolucionesRegistradas++;
                    totalMultas += multa;
                    if (diasRetraso > 0) {
                        devolucionesConRetraso++;
                    }
                    if (topada) {
                        topesAlcanzados++;
                    }
                    if (diasRetraso > mayorRetraso) {
                        mayorRetraso         = diasRetraso;
                        empleadoMayorRetraso = empleado;
                        libroMayorRetraso    = titulo;
                    }

                    // ----- Recibo -----
                    System.out.println();
                    System.out.println("  " + "=".repeat(ANCHO - 4));
                    System.out.println("    RECIBO DE DEVOLUCION  " + referencia);
                    System.out.println("  " + "=".repeat(ANCHO - 4));
                    System.out.printf("    %-18s %s%n", "Empleado:", empleado);
                    System.out.printf("    %-18s %s%n", "Libro:", titulo);
                    System.out.printf("    %-18s %s%n", "ISBN:", isbn);
                    System.out.printf("    %-18s %d%n", "Dias prestado:", diasTranscurridos);
                    System.out.printf("    %-18s %d (%d sem. y %d dias)%n",
                                      "Dias de retraso:", diasRetraso,
                                      semanasRetraso, diasSueltos);
                    System.out.printf("    %-18s %s / %s%n", "Estado:", estado, gravedad);
                    System.out.printf("    %-18s %.2f EUR%n", "Multa:", multa);
                    if (topada) {
                        System.out.println("    AVISO: multa topada. Notificar a RRHH.");
                    }
                    System.out.println("  " + "=".repeat(ANCHO - 4));
                }

                // ---------- OPCION 2: SIMULAR PRESTAMO ----------
                case "2" -> {
                    System.out.println();
                    System.out.println("  --- SIMULAR PRESTAMO ---");

                    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.");
                        }
                    } while (empleado.length() < 3);

                    String titulo;
                    do {
                        System.out.print("  Titulo del libro: ");
                        titulo = scanner.nextLine().trim();
                        if (titulo.isBlank()) {
                            System.out.println("    El titulo no puede estar vacio.");
                        }
                    } while (titulo.isBlank());

                    prestamosSimulados++;

                    System.out.println();
                    System.out.printf("    Prestamo #%d autorizado.%n", prestamosSimulados);
                    System.out.printf("    %s -> %s%n", titulo, empleado);
                    System.out.printf("    Plazo: %d dias. A partir del dia %d se aplican "
                                    + "%.2f EUR/dia.%n",
                                      DIAS_PRESTAMO, DIAS_PRESTAMO + 1, TARIFA_DIARIA);
                    System.out.printf("    El tope de %.2f EUR se alcanza a los %d dias "
                                    + "de retraso.%n",
                                      MULTA_MAXIMA,
                                      (int) Math.ceil(MULTA_MAXIMA / TARIFA_DIARIA));
                }

                // ---------- OPCION 3: RESUMEN DE LA SESION ----------
                case "3" -> {
                    // Guarda contra la division por cero.
                    double mediaMulta;
                    if (devolucionesRegistradas == 0) {
                        mediaMulta = 0.0;
                    } else {
                        mediaMulta = totalMultas / devolucionesRegistradas;
                    }

                    System.out.println();
                    System.out.println("  " + "=".repeat(ANCHO - 4));
                    System.out.println("    RESUMEN DE LA SESION");
                    System.out.println("  " + "=".repeat(ANCHO - 4));
                    System.out.printf("    %-26s %d%n", "Devoluciones registradas:",
                                      devolucionesRegistradas);
                    System.out.printf("    %-26s %d%n", "  con retraso:",
                                      devolucionesConRetraso);
                    System.out.printf("    %-26s %d%n", "  con multa topada:",
                                      topesAlcanzados);
                    System.out.printf("    %-26s %d%n", "Prestamos simulados:",
                                      prestamosSimulados);
                    System.out.printf("    %-26s %.2f EUR%n", "Total de multas:", totalMultas);
                    System.out.printf("    %-26s %.2f EUR%n", "Multa media:", mediaMulta);

                    if (mayorRetraso > 0) {
                        System.out.printf("    %-26s %d dias%n", "Mayor retraso:", mayorRetraso);
                        System.out.printf("    %-26s %s%n", "  empleado:", empleadoMayorRetraso);
                        System.out.printf("    %-26s %s%n", "  libro:", libroMayorRetraso);
                    } else {
                        System.out.printf("    %-26s ninguno%n", "Mayor retraso:");
                    }
                    System.out.println("  " + "=".repeat(ANCHO - 4));
                }

                // ---------- OPCION 0: SALIR ----------
                // Solo levanta la bandera: el break va DESPUES del switch.
                case "0" -> salir = true;

                // ---------- CUALQUIER OTRA ENTRADA ----------
                default -> System.out.println("  Opcion no reconocida. Usa 0, 1, 2 o 3.");
            }

            // El break esta en el cuerpo del bucle, no dentro del switch:
            // asi rompe el WHILE y no el SWITCH (leccion 02-04).
            if (salir) {
                break;
            }
        }

        // ===== CIERRE =====
        System.out.println();
        System.out.println("=".repeat(ANCHO));
        System.out.printf("  Sesion cerrada. %d devoluciones, %.2f EUR recaudados.%n",
                          devolucionesRegistradas, totalMultas);
        System.out.println("  Gracias por usar BiblioTech - " + NOMBRE_EMPRESA);
        System.out.println("=".repeat(ANCHO));

        scanner.close();
    }
}

  1. Compilar y ejecutar

Desde la raíz del proyecto bibliotech que creaste en el módulo 1:

cd bibliotech
javac -d out src/main/java/com/nexussoftware/bibliotech/BiblioTechApp.java
java -cp out com.nexussoftware.bibliotech.BiblioTechApp

Desde el IDE, basta con el botón de ejecutar sobre la clase. Y si algo no cuadra, ya sabes qué hacer: punto de interrupción en la línea del acumulador, condición devolucionesRegistradas == 2, y a mirar la ventana de variables.

  1. Una sesión de prueba completa

Esta es la tabla de prueba del programa: cada línea es una entrada del usuario y lo que debe ocurrir. Ejecútala tal cual y compara.

# Entrada del usuario Respuesta esperada
1 1 Entra en el formulario de devolución
2 Marta Ruiz Aceptado
3 Java Efectivo Aceptado
4 978-0000000001 Aceptado
5 veinte Rechazado: "Escribe un numero entero sin signos ni letras."
6 27 Aceptado. Recibo DEV-0001-1: 12 días de retraso, 3,00 €, RETRASADO / GRAVE
7 1 Segundo formulario de devolución
8 Diego Alonso Aceptado
9 Refactorizacion Aceptado
10 978-0000000003 Aceptado
11 -5 Rechazado: el signo - no es un dígito
12 120 Aceptado. Recibo DEV-0003-2: 105 días, 20,00 €, MAXIMA, con aviso a RRHH
13 2 Formulario de préstamo
14 Nuria Vidal Aceptado
15 Patrones de Diseno Préstamo #1 autorizado, plazo 15 días, tope a los 80 días
16 9 "Opcion no reconocida. Usa 0, 1, 2 o 3."
17 3 Resumen: 2 devoluciones (2 con retraso, 1 topada), 1 préstamo, 23,00 € totales, 11,50 € de media, mayor retraso 105 días de Diego Alonso con "Refactorizacion"
18 0 Cierre: "Sesion cerrada. 2 devoluciones, 23,00 EUR recaudados."

Los tramos más significativos de la salida real:

====================================================
  BIBLIOTECH 2.0 (modulo 2)
  Biblioteca tecnica de Nexus Software
====================================================

----------------------------------------------------
  MENU PRINCIPAL
----------------------------------------------------
  1. Registrar devolucion
  2. Simular prestamo
  3. Ver resumen de la sesion
  0. Salir
  Opcion: 1

  --- REGISTRAR DEVOLUCION ---
  Empleado: Marta Ruiz
  Titulo del libro: Java Efectivo
  ISBN (978-XXXXXXXXXX): 978-0000000001
  Dias transcurridos (0-3650): veinte
    Escribe un numero entero sin signos ni letras.
  Dias transcurridos (0-3650): 27

  ================================================
    RECIBO DE DEVOLUCION  DEV-0001-1
  ================================================
    Empleado:          Marta Ruiz
    Libro:             Java Efectivo
    ISBN:              978-0000000001
    Dias prestado:     27
    Dias de retraso:   12 (1 sem. y 5 dias)
    Estado:            RETRASADO / GRAVE
    Multa:             3,00 EUR
  ================================================

El segundo recibo, con el tope aplicado:

  ================================================
    RECIBO DE DEVOLUCION  DEV-0003-2
  ================================================
    Empleado:          Diego Alonso
    Libro:             Refactorizacion
    ISBN:              978-0000000003
    Dias prestado:     120
    Dias de retraso:   105 (15 sem. y 0 dias)
    Estado:            RETRASADO / MAXIMA
    Multa:             20,00 EUR
    AVISO: multa topada. Notificar a RRHH.
  ================================================

El resumen de la sesión y el cierre:

  ================================================
    RESUMEN DE LA SESION
  ================================================
    Devoluciones registradas:  2
      con retraso:             2
      con multa topada:        1
    Prestamos simulados:       1
    Total de multas:           23,00 EUR
    Multa media:               11,50 EUR
    Mayor retraso:             105 dias
      empleado:                Diego Alonso
      libro:                   Refactorizacion
  ================================================

====================================================
  Sesion cerrada. 2 devoluciones, 23,00 EUR recaudados.
  Gracias por usar BiblioTech - Nexus Software
====================================================

Verificación manual de las cifras, que es el paso 2 del método de depuración: la devolución de Marta Ruiz son 27 − 15 = 12 días × 0,25 = 3,00 €; la de Diego Alonso, 120 − 15 = 105 días × 0,25 = 26,25 €, topada a 20,00 €. Total 23,00 €, media 11,50 €. Coincide.

Además, prueba estos casos límite, que es donde viven los bugs:

Caso límite Comportamiento correcto
Opción 3 nada más arrancar Todo a cero, "Mayor retraso: ninguno", sin dividir por cero
0 días transcurridos 0 de retraso, EN PLAZO / NINGUNA, 0,00 €
15 días exactos 0 de retraso (el día 15 aún está en plazo)
16 días 1 día de retraso, 0,25 €, LEVE
22 días 7 de retraso: LEVE (el umbral es <=)
23 días 8 de retraso: GRAVE
95 días 80 de retraso, 20,00 € exactos: MAXIMA
Pulsar Intro sin escribir nada Cae en el default o repite la pregunta, nunca rompe

  1. Limitaciones: qué no puede hacer y qué módulo lo resolverá

Esta sección es tan importante como el código. Un buen desarrollador conoce las carencias de lo que ha construido y sabe por qué existen.

1. Todo el estado son variables sueltas. empleadoMayorRetraso, libroMayorRetraso y mayorRetraso son tres variables independientes que en realidad describen una sola cosa. Nada impide actualizar una y olvidar las otras dos, y el compilador no puede ayudarte. Lo natural sería un objeto Devolucion con sus campos empleado, libro y diasRetraso agrupados y coherentes. → Módulo 3: Programación Orientada a Objetos, donde crearás las clases Libro, Empleado y Prestamo.

2. Solo cabe un libro a la vez. Cada devolución sobrescribe la anterior. No puedes listar lo procesado, ni buscar un ISBN concreto, ni ordenar por multa, ni consultar un catálogo real: el "catálogo" son tres líneas escritas a mano. Necesitas estructuras que guarden muchos valores bajo un solo nombre. → Módulo 5: Estructuras de Datos y Colecciones, con arrays, ArrayList y HashMap.

3. Una entrada mal formada aún puede romper el programa. La comprobación de dígitos es artesanal y solo cubre el caso que anticipaste. Un Integer.parseInt de un número de veinte cifras desborda el rango de int, y cualquier otra operación imprevista detiene la aplicación con una traza de pila. Hace falta un mecanismo general para tratar lo excepcional. → Módulo 6: Manejo de Excepciones, con try-catch, excepciones propias y try-with-resources (que además cerrará el Scanner correctamente).

4. Al salir se pierde todo. El resumen de la sesión desaparece con el proceso. Mañana el bibliotecario empieza de cero: ni multas pendientes, ni histórico, ni catálogo persistente. Hace falta guardar en disco y volver a leer. → Módulo 7: Entrada/Salida de Archivos, con lectura y escritura, CSV y Properties.

5. main mide más de doscientas líneas. Cada rama del switch es un bloque enorme y los formularios de las opciones 1 y 2 repiten literalmente el mismo bucle de validación del empleado y del título. Duplicar código es duplicar los sitios donde corregir un fallo. → Lección 03-03: Métodos, que te permitirá escribir leerTextoValido("Empleado: ", 3) una vez y llamarlo cuatro veces.

6. No hay pruebas automáticas. Cada cambio obliga a repetir la sesión de prueba a mano, tecleando dieciocho entradas. Es lento y acabas no haciéndolo. → Módulo 11: JUnit, y las técnicas de calidad del módulo 12.

7. Es de un solo usuario, local y sin interfaz. Un bibliotecario, una consola, un ordenador. → Módulo 9 (Redes) y módulo 12 (Aplicación Web).

Ninguna de estas limitaciones es un fallo del programa: son las fronteras exactas de lo que se puede construir con flujo de control y variables escalares. Y cada módulo restante del curso está diseñado para derribar una de ellas.

Errores Comunes y Consejos

1. Declarar las estadísticas dentro del bucle del menú. Se reinician en cada operación y el resumen siempre muestra los datos de la última. Es el bug que depuraste en la lección 02-05, y en este programa es especialmente fácil de cometer.

2. Poner el break de salida dentro del switch. El menú no se cierra jamás. Recuerda que en la sintaxis de flechas case "0" -> { … break; } compila y hace exactamente lo que no quieres.

3. Leer la opción con nextInt(). Reintroduce el problema del salto de línea pendiente que resolviste en el módulo 1 y, además, rompe el programa ante cualquier texto. La convención del curso es firme: siempre nextLine() y conversión explícita.

4. Validar después de calcular. Si conviertes a número antes de comprobar que la cadena es numérica, la comprobación llega tarde. El orden es: leer → comprobar → convertir → usar.

5. Olvidar trim(). Un espacio final convierte "1 " en una opción no reconocida y al usuario le parecerá que el programa está roto.

6. Actualizar el máximo a medias. Si actualizas mayorRetraso pero no empleadoMayorRetraso, el resumen mentirá. Mantén las tres asignaciones juntas en el mismo if y con la misma indentación, para que salte a la vista si falta una.

7. Cerrar el Scanner dentro del bucle. Un scanner.close() en la rama de salir, antes del break, funciona; dentro de otra rama, deja el programa sin entrada para la siguiente vuelta y todo falla a partir de ahí. Ciérralo una sola vez, al final de main.

8. Consejo: prueba cada iteración antes de escribir la siguiente. Es la razón de construir en cinco pasos. Si el menú no se cierra, arréglalo antes de añadir la validación; si no, tendrás dos bugs entrelazados y no sabrás cuál es cuál.

9. Consejo: extrae los números mágicos a constantes. ANCHO, DIAS_MAXIMOS y las cuatro reglas de negocio están arriba del todo, con nombre. Cambiar la tarifa es tocar una línea, no buscar 0.25 por todo el fichero.

10. Consejo: guarda esta versión en Git antes de tocarla. git add y git commit -m "BiblioTech 2.0: menu interactivo" te dan un punto de retorno seguro para hacer los ejercicios sin miedo.

Ejercicios

Ejercicio 1: nueva opción "4. Consultar tabla de multas"

Añade al menú una cuarta opción que muestre la escala de multas de BiblioTech sin pedir ningún dato: una tabla con los días de retraso de 5 en 5 hasta 90, mostrando la multa sin tope, la multa aplicada y la gravedad (LEVE, GRAVE o MAXIMA). Debe generarse con un bucle for y usar las constantes existentes, sin números escritos a mano.

Requisitos:

  • La opción debe aparecer en el menú y en el mensaje del default.
  • La tabla debe estar alineada con printf.
  • Marca con un <-- la primera fila en la que se alcanza el tope, y solo esa (pista: una bandera booleana).

Ejercicio 2: contador de opciones inválidas y de tiempo de sesión

Añade dos estadísticas más al resumen y al cierre:

  1. opcionesInvalidas: cuántas veces el usuario ha introducido algo que cayó en el default.
  2. operacionesTotales: la suma de devoluciones, préstamos y consultas de resumen (es decir, todas las opciones válidas distintas de salir).

Muestra ambas en la opción 3 y en el cierre. Añade también una línea que calcule el porcentaje de acierto del usuario: operacionesTotales * 100.0 / (operacionesTotales + opcionesInvalidas), protegiéndote de la división por cero cuando aún no se haya hecho nada.

Ejercicio 3: rechazar devoluciones duplicadas en la misma sesión

Con lo que tienes (variables escalares, sin colecciones), implementa una comprobación mínima: guarda el ISBN de la última devolución registrada en una variable String ultimoIsbn, inicializada a "". Si el usuario intenta registrar una devolución con el mismo ISBN dos veces seguidas, muestra un aviso y pregunta si quiere continuar de todos modos (si/no, validado en bucle e insensible a mayúsculas). Si responde que no, se descarta la operación y se vuelve al menú sin tocar ninguna estadística.

Al terminar, responde por escrito: ¿por qué esta comprobación solo detecta duplicados consecutivos y qué necesitarías para detectar cualquier duplicado de la sesión?

Soluciones

Solución 1

Se añade la línea al menú, se amplía el mensaje del default y se introduce la nueva rama en el switch:

System.out.println("  4. Consultar tabla de multas");
// ---------- OPCION 4: TABLA DE MULTAS ----------
case "4" -> {
    System.out.println();
    System.out.println("  " + "=".repeat(ANCHO - 4));
    System.out.println("    ESCALA DE MULTAS DE BIBLIOTECH");
    System.out.println("  " + "=".repeat(ANCHO - 4));
    System.out.printf("    %6s %12s %12s  %s%n",
                      "DIAS", "SIN TOPE", "APLICADA", "GRAVEDAD");

    boolean topeYaMarcado = false;      // bandera: solo se marca la primera fila

    for (int dias = 5; dias <= 90; dias += 5) {

        double sinTope = dias * TARIFA_DIARIA;
        double aplicada = sinTope;
        String gravedad;
        String marca = "";

        if (sinTope >= MULTA_MAXIMA) {
            aplicada = MULTA_MAXIMA;
            gravedad = "MAXIMA";
            if (!topeYaMarcado) {
                marca = "  <--";        // solo la PRIMERA vez
                topeYaMarcado = true;
            }
        } else if (dias <= UMBRAL_LEVE) {
            gravedad = "LEVE";
        } else {
            gravedad = "GRAVE";
        }

        System.out.printf("    %6d %12.2f %12.2f  %-8s%s%n",
                          dias, sinTope, aplicada, gravedad, marca);
    }

    System.out.println("  " + "=".repeat(ANCHO - 4));
}

Y el default actualizado:

default -> System.out.println("  Opcion no reconocida. Usa 0, 1, 2, 3 o 4.");

Salida (fragmento):

    ESCALA DE MULTAS DE BIBLIOTECH
  ================================================
      DIAS     SIN TOPE     APLICADA  GRAVEDAD
         5         1,25         1,25  LEVE
        10         2,50         2,50  GRAVE
        15         3,75         3,75  GRAVE
        ...
        75        18,75        18,75  GRAVE
        80        20,00        20,00  MAXIMA    <--
        85        21,25        20,00  MAXIMA
        90        22,50        20,00  MAXIMA

Puntos clave: la bandera topeYaMarcado es el patrón de la lección 02-02 (una vez levantada no vuelve a bajar), y marca se inicializa a cadena vacía para que el printf funcione igual en todas las filas sin necesidad de dos printf distintos. Fíjate también en que la fila de 5 días es LEVE y la de 10 ya es GRAVE: el umbral está en 7, entre ambas.

Solución 2

Nuevas variables, declaradas junto al resto de estadísticas, fuera del bucle:

int opcionesInvalidas  = 0;
int operacionesTotales = 0;

Incrementos en cada rama válida y en el default:

case "1" -> {
    operacionesTotales++;
    // ... resto de la rama sin cambios ...
}

case "2" -> {
    operacionesTotales++;
    // ...
}

case "3" -> {
    operacionesTotales++;
    // ...
}

default -> {
    opcionesInvalidas++;
    System.out.println("  Opcion no reconocida. Usa 0, 1, 2 o 3.");
}

Bloque añadido al resumen de la opción 3:

// Porcentaje de acierto, con guarda contra la division por cero.
int intentos = operacionesTotales + opcionesInvalidas;
double acierto;
if (intentos == 0) {
    acierto = 100.0;                    // aun no ha hecho nada: no hay errores
} else {
    acierto = operacionesTotales * 100.0 / intentos;
}

System.out.printf("    %-26s %d%n",      "Operaciones validas:", operacionesTotales);
System.out.printf("    %-26s %d%n",      "Opciones invalidas:", opcionesInvalidas);
System.out.printf("    %-26s %.1f %%%n", "Acierto:", acierto);

Y en el cierre:

System.out.printf("  %d operaciones validas y %d opciones invalidas.%n",
                  operacionesTotales, opcionesInvalidas);

Ejemplo con la sesión de prueba del apartado 9 (dos devoluciones, un préstamo, un 9 erróneo y una consulta de resumen):

    Operaciones validas:       4
    Opciones invalidas:        1
    Acierto:                   80,0 %

Dos detalles a razonar. Primero, operacionesTotales++ va al principio de cada rama: si lo pusieras al final y el usuario abandonara a mitad de un formulario, no contaría. Segundo, la multiplicación operacionesTotales * 100.0 usa 100.0 y no 100: al ser double, promociona toda la expresión y evita la división entera que en el módulo 1 convertía 5/2 en 2. Con 100 obtendrías siempre 0 % o 100 %.

Solución 3

Nueva variable junto a las estadísticas:

String ultimoIsbn = "";

Y, dentro de la rama case "1", justo después de validar el ISBN y antes de pedir los días:

// --- Comprobacion de duplicado consecutivo ---
boolean continuar = true;

if (isbn.equals(ultimoIsbn)) {
    System.out.println("    AVISO: este ISBN es el mismo de la devolucion anterior.");

    String respuesta;
    do {
        System.out.print("    ¿Registrarla de todos modos? (si/no): ");
        respuesta = scanner.nextLine().trim();
        if (!respuesta.equalsIgnoreCase("si") && !respuesta.equalsIgnoreCase("no")) {
            System.out.println("      Responde 'si' o 'no'.");
        }
    } while (!respuesta.equalsIgnoreCase("si") && !respuesta.equalsIgnoreCase("no"));

    continuar = respuesta.equalsIgnoreCase("si");
}

if (!continuar) {
    System.out.println("    Devolucion descartada. Ninguna estadistica modificada.");
} else {

    // ... aqui va TODO el resto de la rama: pedir dias, calcular,
    //     actualizar estadisticas e imprimir el recibo ...

    ultimoIsbn = isbn;      // se recuerda para la proxima comprobacion
}

Sesión de ejemplo:

  Opcion: 1
  Empleado: Marta Ruiz
  Titulo del libro: Java Efectivo
  ISBN (978-XXXXXXXXXX): 978-0000000001
    AVISO: este ISBN es el mismo de la devolucion anterior.
    ¿Registrarla de todos modos? (si/no): quiza
      Responde 'si' o 'no'.
    ¿Registrarla de todos modos? (si/no): no
    Devolucion descartada. Ninguna estadistica modificada.

Claves de la solución:

  1. La comprobación se hace después de validar el ISBN (necesitas el valor limpio) y antes de pedir los días (para no molestar al usuario con más preguntas si va a descartar la operación).
  2. La comparación es isbn.equals(ultimoIsbn), nunca ==: es la regla de la lección 02-01. Y como ultimoIsbn se inicializa a "" y no a null, la llamada es segura desde la primera vuelta.
  3. La confirmación usa equalsIgnoreCase en un bucle de validación, así que SI, Si y si valen igual, y cualquier otra cosa vuelve a preguntar.
  4. ultimoIsbn = isbn; se asigna solo si la devolución se registra de verdad, dentro del else. Si lo pusieras fuera, una operación descartada contaminaría la siguiente comprobación.

Respuesta a la pregunta final. La comprobación solo detecta duplicados consecutivos porque ultimoIsbn es una única variable escalar: solo puede recordar un valor, el más reciente, y cada nueva devolución lo sobrescribe. Si el bibliotecario registra 978-0000000001, luego 978-0000000002 y después otra vez 978-0000000001, el tercer registro pasará sin aviso, porque el primero ya se ha olvidado.

Para detectar cualquier duplicado de la sesión necesitarías recordar todos los ISBN registrados, no solo el último. Eso exige una estructura capaz de almacenar un número indeterminado de valores y de responder rápido a la pregunta "¿está este ISBN ya dentro?". Esa estructura existe y se llama HashSet (lección 05-06), y su versión con datos asociados es HashMap (lección 05-05). Es, palabra por palabra, la carencia número 2 de la lista de limitaciones: el programa solo puede recordar un elemento a la vez.

Conclusión

Has construido BiblioTechApp 2.0, una aplicación de consola completa y funcional. Muestra un menú en bucle con while y lo despacha con un switch de flechas; valida cada entrada con bucles do-while que insisten hasta obtener un dato correcto, incluida una comprobación carácter a carácter que impide que un texto no numérico tumbe el programa; aplica las reglas de negocio de Nexus Software con la tabla de decisión del módulo —retraso saturado a cero, multa con tope, estado y gravedad—; acumula ocho estadísticas de sesión con contadores, acumuladores y el patrón del máximo con dato asociado; emite recibos y resúmenes alineados con printf; y se cierra de forma ordenada con una bandera booleana y un break colocado exactamente donde debe estar. Nada de esto existía hace seis lecciones.

Con esto cierras el módulo 2. Tu programa ya decide (if, else if, ternario, comparación correcta de String), repite (while, do-while, for, bucles anidados, contadores, acumuladores y banderas), selecciona (switch clásico y moderno, como sentencia y como expresión), controla el flujo dentro de los bucles (break, continue, etiquetas) y, cuando algo falla, sabes mirar dentro de él con trazas y con el depurador en lugar de adivinar.

Y sabes exactamente qué le falta, porque lo has enumerado con precisión: el estado son variables sueltas que deberían ser objetos, solo cabe un libro a la vez donde debería haber una lista, una entrada imprevista sigue pudiendo romperlo, al salir se pierde todo y main mide doscientas líneas que piden a gritos descomponerse en métodos.

En el módulo 3, Programación Orientada a Objetos, empieza la transformación más importante del curso: dejarás de pensar en variables y pasos, y empezarás a pensar en objetos que conocen sus datos y saben operar con ellos. Crearás las clases Libro, Empleado y Prestamo; les darás constructores para nacer siempre en un estado válido y métodos para encapsular las reglas de negocio que hoy están sueltas por main; descubrirás la herencia y el polimorfismo, el encapsulamiento y la abstracción; y aprenderás a redefinir toString, equals y hashCode para que tus objetos se impriman y se comparen como es debido. Al final de ese módulo, esas tres variables incómodas —mayorRetraso, empleadoMayorRetraso y libroMayorRetraso— serán un único objeto coherente, y BiblioTech empezará a parecerse de verdad a un sistema.

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