Aquesta és la lliçó integradora del mòdul. Tot el que has après a les cinc anteriors —condicionals, bucles, switch, break i continue, i el mètode per depurar quan alguna cosa no quadra— conflueix aquí en una única aplicació completa: la versió 2.0 de BiblioTechApp, el programa que Nexus Software farà servir al taulell de la seva biblioteca tècnica. Ja no acaba després d'un sol préstec: mostra un menú, atén operacions una rere l'altra, exigeix dades correctes abans d'acceptar-les, acumula estadístiques de tota la sessió i s'acomiada amb un tancament ordenat quan el bibliotecari decideix marxar. No l'escriurem de cop: el construirem en cinc iteracions, cadascuna afegint una capa sobre l'anterior, exactament com es desenvolupa programari real. I acabarem amb la part més honesta de totes: la llista del que aquest programa continua sense poder fer, amb el mòdul del curs que resol cada mancança.

Contingut

  1. L'especificació completa
  2. Iteració 1: l'esquelet del menú
  3. Iteració 2: la validació de totes les entrades
  4. Iteració 3: la lògica de la devolució
  5. Iteració 4: les estadístiques de la sessió
  6. Iteració 5: el poliment de la sortida
  7. El codi complet final, comentat
  8. Compilar i executar
  9. Una sessió de prova completa
  10. Limitacions: què no pot fer i quin mòdul ho resoldrà
  11. Errors Habituals i Consells
  12. Exercicis

  1. L'especificació completa

Com a la lliçó 01-07, s'escriu primer què ha de fer el programa i només després com.

Nom: BiblioTechApp (versió 2.0) Paquet: com.nexussoftware.bibliotech

Comportament esperat:

  1. Mostrar una capçalera amb l'empresa, el nom i la versió de l'aplicació.
  2. Mostrar en bucle un menú amb quatre opcions i no acabar fins que l'usuari triï sortir:
1. Registrar devolucio
2. Simular prestec
3. Veure resum de la sessio
0. Sortir
  1. Opció 1 (registrar devolució): demanar empleat, títol, ISBN i dies transcorreguts; validar cada dada; calcular el retard, la multa amb sostre, l'estat i la gravetat; emetre un rebut formatat amb una referència única.
  2. Opció 2 (simular préstec): demanar empleat i títol; informar del termini, de la tarifa que s'aplicarà i de quan s'assoliria el sostre.
  3. Opció 3 (resum de la sessió): mostrar els acumulats de tot el que s'ha fet des que va arrencar el programa.
  4. Opció 0 (sortir): imprimir un tancament amb el total recaptat i acabar.
  5. Qualsevol altra entrada ha de mostrar un avís i tornar al menú sense trencar res.

Regles de negoci, les mateixes des del mòdul 1:

Regla Valor Constant
Durada estàndard del préstec 15 dies DIES_PRESTEC
Tarifa per dia de retard 0,25 € TARIFA_DIARIA
Multa màxima per devolució 20,00 € MULTA_MAXIMA
Llindar de retard lleu 7 dies LLINDAR_LLEU

Regles de validació:

Dada Condició per acceptar-la
Empleat Almenys 3 caràcters després de trim()
Títol No buit ni en blanc
ISBN Comença per 978 i té exactament 14 caràcters
Dies transcorreguts Només dígits, i valor entre 0 i 3650

Estadístiques de la sessió (totes en variables escalars, que és l'únic que tens):

Variable Què guarda
devolucionsRegistrades Comptador de devolucions processades
devolucionsAmbRetard Quantes d'elles van arribar tard
sostresAssolits Quantes van assolir MULTA_MAXIMA
prestecsSimulats Comptador de préstecs
totalMultes Acumulador d'imports
retardMesGran Màxim de dies de retard vist
empleatRetardMesGran Nom associat a aquest màxim
llibreRetardMesGran Títol associat a aquest màxim

Restriccions tècniques: tot dins del main, sense classes pròpies, sense arrays ni col·leccions, sense excepcions. Només el que s'ha après fins aquí.

El flux complet del programa:

flowchart TD
    INI["Capcalera de benvinguda<br/>i inicialitzacio de comptadors"] --> MENU["Mostrar menu"]
    MENU --> LEER["Llegir opcio amb nextLine()"]
    LEER --> SW{"switch (opcio)"}

    SW -- "1" --> V1["Validar empleat, titol,<br/>ISBN i dies (bucles do-while)"]
    V1 --> C1["Calcular retard, multa,<br/>estat i gravetat"]
    C1 --> E1["Actualitzar estadistiques"]
    E1 --> R1["Imprimir rebut"] --> MENU

    SW -- "2" --> V2["Validar empleat i titol"]
    V2 --> R2["Informar del termini<br/>i la tarifa"] --> MENU

    SW -- "3" --> R3["Imprimir resum<br/>de la sessio"] --> MENU

    SW -- "altra" --> ERR["Avis: opcio no reconeguda"] --> MENU

    SW -- "0" --> FIN["sortir = true"]
    FIN --> CIERRE["Tancament amb total recaptat<br/>i scanner.close()"]

  1. Iteració 1: l'esquelet del menú

Primer, l'armadura: mostrar el menú, llegir una opció, despatxar-la i repetir. Sense lògica de negoci, només missatges de lloc.

package com.nexussoftware.bibliotech;

import java.util.Scanner;

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

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

        while (true) {

            System.out.println();
            System.out.println("  1. Registrar devolucio");
            System.out.println("  2. Simular prestec");
            System.out.println("  3. Veure resum de la sessio");
            System.out.println("  0. Sortir");
            System.out.print("  Opcio: ");

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

            switch (opcio) {
                case "1" -> System.out.println("  [registrar devolucio]");
                case "2" -> System.out.println("  [simular prestec]");
                case "3" -> System.out.println("  [resum de la sessio]");
                case "0" -> sortir = true;
                default  -> System.out.println("  Opcio no reconeguda.");
            }

            if (sortir) {
                break;
            }
        }

        System.out.println("Sessio tancada.");
        scanner.close();
    }
}

