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
- L'especificació completa
- Iteració 1: l'esquelet del menú
- Iteració 2: la validació de totes les entrades
- Iteració 3: la lògica de la devolució
- Iteració 4: les estadístiques de la sessió
- Iteració 5: el poliment de la sortida
- El codi complet final, comentat
- Compilar i executar
- Una sessió de prova completa
- Limitacions: què no pot fer i quin mòdul ho resoldrà
- Errors Habituals i Consells
- Exercicis
- 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:
- Mostrar una capçalera amb l'empresa, el nom i la versió de l'aplicació.
- Mostrar en bucle un menú amb quatre opcions i no acabar fins que l'usuari triï sortir:
- 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.
- 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.
- Opció 3 (resum de la sessió): mostrar els acumulats de tot el que s'ha fet des que va arrencar el programa.
- Opció 0 (sortir): imprimir un tancament amb el total recaptat i acabar.
- 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()"]
- 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.
- 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.
- 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
- 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;
}
- 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
%-18sperquè les etiquetes quedin alineades en columna. %.2fper a tots els imports. Un rebut que digui3.0 EURno és acceptable.- Separadors amb
"=".repeat(AMPLADA)i una constantAMPLADAper 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.
- 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();
}
}
- 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.BiblioTechAppDes 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.
- 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 |
- 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:
opcionsInvalides: quantes vegades l'usuari ha introduït alguna cosa que ha caigut aldefault.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:
// ---------- 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:
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 MAXIMAPunts 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:
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):
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:
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ó:
- 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ó).
- La comparació és
isbn.equals(ultimIsbn), mai==: és la regla de la lliçó 02-01. I com queultimIsbns'inicialitza a""i no anull, la crida és segura des de la primera volta. - La confirmació fa servir
equalsIgnoreCaseen un bucle de validació, així queSI,Siisivalen igual, i qualsevol altra cosa torna a preguntar. 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
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