Tres decisions de disseny que convé entendre abans de continuar, perquè condicionen tota la resta:

1. L'opció es llegeix com a String, no com a int. Si fessis servir Integer.parseInt directament i l'usuari escrivís hola, el programa s'aturaria amb un NumberFormatException. Llegint-la com a text, qualsevol entrada rara cau netament al default.

2. La sortida es controla amb la bandera sortir i un break situat al cos del bucle, no dins del switch. Aquesta és l'aplicació directa del parany que vas estudiar a la lliçó 02-04: un break escrit dins d'una branca del switch —fins i tot amb la sintaxi de fletxes, on case "0" -> { … break; } compila perfectament— sortiria del switch i no del while, i el menú seria impossible d'abandonar. En posar if (sortir) { break; } després del switch, el break és directament al cos del bucle i no hi ha cap ambigüitat.

3. El switch fa servir fletxes. Sense break entre branques, sense fall-through possible, i amb les claus delimitant l'àmbit de cada branca, de manera que les variables declarades a l'opció 1 no xoquen amb les de l'opció 2.

Prova-ho abans de continuar: ha de recórrer el menú indefinidament, avisar de les opcions desconegudes i tancar-se amb 0. Si no es tanca, revisa el punt 2.

  1. Iteració 2: la validació de totes les entrades

Ara s'omplen els buits amb la lectura de dades, aplicant el bucle de validació de la lliçó 02-02: preguntar, comprovar, i tornar a preguntar mentre la dada no serveixi.

String empleat;
do {
    System.out.print("  Empleat: ");
    empleat = scanner.nextLine().trim();
    if (empleat.length() < 3) {
        System.out.println("    El nom ha de tenir almenys 3 caracters.");
    }
} while (empleat.length() < 3);

El cas interessant és el número. Recorda que ni un if ni un bucle poden evitar que Integer.parseInt("vint") aturi el programa: la fallada passa dins de la conversió. Però sí que pots comprovar abans que la cadena està formada només per dígits, recorrent-la caràcter a caràcter amb un for i charAt:

int diesTranscorreguts = 0;
boolean diesValids = false;

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

    // Comprovacio previa: son tots els caracters digits?
    boolean nomesDigits = !entrada.isEmpty();
    for (int i = 0; i < entrada.length(); i++) {
        if (!Character.isDigit(entrada.charAt(i))) {
            nomesDigits = false;
            break;                  // tan bon punt en falla un, no cal continuar
        }
    }

    if (!nomesDigits) {
        System.out.println("    Escriu un numero enter sense signes ni lletres.");
        continue;                   // descarta aquesta volta i torna a preguntar
    }

    // Aqui ja es segur convertir: parseInt no pot fallar.
    diesTranscorreguts = Integer.parseInt(entrada);
    diesValids = diesTranscorreguts <= 3650;
    if (!diesValids) {
        System.out.println("    Valor fora de rang.");
    }

} while (!diesValids);

Aquest fragment condensa mig mòdul: un do-while de validació, un for de recorregut, un break que talla la comprovació tan bon punt troba un caràcter no vàlid, un continue que descarta la iteració quan l'entrada no és numèrica i una bandera booleana que governa la condició del bucle exterior.

I observa un efecte secundari elegant: com que el signe - no és un dígit, els dies negatius queden rebutjats automàticament per la mateixa comprovació. Aquella promesa de la lliçó 02-01 —"el programa ja no accepta dies negatius"— queda complerta sense una sola condició addicional.

Aquesta comprovació prèvia és una defensa artesanal. Al mòdul 6 la substituiràs per un try-catch, que és la manera correcta i general de tractar entrades mal formades.

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

Amb les dades ja garantides vàlides, s'apliquen les regles de negoci del mòdul 1 i l'escala de decisió de la lliçó 02-01:

// 1. Retard saturat a zero: mai negatiu.
int diesRetard = diesTranscorreguts - DIES_PRESTEC;
if (diesRetard < 0) {
    diesRetard = 0;
}

// 2. Descomposicio en setmanes i dies solts (divisio entera i modul).
int setmanesRetard = diesRetard / 7;
int diesSolts = diesRetard % 7;

// 3. Multa amb sostre.
double multa = diesRetard * TARIFA_DIARIA;
boolean ambSostre = multa >= MULTA_MAXIMA;
if (ambSostre) {
    multa = MULTA_MAXIMA;
}

// 4. Estat i gravetat: la taula de decisio de la llico 02-01.
String estat;
String gravetat;
if (diesRetard == 0) {
    estat = "EN TERMINI";
    gravetat = "CAP";
} else {
    estat = "AMB RETARD";
    if (ambSostre) {
        gravetat = "MAXIMA";
    } else if (diesRetard <= LLINDAR_LLEU) {
        gravetat = "LLEU";
    } else {
        gravetat = "GREU";
    }
}

Fixa't en l'ordre de l'escala interna: el sostre es comprova abans que el llindar lleu, perquè una multa amb sostre és sempre la classificació prioritària, independentment dels dies. És exactament la taula de decisió que vas construir a la lliçó 02-01, i aquí es veu per què valia la pena escriure-la abans que el codi.

La referència del rebut es compon amb mètodes de String del mòdul 1 i el comptador de la sessió, de manera que cada rebut de la sessió sigui únic:

String referencia = "DEV-" + isbn.substring(isbn.length() - 4)
                  + "-" + (devolucionsRegistrades + 1);
// Amb ISBN 978-0000000001 i primera devolucio -> DEV-0001-1

  1. Iteració 4: les estadístiques de la sessió

Aquí entren els patrons d'acumulació de la lliçó 02-02: comptador, acumulador i màxim amb dada associada. Totes aquestes variables es declaren abans del while, perquè han de sobreviure a totes les voltes del menú; si fossin dins del bucle, es reiniciarien a cada operació (que és justament l'error que vas depurar a la lliçó 02-05).

// FORA del bucle del menu: sobreviuen a tota la sessio.
int devolucionsRegistrades  = 0;
int devolucionsAmbRetard    = 0;
int sostresAssolits         = 0;
int prestecsSimulats        = 0;
double totalMultes          = 0.0;
int retardMesGran           = 0;
String empleatRetardMesGran = "-";
String llibreRetardMesGran  = "-";

I l'actualització, dins de la branca de la devolució, després de calcular:

devolucionsRegistrades++;                  // comptador
totalMultes += multa;                      // acumulador

if (diesRetard > 0) {
    devolucionsAmbRetard++;
}
if (ambSostre) {
    sostresAssolits++;
}

// Maxim AMB dada associada: s'actualitzen les tres variables alhora.
if (diesRetard > retardMesGran) {
    retardMesGran = diesRetard;
    empleatRetardMesGran = empleat;
    llibreRetardMesGran = titol;
}

El patró del màxim amb dada associada mereix atenció: quan trobes un rècord nou, has d'actualitzar totes les variables que el descriuen al mateix if. Oblidar-ne una produeix un resum incoherent ("retard més gran: 105 dies, empleat: Marta Ruiz" quan en realitat va ser Diego Alonso). I aquesta incomoditat —tres variables soltes que en realitat són una sola dada— és l'argument exacte que obre el mòdul 3: el natural seria un objecte amb tres camps.

Al resum cal protegir-se de la divisió per zero, tal com vas aprendre depurant:

double multaMitjana;
if (devolucionsRegistrades == 0) {
    multaMitjana = 0.0;                    // sense devolucions no hi ha mitjana
} else {
    multaMitjana = totalMultes / devolucionsRegistrades;
}

  1. Iteració 5: el poliment de la sortida

L'última iteració no afegeix funcionalitat: fa que el programa sembli professional. Les tècniques són les del mòdul 1, printf i String.repeat, aplicades amb criteri.

  • Amplades fixes amb %-18s perquè les etiquetes quedin alineades en columna.
  • %.2f per a tots els imports. Un rebut que digui 3.0 EUR no és acceptable.
  • Separadors amb "=".repeat(AMPLADA) i una constant AMPLADA per no repetir el número màgic arreu.
  • Sagnat per nivells: el menú a dos espais, les dades d'un formulari a quatre. Ajuda a entendre la jerarquia del que s'està llegint sense cap adorn gràfic.
  • Un avís destacat només quan s'assoleix el sostre, en lloc de repetir informació irrellevant a cada rebut.

  1. El codi complet final, comentat

package com.nexussoftware.bibliotech;

import java.util.Scanner;

/**
 * BiblioTech 2.0 - Gestio de la biblioteca tecnica de Nexus Software.
 *
 * Versio del modul 2 (Flux de control): menu interactiu amb validacio
 * d'entrades i estadistiques de sessio. Tot el codi viu a main i fa servir
 * unicament variables escalars, condicionals i bucles.
 */
public class BiblioTechApp {

    public static void main(String[] args) {

        // ===== CONSTANTS: regles de negoci i configuracio =====
        final String NOM_EMPRESA   = "Nexus Software";
        final String VERSIO        = "2.0 (modul 2)";
        final int    DIES_PRESTEC  = 15;
        final double TARIFA_DIARIA = 0.25;
        final double MULTA_MAXIMA  = 20.0;
        final int    LLINDAR_LLEU  = 7;
        final int    DIES_MAXIMS   = 3650;
        final int    AMPLADA       = 52;

        Scanner scanner = new Scanner(System.in);

        // ===== ESTADISTIQUES DE LA SESSIO =====
        // Declarades FORA del bucle: han de sobreviure a totes les operacions.
        int    devolucionsRegistrades = 0;
        int    devolucionsAmbRetard   = 0;
        int    sostresAssolits        = 0;
        int    prestecsSimulats       = 0;
        double totalMultes            = 0.0;
        int    retardMesGran          = 0;
        String empleatRetardMesGran   = "-";
        String llibreRetardMesGran    = "-";

        // ===== CAPCALERA =====
        System.out.println("=".repeat(AMPLADA));
        System.out.println("  BIBLIOTECH " + VERSIO);
        System.out.println("  Biblioteca tecnica de " + NOM_EMPRESA);
        System.out.println("=".repeat(AMPLADA));

        boolean sortir = false;

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

            System.out.println();
            System.out.println("-".repeat(AMPLADA));
            System.out.println("  MENU PRINCIPAL");
            System.out.println("-".repeat(AMPLADA));
            System.out.println("  1. Registrar devolucio");
            System.out.println("  2. Simular prestec");
            System.out.println("  3. Veure resum de la sessio");
            System.out.println("  0. Sortir");
            System.out.print("  Opcio: ");

            // Es llegeix com a text: qualsevol entrada rara cau al default.
            String opcio = scanner.nextLine().trim();

            switch (opcio) {

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

                    // Validacio del nom de l'empleat
                    String empleat;
                    do {
                        System.out.print("  Empleat: ");
                        empleat = scanner.nextLine().trim();
                        if (empleat.length() < 3) {
                            System.out.println("    El nom ha de tenir almenys 3 caracters.");
                        }
                    } while (empleat.length() < 3);

                    // Validacio del titol
                    String titol;
                    do {
                        System.out.print("  Titol del llibre: ");
                        titol = scanner.nextLine().trim();
                        if (titol.isBlank()) {
                            System.out.println("    El titol no pot estar buit.");
                        }
                    } while (titol.isBlank());

                    // Validacio de l'ISBN: prefix 978 i 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 valid per a aquesta biblioteca.");
                        }
                    } while (!isbn.startsWith("978") || isbn.length() != 14);

                    // Validacio dels dies: primer que siguin digits, despres el rang
                    int diesTranscorreguts = 0;
                    boolean diesValids = false;
                    do {
                        System.out.print("  Dies transcorreguts (0-" + DIES_MAXIMS + "): ");
                        String entrada = scanner.nextLine().trim();

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

                        if (!nomesDigits) {
                            System.out.println("    Escriu un numero enter sense signes ni lletres.");
                            continue;       // torna a preguntar
                        }

                        diesTranscorreguts = Integer.parseInt(entrada);
                        diesValids = diesTranscorreguts <= DIES_MAXIMS;
                        if (!diesValids) {
                            System.out.println("    Valor fora de rang.");
                        }
                    } while (!diesValids);

                    // ----- Regles de negoci -----
                    int diesRetard = diesTranscorreguts - DIES_PRESTEC;
                    if (diesRetard < 0) {
                        diesRetard = 0;                  // saturat a zero
                    }

                    int setmanesRetard = diesRetard / 7;
                    int diesSolts      = diesRetard % 7;

                    double multa = diesRetard * TARIFA_DIARIA;
                    boolean ambSostre = multa >= MULTA_MAXIMA;
                    if (ambSostre) {
                        multa = MULTA_MAXIMA;            // sostre aplicat
                    }

                    String estat;
                    String gravetat;
                    if (diesRetard == 0) {
                        estat = "EN TERMINI";
                        gravetat = "CAP";
                    } else {
                        estat = "AMB RETARD";
                        if (ambSostre) {
                            gravetat = "MAXIMA";         // el sostre mana sobre els dies
                        } else if (diesRetard <= LLINDAR_LLEU) {
                            gravetat = "LLEU";
                        } else {
                            gravetat = "GREU";
                        }
                    }

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

                    // ----- Actualitzacio de les estadistiques de sessio -----
                    devolucionsRegistrades++;
                    totalMultes += multa;
                    if (diesRetard > 0) {
                        devolucionsAmbRetard++;
                    }
                    if (ambSostre) {
                        sostresAssolits++;
                    }
                    if (diesRetard > retardMesGran) {
                        retardMesGran        = diesRetard;
                        empleatRetardMesGran = empleat;
                        llibreRetardMesGran  = titol;
                    }

                    // ----- Rebut -----
                    System.out.println();
                    System.out.println("  " + "=".repeat(AMPLADA - 4));
                    System.out.println("    REBUT DE DEVOLUCIO  " + referencia);
                    System.out.println("  " + "=".repeat(AMPLADA - 4));
                    System.out.printf("    %-18s %s%n", "Empleat:", empleat);
                    System.out.printf("    %-18s %s%n", "Llibre:", titol);
                    System.out.printf("    %-18s %s%n", "ISBN:", isbn);
                    System.out.printf("    %-18s %d%n", "Dies prestat:", diesTranscorreguts);
                    System.out.printf("    %-18s %d (%d set. i %d dies)%n",
                                      "Dies de retard:", diesRetard,
                                      setmanesRetard, diesSolts);
                    System.out.printf("    %-18s %s / %s%n", "Estat:", estat, gravetat);
                    System.out.printf("    %-18s %.2f EUR%n", "Multa:", multa);
                    if (ambSostre) {
                        System.out.println("    AVIS: multa amb sostre. Notificar a RRHH.");
                    }
                    System.out.println("  " + "=".repeat(AMPLADA - 4));
                }

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

                    String empleat;
                    do {
                        System.out.print("  Empleat: ");
                        empleat = scanner.nextLine().trim();
                        if (empleat.length() < 3) {
                            System.out.println("    El nom ha de tenir almenys 3 caracters.");
                        }
                    } while (empleat.length() < 3);

                    String titol;
                    do {
                        System.out.print("  Titol del llibre: ");
                        titol = scanner.nextLine().trim();
                        if (titol.isBlank()) {
                            System.out.println("    El titol no pot estar buit.");
                        }
                    } while (titol.isBlank());

                    prestecsSimulats++;

                    System.out.println();
                    System.out.printf("    Prestec #%d autoritzat.%n", prestecsSimulats);
                    System.out.printf("    %s -> %s%n", titol, empleat);
                    System.out.printf("    Termini: %d dies. A partir del dia %d s'apliquen "
                                    + "%.2f EUR/dia.%n",
                                      DIES_PRESTEC, DIES_PRESTEC + 1, TARIFA_DIARIA);
                    System.out.printf("    El sostre de %.2f EUR s'assoleix als %d dies "
                                    + "de retard.%n",
                                      MULTA_MAXIMA,
                                      (int) Math.ceil(MULTA_MAXIMA / TARIFA_DIARIA));
                }

                // ---------- OPCIO 3: RESUM DE LA SESSIO ----------
                case "3" -> {
                    // Guarda contra la divisio per zero.
                    double multaMitjana;
                    if (devolucionsRegistrades == 0) {
                        multaMitjana = 0.0;
                    } else {
                        multaMitjana = totalMultes / devolucionsRegistrades;
                    }

                    System.out.println();
                    System.out.println("  " + "=".repeat(AMPLADA - 4));
                    System.out.println("    RESUM DE LA SESSIO");
                    System.out.println("  " + "=".repeat(AMPLADA - 4));
                    System.out.printf("    %-26s %d%n", "Devolucions registrades:",
                                      devolucionsRegistrades);
                    System.out.printf("    %-26s %d%n", "  amb retard:",
                                      devolucionsAmbRetard);
                    System.out.printf("    %-26s %d%n", "  amb multa amb sostre:",
                                      sostresAssolits);
                    System.out.printf("    %-26s %d%n", "Prestecs simulats:",
                                      prestecsSimulats);
                    System.out.printf("    %-26s %.2f EUR%n", "Total de multes:", totalMultes);
                    System.out.printf("    %-26s %.2f EUR%n", "Multa mitjana:", multaMitjana);

                    if (retardMesGran > 0) {
                        System.out.printf("    %-26s %d dies%n", "Retard mes gran:", retardMesGran);
                        System.out.printf("    %-26s %s%n", "  empleat:", empleatRetardMesGran);
                        System.out.printf("    %-26s %s%n", "  llibre:", llibreRetardMesGran);
                    } else {
                        System.out.printf("    %-26s cap%n", "Retard mes gran:");
                    }
                    System.out.println("  " + "=".repeat(AMPLADA - 4));
                }

                // ---------- OPCIO 0: SORTIR ----------
                // Nomes aixeca la bandera: el break va DESPRES del switch.
                case "0" -> sortir = true;

                // ---------- QUALSEVOL ALTRA ENTRADA ----------
                default -> System.out.println("  Opcio no reconeguda. Fes servir 0, 1, 2 o 3.");
            }

            // El break es al cos del bucle, no dins del switch:
            // aixi trenca el WHILE i no el SWITCH (llico 02-04).
            if (sortir) {
                break;
            }
        }

        // ===== TANCAMENT =====
        System.out.println();
        System.out.println("=".repeat(AMPLADA));
        System.out.printf("  Sessio tancada. %d devolucions, %.2f EUR recaptats.%n",
                          devolucionsRegistrades, totalMultes);
        System.out.println("  Gracies per fer servir BiblioTech - " + NOM_EMPRESA);
        System.out.println("=".repeat(AMPLADA));

        scanner.close();
    }
}

  1. Compilar i executar

Des de l'arrel del projecte bibliotech que vas crear al mòdul 1:

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

Des de l'IDE, n'hi ha prou amb el botó d'executar sobre la classe. I si alguna cosa no quadra, ja saps què fer: punt d'interrupció a la línia de l'acumulador, condició devolucionsRegistrades == 2, i a mirar la finestra de variables.

  1. Una sessió de prova completa

Aquesta és la taula de prova del programa: cada línia és una entrada de l'usuari i el que ha de passar. Executa-la tal qual i compara.

# Entrada de l'usuari Resposta esperada
1 1 Entra al formulari de devolució
2 Marta Ruiz Acceptat
3 Java Eficac Acceptat
4 978-0000000001 Acceptat
5 vint Rebutjat: "Escriu un numero enter sense signes ni lletres."
6 27 Acceptat. Rebut DEV-0001-1: 12 dies de retard, 3,00 €, AMB RETARD / GREU
7 1 Segon formulari de devolució
8 Diego Alonso Acceptat
9 Refactoritzacio Acceptat
10 978-0000000003 Acceptat
11 -5 Rebutjat: el signe - no és un dígit
12 120 Acceptat. Rebut DEV-0003-2: 105 dies, 20,00 €, MAXIMA, amb avís a RRHH
13 2 Formulari de préstec
14 Nuria Vidal Acceptat
15 Patrons de Disseny Préstec #1 autoritzat, termini 15 dies, sostre als 80 dies
16 9 "Opcio no reconeguda. Fes servir 0, 1, 2 o 3."
17 3 Resum: 2 devolucions (2 amb retard, 1 amb sostre), 1 préstec, 23,00 € totals, 11,50 € de mitjana, retard més gran 105 dies de Diego Alonso amb "Refactoritzacio"
18 0 Tancament: "Sessio tancada. 2 devolucions, 23,00 EUR recaptats."

Els trams més significatius de la sortida real:

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

----------------------------------------------------
  MENU PRINCIPAL
----------------------------------------------------
  1. Registrar devolucio
  2. Simular prestec
  3. Veure resum de la sessio
  0. Sortir
  Opcio: 1

  --- REGISTRAR DEVOLUCIO ---
  Empleat: Marta Ruiz
  Titol del llibre: Java Eficac
  ISBN (978-XXXXXXXXXX): 978-0000000001
  Dies transcorreguts (0-3650): vint
    Escriu un numero enter sense signes ni lletres.
  Dies transcorreguts (0-3650): 27

  ================================================
    REBUT DE DEVOLUCIO  DEV-0001-1
  ================================================
    Empleat:           Marta Ruiz
    Llibre:            Java Eficac
    ISBN:              978-0000000001
    Dies prestat:      27
    Dies de retard:    12 (1 set. i 5 dies)
    Estat:             AMB RETARD / GREU
    Multa:             3,00 EUR
  ================================================

El segon rebut, amb el sostre aplicat:

  ================================================
    REBUT DE DEVOLUCIO  DEV-0003-2
  ================================================
    Empleat:           Diego Alonso
    Llibre:            Refactoritzacio
    ISBN:              978-0000000003
    Dies prestat:      120
    Dies de retard:    105 (15 set. i 0 dies)
    Estat:             AMB RETARD / MAXIMA
    Multa:             20,00 EUR
    AVIS: multa amb sostre. Notificar a RRHH.
  ================================================

El resum de la sessió i el tancament:

  ================================================
    RESUM DE LA SESSIO
  ================================================
    Devolucions registrades:   2
      amb retard:              2
      amb multa amb sostre:    1
    Prestecs simulats:         1
    Total de multes:           23,00 EUR
    Multa mitjana:             11,50 EUR
    Retard mes gran:           105 dies
      empleat:                 Diego Alonso
      llibre:                  Refactoritzacio
  ================================================

====================================================
  Sessio tancada. 2 devolucions, 23,00 EUR recaptats.
  Gracies per fer servir BiblioTech - Nexus Software
====================================================

Verificació manual de les xifres, que és el pas 2 del mètode de depuració: la devolució de la Marta Ruiz són 27 − 15 = 12 dies × 0,25 = 3,00 €; la del Diego Alonso, 120 − 15 = 105 dies × 0,25 = 26,25 €, amb sostre a 20,00 €. Total 23,00 €, mitjana 11,50 €. Coincideix.

A més, prova aquests casos límit, que és on viuen els errors:

Cas límit Comportament correcte
Opció 3 tot just arrencar Tot a zero, "Retard mes gran: cap", sense dividir per zero
0 dies transcorreguts 0 de retard, EN TERMINI / CAP, 0,00 €
15 dies exactes 0 de retard (el dia 15 encara és en termini)
16 dies 1 dia de retard, 0,25 €, LLEU
22 dies 7 de retard: LLEU (el llindar és <=)
23 dies 8 de retard: GREU
95 dies 80 de retard, 20,00 € exactes: MAXIMA
Prémer Retorn sense escriure res Cau al default o repeteix la pregunta, mai no trenca

  1. Limitacions: què no pot fer i quin mòdul ho resoldrà

Aquesta secció és tan important com el codi. Un bon desenvolupador coneix les mancances del que ha construït i sap per què existeixen.

1. Tot l'estat són variables soltes. empleatRetardMesGran, llibreRetardMesGran i retardMesGran són tres variables independents que en realitat descriuen una sola cosa. Res no impedeix actualitzar-ne una i oblidar les altres dues, i el compilador no et pot ajudar. El natural seria un objecte Devolucio amb els seus camps empleat, llibre i diesRetard agrupats i coherents. → Mòdul 3: Programació Orientada a Objectes, on crearàs les classes Llibre, Empleat i Prestec.

2. Només hi cap un llibre alhora. Cada devolució sobreescriu l'anterior. No pots llistar el que s'ha processat, ni cercar un ISBN concret, ni ordenar per multa, ni consultar un catàleg real: el "catàleg" són tres línies escrites a mà. Necessites estructures que guardin molts valors sota un sol nom. → Mòdul 5: Estructures de dades i col·leccions, amb arrays, ArrayList i HashMap.

3. Una entrada mal formada encara pot trencar el programa. La comprovació de dígits és artesanal i només cobreix el cas que vas anticipar. Un Integer.parseInt d'un número de vint xifres desborda el rang d'int, i qualsevol altra operació imprevista atura l'aplicació amb una traça de pila. Cal un mecanisme general per tractar l'excepcional. → Mòdul 6: Gestió d'excepcions, amb try-catch, excepcions pròpies i try-with-resources (que a més tancarà el Scanner correctament).

4. En sortir es perd tot. El resum de la sessió desapareix amb el procés. Demà el bibliotecari comença de zero: ni multes pendents, ni històric, ni catàleg persistent. Cal desar a disc i tornar a llegir. → Mòdul 7: Entrada/sortida de fitxers, amb lectura i escriptura, CSV i Properties.

5. main fa més de dues-centes línies. Cada branca del switch és un bloc enorme i els formularis de les opcions 1 i 2 repeteixen literalment el mateix bucle de validació de l'empleat i del títol. Duplicar codi és duplicar els llocs on corregir una fallada. → Lliçó 03-03: Mètodes, que et permetrà escriure llegirTextValid("Empleat: ", 3) una vegada i cridar-lo quatre.

6. No hi ha proves automàtiques. Cada canvi obliga a repetir la sessió de prova a mà, teclejant divuit entrades. És lent i acabes no fent-ho. → Mòdul 11: JUnit, i les tècniques de qualitat del mòdul 12.

7. És d'un sol usuari, local i sense interfície. Un bibliotecari, una consola, un ordinador. → Mòdul 9 (Xarxes) i mòdul 12 (Aplicació web).

Cap d'aquestes limitacions no és una fallada del programa: són les fronteres exactes del que es pot construir amb flux de control i variables escalars. I cada mòdul restant del curs està dissenyat per enderrocar-ne una.

Errors Habituals i Consells

1. Declarar les estadístiques dins del bucle del menú. Es reinicien a cada operació i el resum sempre mostra les dades de l'última. És l'error que vas depurar a la lliçó 02-05, i en aquest programa és especialment fàcil de cometre.

2. Posar el break de sortida dins del switch. El menú no es tanca mai. Recorda que a la sintaxi de fletxes case "0" -> { … break; } compila i fa exactament el que no vols.

3. Llegir l'opció amb nextInt(). Reintrodueix el problema del salt de línia pendent que vas resoldre al mòdul 1 i, a més, trenca el programa davant de qualsevol text. La convenció del curs és ferma: sempre nextLine() i conversió explícita.

4. Validar després de calcular. Si converteixes a número abans de comprovar que la cadena és numèrica, la comprovació arriba tard. L'ordre és: llegir → comprovar → convertir → fer servir.

5. Oblidar trim(). Un espai final converteix "1 " en una opció no reconeguda i a l'usuari li semblarà que el programa està trencat.

6. Actualitzar el màxim a mitges. Si actualitzes retardMesGran però no empleatRetardMesGran, el resum mentirà. Mantén les tres assignacions juntes al mateix if i amb el mateix sagnat, perquè salti a la vista si en falta una.

7. Tancar el Scanner dins del bucle. Un scanner.close() a la branca de sortir, abans del break, funciona; dins d'una altra branca, deixa el programa sense entrada per a la volta següent i tot falla a partir d'aquí. Tanca'l una sola vegada, al final del main.

8. Consell: prova cada iteració abans d'escriure la següent. És la raó de construir en cinc passos. Si el menú no es tanca, arregla-ho abans d'afegir-hi la validació; si no, tindràs dos errors entrellaçats i no sabràs quin és quin.

9. Consell: extreu els números màgics a constants. AMPLADA, DIES_MAXIMS i les quatre regles de negoci són a dalt de tot, amb nom. Canviar la tarifa és tocar una línia, no buscar 0.25 per tot el fitxer.

10. Consell: desa aquesta versió a Git abans de tocar-la. git add i git commit -m "BiblioTech 2.0: menu interactiu" et donen un punt de retorn segur per fer els exercicis sense por.

Exercicis

Exercici 1: nova opció "4. Consultar taula de multes"

Afegeix al menú una quarta opció que mostri l'escala de multes de BiblioTech sense demanar cap dada: una taula amb els dies de retard de 5 en 5 fins a 90, mostrant la multa sense sostre, la multa aplicada i la gravetat (LLEU, GREU o MAXIMA). S'ha de generar amb un bucle for i fer servir les constants existents, sense números escrits a mà.

Requisits:

  • L'opció ha d'aparèixer al menú i al missatge del default.
  • La taula ha d'estar alineada amb printf.
  • Marca amb un <-- la primera fila en què s'assoleix el sostre, i només aquesta (pista: una bandera booleana).

Exercici 2: comptador d'opcions invàlides i de temps de sessió

Afegeix dues estadístiques més al resum i al tancament:

  1. opcionsInvalides: quantes vegades l'usuari ha introduït alguna cosa que ha caigut al default.
  2. operacionsTotals: la suma de devolucions, préstecs i consultes de resum (és a dir, totes les opcions vàlides diferents de sortir).

Mostra-les totes dues a l'opció 3 i al tancament. Afegeix-hi també una línia que calculi el percentatge d'encert de l'usuari: operacionsTotals * 100.0 / (operacionsTotals + opcionsInvalides), protegint-te de la divisió per zero quan encara no s'hagi fet res.

Exercici 3: rebutjar devolucions duplicades a la mateixa sessió

Amb el que tens (variables escalars, sense col·leccions), implementa una comprovació mínima: desa l'ISBN de l'última devolució registrada en una variable String ultimIsbn, inicialitzada a "". Si l'usuari intenta registrar una devolució amb el mateix ISBN dues vegades seguides, mostra un avís i pregunta si vol continuar igualment (si/no, validat en bucle i insensible a majúscules). Si respon que no, es descarta l'operació i es torna al menú sense tocar cap estadística.

En acabar, respon per escrit: per què aquesta comprovació només detecta duplicats consecutius i què necessitaries per detectar qualsevol duplicat de la sessió?

Solucions

Solució 1

S'afegeix la línia al menú, s'amplia el missatge del default i s'introdueix la nova branca al switch:

System.out.println("  4. Consultar taula de multes");
// ---------- OPCIO 4: TAULA DE MULTES ----------
case "4" -> {
    System.out.println();
    System.out.println("  " + "=".repeat(AMPLADA - 4));
    System.out.println("    ESCALA DE MULTES DE BIBLIOTECH");
    System.out.println("  " + "=".repeat(AMPLADA - 4));
    System.out.printf("    %6s %12s %12s  %s%n",
                      "DIES", "SENSE SOSTRE", "APLICADA", "GRAVETAT");

    boolean sostreJaMarcat = false;     // bandera: nomes es marca la primera fila

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

        double senseSostre = dies * TARIFA_DIARIA;
        double aplicada = senseSostre;
        String gravetat;
        String marca = "";

        if (senseSostre >= MULTA_MAXIMA) {
            aplicada = MULTA_MAXIMA;
            gravetat = "MAXIMA";
            if (!sostreJaMarcat) {
                marca = "  <--";        // nomes la PRIMERA vegada
                sostreJaMarcat = true;
            }
        } else if (dies <= LLINDAR_LLEU) {
            gravetat = "LLEU";
        } else {
            gravetat = "GREU";
        }

        System.out.printf("    %6d %12.2f %12.2f  %-8s%s%n",
                          dies, senseSostre, aplicada, gravetat, marca);
    }

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

I el default actualitzat:

default -> System.out.println("  Opcio no reconeguda. Fes servir 0, 1, 2, 3 o 4.");

Sortida (fragment):

    ESCALA DE MULTES DE BIBLIOTECH
  ================================================
      DIES SENSE SOSTRE     APLICADA  GRAVETAT
         5         1,25         1,25  LLEU
        10         2,50         2,50  GREU
        15         3,75         3,75  GREU
        ...
        75        18,75        18,75  GREU
        80        20,00        20,00  MAXIMA    <--
        85        21,25        20,00  MAXIMA
        90        22,50        20,00  MAXIMA

Punts clau: la bandera sostreJaMarcat és el patró de la lliçó 02-02 (un cop aixecada no es torna a abaixar), i marca s'inicialitza a cadena buida perquè el printf funcioni igual a totes les files sense necessitat de dos printf diferents. Fixa't també que la fila de 5 dies és LLEU i la de 10 ja és GREU: el llindar és a 7, entremig.

Solució 2

Variables noves, declarades al costat de la resta d'estadístiques, fora del bucle:

int opcionsInvalides = 0;
int operacionsTotals = 0;

Increments a cada branca vàlida i al default:

case "1" -> {
    operacionsTotals++;
    // ... resta de la branca sense canvis ...
}

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

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

default -> {
    opcionsInvalides++;
    System.out.println("  Opcio no reconeguda. Fes servir 0, 1, 2 o 3.");
}

Bloc afegit al resum de l'opció 3:

// Percentatge d'encert, amb guarda contra la divisio per zero.
int intents = operacionsTotals + opcionsInvalides;
double encert;
if (intents == 0) {
    encert = 100.0;                     // encara no ha fet res: no hi ha errors
} else {
    encert = operacionsTotals * 100.0 / intents;
}

System.out.printf("    %-26s %d%n",      "Operacions valides:", operacionsTotals);
System.out.printf("    %-26s %d%n",      "Opcions invalides:", opcionsInvalides);
System.out.printf("    %-26s %.1f %%%n", "Encert:", encert);

I al tancament:

System.out.printf("  %d operacions valides i %d opcions invalides.%n",
                  operacionsTotals, opcionsInvalides);

Exemple amb la sessió de prova de l'apartat 9 (dues devolucions, un préstec, un 9 erroni i una consulta de resum):

    Operacions valides:        4
    Opcions invalides:         1
    Encert:                    80,0 %

Dos detalls a raonar. Primer, operacionsTotals++ va al principi de cada branca: si el posessis al final i l'usuari abandonés a mig formulari, no comptaria. Segon, la multiplicació operacionsTotals * 100.0 fa servir 100.0 i no 100: en ser double, promociona tota l'expressió i evita la divisió entera que al mòdul 1 convertia 5/2 en 2. Amb 100 obtindries sempre 0 % o 100 %.

Solució 3

Variable nova al costat de les estadístiques:

String ultimIsbn = "";

I, dins de la branca case "1", just després de validar l'ISBN i abans de demanar els dies:

// --- Comprovacio de duplicat consecutiu ---
boolean continuar = true;

if (isbn.equals(ultimIsbn)) {
    System.out.println("    AVIS: aquest ISBN es el mateix de la devolucio anterior.");

    String resposta;
    do {
        System.out.print("    Registrar-la igualment? (si/no): ");
        resposta = scanner.nextLine().trim();
        if (!resposta.equalsIgnoreCase("si") && !resposta.equalsIgnoreCase("no")) {
            System.out.println("      Respon 'si' o 'no'.");
        }
    } while (!resposta.equalsIgnoreCase("si") && !resposta.equalsIgnoreCase("no"));

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

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

    // ... aqui va TOTA la resta de la branca: demanar dies, calcular,
    //     actualitzar estadistiques i imprimir el rebut ...

    ultimIsbn = isbn;      // es recorda per a la propera comprovacio
}

Sessió d'exemple:

  Opcio: 1
  Empleat: Marta Ruiz
  Titol del llibre: Java Eficac
  ISBN (978-XXXXXXXXXX): 978-0000000001
    AVIS: aquest ISBN es el mateix de la devolucio anterior.
    Registrar-la igualment? (si/no): potser
      Respon 'si' o 'no'.
    Registrar-la igualment? (si/no): no
    Devolucio descartada. Cap estadistica modificada.

Claus de la solució:

  1. La comprovació es fa després de validar l'ISBN (necessites el valor net) i abans de demanar els dies (per no molestar l'usuari amb més preguntes si descartarà l'operació).
  2. La comparació és isbn.equals(ultimIsbn), mai ==: és la regla de la lliçó 02-01. I com que ultimIsbn s'inicialitza a "" i no a null, la crida és segura des de la primera volta.
  3. La confirmació fa servir equalsIgnoreCase en un bucle de validació, així que SI, Si i si valen igual, i qualsevol altra cosa torna a preguntar.
  4. ultimIsbn = isbn; s'assigna només si la devolució es registra de debò, dins de l'else. Si el posessis fora, una operació descartada contaminaria la comprovació següent.

Resposta a la pregunta final. La comprovació només detecta duplicats consecutius perquè ultimIsbn és una única variable escalar: només pot recordar un valor, el més recent, i cada nova devolució el sobreescriu. Si el bibliotecari registra 978-0000000001, després 978-0000000002 i tot seguit un altre cop 978-0000000001, el tercer registre passarà sense avís, perquè el primer ja s'ha oblidat.

Per detectar qualsevol duplicat de la sessió necessitaries recordar tots els ISBN registrats, no només l'últim. Això exigeix una estructura capaç d'emmagatzemar un nombre indeterminat de valors i de respondre ràpid a la pregunta "aquest ISBN ja hi és?". Aquesta estructura existeix i s'anomena HashSet (lliçó 05-06), i la seva versió amb dades associades és HashMap (lliçó 05-05). És, paraula per paraula, la mancança número 2 de la llista de limitacions: el programa només pot recordar un element alhora.

Conclusió

Has construït BiblioTechApp 2.0, una aplicació de consola completa i funcional. Mostra un menú en bucle amb while i el despatxa amb un switch de fletxes; valida cada entrada amb bucles do-while que insisteixen fins a obtenir una dada correcta, inclosa una comprovació caràcter a caràcter que impedeix que un text no numèric tombi el programa; aplica les regles de negoci de Nexus Software amb la taula de decisió del mòdul —retard saturat a zero, multa amb sostre, estat i gravetat—; acumula vuit estadístiques de sessió amb comptadors, acumuladors i el patró del màxim amb dada associada; emet rebuts i resums alineats amb printf; i es tanca de manera ordenada amb una bandera booleana i un break col·locat exactament on ha d'estar. Res d'això no existia fa sis lliçons.

Amb això tanques el mòdul 2. El teu programa ja decideix (if, else if, ternari, comparació correcta de String), repeteix (while, do-while, for, bucles imbricats, comptadors, acumuladors i banderes), selecciona (switch clàssic i modern, com a sentència i com a expressió), controla el flux dins dels bucles (break, continue, etiquetes) i, quan alguna cosa falla, saps mirar-hi a dins amb traces i amb el depurador en lloc d'endevinar.

I saps exactament què li falta, perquè ho has enumerat amb precisió: l'estat són variables soltes que haurien de ser objectes, només hi cap un llibre alhora on hi hauria d'haver una llista, una entrada imprevista encara el pot trencar, en sortir es perd tot i el main fa dues-centes línies que demanen a crits descompondre's en mètodes.

Al mòdul 3, Programació Orientada a Objectes, comença la transformació més important del curs: deixaràs de pensar en variables i passos, i començaràs a pensar en objectes que coneixen les seves dades i saben operar-hi. Crearàs les classes Llibre, Empleat i Prestec; els donaràs constructors per néixer sempre en un estat vàlid i mètodes per encapsular les regles de negoci que avui són escampades pel main; descobriràs l'herència i el polimorfisme, l'encapsulament i l'abstracció; i aprendràs a redefinir toString, equals i hashCode perquè els teus objectes s'imprimeixin i es comparin com cal. Al final d'aquest mòdul, aquestes tres variables incòmodes —retardMesGran, empleatRetardMesGran i llibreRetardMesGran— seran un únic objecte coherent, i BiblioTech començarà a semblar-se de debò a un sistema.

Curs de Programació en Java

Mòdul 1: Introducció a Java

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats