A la lliçó anterior vas dibuixar el plànol: tres classes, Llibre, Empleat i Prestec, amb els seus atributs i les seves responsabilitats. Ara toca construir. Aquesta lliçó és la que converteix el diagrama en codi Java real: aprendràs la sintaxi exacta per declarar una classe, quin valor tenen els seus camps abans que ningú els assigni, com crear objectes amb new i què passa exactament a la memòria quan ho fas. Veuràs per què null no és "buit" sinó "cap adreça", què significa realment el NullPointerException que tard o d'hora et saltarà, i una de les sorpreses més desagradables per a qui comença: dues variables poden apuntar al mateix objecte, de manera que modificar-lo per una el modifica també per a l'altra. En acabar, "Java Eficaç", "Patrons de Disseny" i "Refactorització" existiran al teu programa com a tres objectes amb vida pròpia.
Contingut
- Declarar una classe: sintaxi mínima
- Camps d'instància i valors per defecte
- Un fitxer per classe pública i organització en paquets
- Crear objectes amb
new - Què passa a la memòria: pila i heap
- Accedir als camps amb el punt
- La referència
nulli elNullPointerException - Aliasing: diverses referències al mateix objecte
- Camps
staticenfront de camps d'instància - Constants
static final - Cicle de vida d'un objecte i recol·lecció de brossa
- BiblioTech: primera versió de
Llibreen funcionament - Errors Habituals i Consells
- Exercicis
- Declarar una classe: sintaxi mínima
Una classe és, en la seva forma més simple, un nom i un bloc:
Dissequem-ho amb la mateixa calma amb què al mòdul 1 vas dissecar main:
publicés un modificador d'accés: indica que la classe és visible des de qualsevol paquet. Si l'omets, la classe només serà visible dins del seu propi paquet (visibilitat default, o "de paquet"). Els modificadors s'estudien a fons a la lliçó 03-07.classés la paraula reservada que declara una classe.Llibreés el nom. Per convenció (mòdul 1): substantiu, singular, enUpperCamelCase.- Les claus delimiten el cos de la classe, on viuen camps, constructors i mètodes.
Compte amb una diferència fonamental respecte a tot el que has escrit fins ara: dins del cos d'una classe no pots posar sentències soltes. Això no compila:
El cos d'una classe només admet declaracions: camps, constructors, mètodes, blocs d'inicialització i altres classes. El codi executable viu dins de mètodes.
- Camps d'instància i valors per defecte
Un camp (o atribut, o variable d'instància) es declara igual que una variable local, però directament al cos de la classe:
package com.nexussoftware.bibliotech.domini;
public class Llibre {
public String titol;
public String autor;
public String isbn;
public int anyPublicacio;
public boolean disponible;
}Nota important: aquests camps són
publicnomés de manera provisional, perquè puguis manipular-los amb el punt mentre n'aprens el mecanisme. A la lliçó 03-07 els faràsprivatei els exposaràs mitjançant operacions controlades, que és com s'escriu codi professional. Considera aquestpublicuna bastida.
I aquí arriba la primera diferència important respecte a les variables locals que ja coneixes. Al mòdul 1 vas aprendre que una variable local s'ha d'inicialitzar abans de fer-la servir o el compilador es queixa. Els camps, en canvi, s'inicialitzen automàticament amb un valor per defecte:
| Tipus del camp | Valor per defecte | Comentari |
|---|---|---|
byte, short, int, long |
0 |
El 0L en el cas de long |
float, double |
0.0 |
|
char |
' ' |
Caràcter nul, no visible en imprimir |
boolean |
false |
|
Qualsevol referència (String, Llibre...) |
null |
No cadena buida: literalment "cap adreça" |
Llibre buit = new Llibre();
System.out.println(buit.titol); // null (no "" ni "buit")
System.out.println(buit.anyPublicacio); // 0
System.out.println(buit.disponible); // falseAquesta inicialització automàtica és una comoditat, però també un parany: un objecte acabat de crear amb new Llibre() és un llibre sense títol, sense autor i no disponible, és a dir, un objecte en estat invàlid. La solució definitiva són els constructors, que estudiaràs a la lliçó 03-04 i que garanteixen que cap objecte no neixi incomplet. De moment, assignarem els camps a mà.
- Un fitxer per classe pública i organització en paquets
Regles del llenguatge, no d'estil:
- Un fitxer
.javapot contenir com a màxim una classepublic. - Si conté una classe
public, el fitxer s'ha d'anomenar igual que la classe, amb la mateixa capitalització:Llibreva aLlibre.java. - El
packagedeclarat a la primera línia ha de correspondre amb el camí de directoris (mòdul 1).
Aplicat al projecte bibliotech, l'estructura queda així:
bibliotech/
└── src/main/java/
└── com/nexussoftware/bibliotech/
├── BiblioTechApp.java (paquet com.nexussoftware.bibliotech)
└── domini/
├── Llibre.java (paquet ...bibliotech.domini)
├── Empleat.java
└── Prestec.javaEl subpaquet domini no és un caprici: separa les classes que representen el negoci del codi que executa l'aplicació. És la primera decisió d'arquitectura del curs, i la reprendràs a 03-07 (encapsulament a nivell de paquet) i al mòdul 12.
Com que BiblioTechApp i Llibre ara són en paquets diferents, la teva classe d'arrencada necessita un import:
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.domini.Llibre;
public class BiblioTechApp {
public static void main(String[] args) {
// ...
}
}És exactament el mateix mecanisme que feies servir amb import java.util.Scanner;. La diferència és que ara la classe importada l'has escrita tu.
- Crear objectes amb
new
newLa classe és el plànol; new és la constructora que aixeca la casa:
Tres parts que convé separar mentalment:
| Fragment | Què és | Què fa |
|---|---|---|
Llibre javaEficac |
Declaració d'una variable de tipus referència | Reserva un forat a la pila per guardar una adreça |
new Llibre() |
Operador de creació | Reserva memòria al heap i inicialitza els camps als seus valors per defecte |
= |
Assignació | Copia a la variable l'adreça de l'objecte acabat de crear |
Els parèntesis de new Llibre() són una crida al constructor. Com que encara no n'has escrit cap, Java et proporciona un constructor per defecte sense paràmetres que no fa res més que crear l'objecte. A 03-04 veuràs quan desapareix aquest regal.
Res no t'obliga a guardar la referència:
Aquest objecte existeix durant un instant i queda inabastable: no hi ha cap variable que hi porti, així que el recol·lector de brossa acabarà eliminant-lo (apartat 11).
- Què passa a la memòria: pila i heap
Recupera del mòdul 1 la distinció entre pila (stack) i monticle (heap):
- La pila guarda les variables locals i els paràmetres de cada mètode en execució. És petita, rapidíssima i s'allibera sola en acabar el mètode.
- El heap guarda tots els objectes. És gran, el gestiona la JVM i el neteja el recol·lector de brossa.
Executa mentalment aquest fragment:
Llibre a = new Llibre();
a.titol = "Java Eficac";
a.isbn = "978-0000000001";
Llibre b = new Llibre();
b.titol = "Patrons de Disseny";
b.isbn = "978-0000000002";flowchart LR
subgraph PILA["Pila (main)"]
A["a = 0x1A"]
B["b = 0x2B"]
end
subgraph HEAP["Heap"]
O1["0x1A : Llibre
titol = Java Eficac
isbn = 978-0000000001
disponible = false"]
O2["0x2B : Llibre
titol = Patrons de Disseny
isbn = 978-0000000002
disponible = false"]
end
A --> O1
B --> O2
Quatre lectures que has d'extreure del diagrama:
- La variable no conté l'objecte, conté una fletxa cap a ell. Les adreces (
0x1A) són fictícies; mai no les veuràs ni les necessites. - Cada
newprodueix un objecte diferent, encara que els valors fossin idèntics. - Els camps viuen dins de l'objecte, al heap, no a la pila.
- Quan
mainacabi,aibdesapareixen de la pila; els objectes del heap queden sense referències i es converteixen en brossa.
- Accedir als camps amb el punt
L'operador . serveix per arribar a l'interior d'un objecte a través d'una referència:
Llibre javaEficac = new Llibre();
// Escriptura
javaEficac.titol = "Java Eficac";
javaEficac.autor = "Joshua Bloch";
javaEficac.isbn = "978-0000000001";
javaEficac.anyPublicacio = 2018;
javaEficac.disponible = true;
// Lectura
System.out.println(javaEficac.titol); // Java Eficac
System.out.printf("%s (%d)%n", javaEficac.autor, javaEficac.anyPublicacio);El punt es llegeix com un genitiu: javaEficac.titol és "el títol de javaEficac". I es pot encadenar quan un camp és al seu torn un objecte:
Funciona, però apunta mentalment que encadenar punts és una olor de disseny anomenada tren de missatges; la lliçó 03-07 el formalitza amb la Llei de Demeter.
Els camps també es poden fer servir en qualsevol expressió de les que ja domines:
if (javaEficac.disponible && javaEficac.anyPublicacio >= 2015) {
System.out.println("Disponible i recent");
}
- La referència
null i el NullPointerException
null i el NullPointerExceptionnull és un valor especial que significa "aquesta referència no apunta a cap objecte". No és zero, no és cadena buida, no és un objecte "en blanc": és l'absència d'adreça.
Imprimir una referència nul·la és inofensiu. El que no és inofensiu és intentar fer servir el punt sobre ella:
Llibre senseAssignar = null;
System.out.println(senseAssignar.titol); // NullPointerException en execucioEl programa s'atura amb una traça com aquesta:
Exception in thread "main" java.lang.NullPointerException:
Cannot read field "titol" because "senseAssignar" is null
at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:14)Llegeix-la amb el mètode sistemàtic de la lliçó 02-05:
| Part del missatge | Què et diu |
|---|---|
NullPointerException |
S'ha fet servir el punt sobre una referència nul·la |
Cannot read field "titol" |
El camp al qual s'intentava accedir |
because "senseAssignar" is null |
Quina referència era nul·la (missatge detallat de Java 14+) |
at ...BiblioTechApp.main(BiblioTechApp.java:14) |
Fitxer i línia exactes |
Aquest "because ... is null" és una millora enorme de Java modern: en versions antigues només veies el nom de l'excepció i la línia, i amb a.b.c.d calia endevinar quin dels quatre era nul.
Un NullPointerException no es resol amb try/catch (mòdul 6): es resol evitant que existeixi la referència nul·la. Les eines per fer-ho són les guardes de nul·litat que ja vas fer servir al mòdul 2 i, sobretot, els constructors que obliguen a donar valors (03-04):
if (llibre != null) {
System.out.println(llibre.titol);
} else {
System.out.println("No hi ha cap llibre associat.");
}Recorda del mòdul 1 la regla de comparar cadenes: amb un possible null pel mig, "Java Eficac".equals(llibre.titol) és més segur que llibre.titol.equals("Java Eficac"), perquè el literal mai no és nul.
- Aliasing: diverses referències al mateix objecte
Aquest és el concepte que més sorpreses causa. Observa:
Llibre original = new Llibre();
original.titol = "Refactoritzacio";
original.disponible = true;
Llibre copia = original; // copia?
copia.disponible = false; // es modifica "copia"
System.out.println(original.disponible); // false <-- no era una copia!Llibre copia = original; no duplica l'objecte. Copia la referència, és a dir, l'adreça. El resultat són dues variables apuntant al mateix objecte del heap:
flowchart LR
subgraph PILA["Pila"]
V1["original = 0x3C"]
V2["copia = 0x3C"]
end
subgraph HEAP["Heap"]
OBJ["0x3C : Llibre
titol = Refactoritzacio
disponible = false"]
end
V1 --> OBJ
V2 --> OBJ
A aquest fenomen se l'anomena aliasing (dos noms per a la mateixa cosa). Conseqüències pràctiques:
| Operació | Efecte |
|---|---|
copia.disponible = false; |
Modifica **l'**objecte; es veu des d'original |
copia = new Llibre(); |
Reapunta la variable copia; original no se n'assabenta |
copia == original |
true si apunten al mateix objecte (identitat) |
copia = null; |
Només copia queda a nul; l'objecte continua viu per original |
Comprova la diferència entre mutar i reapuntar:
Llibre a = new Llibre();
a.titol = "Java Eficac";
Llibre b = a;
b.titol = "Patrons de Disseny"; // MUTAR l'objecte compartit
System.out.println(a.titol); // Patrons de Disseny
b = new Llibre(); // REAPUNTAR la variable b
b.titol = "Refactoritzacio";
System.out.println(a.titol); // Patrons de Disseny (a no canvia)I compara la identitat amb la igualtat, reprenent el == del mòdul 1:
Llibre x = new Llibre();
x.isbn = "978-0000000001";
Llibre y = new Llibre();
y.isbn = "978-0000000001";
System.out.println(x == y); // false: son dos objectes diferents
System.out.println(x == x); // trueDos llibres amb el mateix ISBN són objectes diferents i == ho diu clarament. I si vols que es considerin iguals pel seu ISBN? Això exigeix sobreescriure equals, i és el tema de la lliçó 03-09.
Si de debò vols duplicar un objecte, cal fer-ho camp a camp (o amb un constructor de còpia, lliçó 03-04):
Llibre duplicat = new Llibre();
duplicat.titol = original.titol;
duplicat.autor = original.autor;
duplicat.isbn = original.isbn;
duplicat.anyPublicacio = original.anyPublicacio;
duplicat.disponible = original.disponible;
- Camps
static enfront de camps d'instància
static enfront de camps d'instànciaTots els camps vistos fins ara són d'instància: cada objecte té la seva pròpia còpia. Un camp static és diferent: pertany a la classe, i n'existeix una sola còpia compartida per tots els objectes.
El cas d'ús clàssic és un comptador. Nexus Software vol saber quants préstecs s'han creat durant la sessió:
package com.nexussoftware.bibliotech.domini;
public class Prestec {
// Camp de classe: un de sol, compartit per tots els prestecs
public static int prestecsCreats = 0;
// Camps d'instancia: un per objecte
public Llibre llibre;
public Empleat empleat;
public int diesTranscorreguts;
}De moment el comptador s'ha d'incrementar a mà en crear cada préstec (a 03-04 ho farà el constructor, que és el seu lloc natural):
Prestec p1 = new Prestec();
Prestec.prestecsCreats++;
Prestec p2 = new Prestec();
Prestec.prestecsCreats++;
System.out.println(Prestec.prestecsCreats); // 2Diferències essencials:
| Aspecte | Camp d'instància | Camp static |
|---|---|---|
| Quantes còpies hi ha | Una per objecte | Una per classe |
| Com s'hi accedeix | objecte.camp |
Classe.camp (recomanat) |
| Quan existeix | Des de new fins que es recol·lecta |
Des que es carrega la classe fins que acaba el programa |
| Exemple | llibre.titol |
Prestec.prestecsCreats |
| Serveix per a | Estat propi de cada objecte | Estat o dades comunes a tots |
Java permet accedir a un camp static a través d'una instància (p1.prestecsCreats), però és molt mala pràctica: dona la falsa impressió que el valor pertany a aquell objecte. Fes servir sempre el nom de la classe.
I un advertiment: els camps static mutables són estat global disfressat, amb els mateixos riscos que les vint variables de main. Fes-los servir per a comptadors, constants i poca cosa més.
- Constants
static final
static finalCombinant static (una sola còpia) amb final (no es pot reassignar, mòdul 1) s'obté la forma canònica de declarar una constant:
public class Prestec {
/** Durada estandard d'un prestec, en dies. */
public static final int DIES_PRESTEC = 15;
/** Import cobrat per cada dia de retard, en euros. */
public static final double TARIFA_DIARIA = 0.25;
/** Import maxim que pot assolir una multa, en euros. */
public static final double MULTA_MAXIMA = 20.0;
/** Retard maxim (en dies) que es considera lleu. */
public static final int LLINDAR_LLEU = 7;
public static int prestecsCreats = 0;
public Llibre llibre;
public Empleat empleat;
public int diesTranscorreguts;
}Això resol un problema real del teu codi actual: fins ara les quatre regles de negoci eren final locals dins de main, invisibles per a qualsevol altra part del programa. Ara viuen a la classe que les governa i es fan servir des de qualsevol lloc:
System.out.printf("Termini: %d dies, tarifa %.2f EUR/dia%n",
Prestec.DIES_PRESTEC, Prestec.TARIFA_DIARIA);Convencions per a constants (mòdul 1): MAJUSCULES_AMB_GUIO_BAIX. I una precaució: final impedeix reassignar la variable, no modificar l'objecte al qual apunta. public static final Llibre CATALEG_BASE = new Llibre(); t'impediria fer CATALEG_BASE = altreLlibre;, però no CATALEG_BASE.titol = "Altre";. Per això les constants es declaren de tipus immutables (primitius, String).
- Cicle de vida d'un objecte i recol·lecció de brossa
Un objecte Java travessa tres fases:
flowchart LR
A["1. Creacio
new reserva memoria
i inicialitza camps"] --> B["2. Us
hi ha almenys
una referencia viva"]
B --> C["3. Inabastable
cap referencia
hi arriba"]
C --> D["4. Recollit
el GC allibera
la memoria"]
En Java no existeix cap operador per destruir objectes (no hi ha delete com en C++). Quan un objecte deixa de ser abastable des de qualsevol referència viva, es converteix en candidat a ser eliminat pel recol·lector de brossa (garbage collector, GC), un procés de la JVM que allibera memòria automàticament.
Llibre temporal = new Llibre();
temporal.titol = "Refactoritzacio";
temporal = null; // l'objecte queda inabastable: sera recollitTres coses que has de saber ja, sense entrar en detalls:
- No controles quan s'executa el GC. Pot ser immediatament o molt després.
System.gc()és un suggeriment que la JVM pot ignorar; no el facis servir. - El GC elimina la major part de les fuites de memòria, però no totes: si mantens referències vives a objectes que ja no necessites (per exemple, en una estructura global que creix sense límit), la memòria no s'allibera.
- No posis lògica de neteja a
finalize(): està obsolet des de Java 9 i no s'ha de fer servir (lliçó 03-09).
El funcionament intern del GC, les generacions i l'ajust de rendiment s'estudien a la lliçó 10-07.
- BiblioTech: primera versió de
Llibre en funcionament
Llibre en funcionamentReunim-ho tot. Fitxer src/main/java/com/nexussoftware/bibliotech/domini/Llibre.java:
package com.nexussoftware.bibliotech.domini;
/**
* Representa un llibre del cataleg tecnic de Nexus Software.
*
* Versio 1: els camps son publics de manera provisional per poder
* assignar-los amb l'operador punt. A la llico 03-07 passaran a ser
* privats i s'hi accedira mitjancant operacions controlades.
*/
public class Llibre {
/** Nombre total de llibres creats durant l'execucio. */
public static int llibresCreats = 0;
public String titol;
public String autor;
public String isbn;
public int anyPublicacio;
public boolean disponible;
}Fitxer Prestec.java, encara mínim (les seves operacions arriben a 03-03 i el seu constructor a 03-04):
package com.nexussoftware.bibliotech.domini;
/** Registre d'un prestec d'un llibre a un empleat. */
public class Prestec {
public static final int DIES_PRESTEC = 15;
public static final double TARIFA_DIARIA = 0.25;
public static final double MULTA_MAXIMA = 20.0;
public static final int LLINDAR_LLEU = 7;
public static int prestecsCreats = 0;
public Llibre llibre;
public String empleat; // passara a ser un objecte Empleat a 03-04
public int diesTranscorreguts;
}I l'arrencada, BiblioTechApp.java:
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.domini.Llibre;
import com.nexussoftware.bibliotech.domini.Prestec;
public class BiblioTechApp {
public static void main(String[] args) {
System.out.println("=== BiblioTech 3.0 - Cataleg d'objectes ===");
// --- Creacio dels tres llibres del cataleg ---
Llibre javaEficac = new Llibre();
javaEficac.titol = "Java Eficac";
javaEficac.autor = "Joshua Bloch";
javaEficac.isbn = "978-0000000001";
javaEficac.anyPublicacio = 2018;
javaEficac.disponible = true;
Llibre.llibresCreats++;
Llibre patrons = new Llibre();
patrons.titol = "Patrons de Disseny";
patrons.autor = "Erich Gamma";
patrons.isbn = "978-0000000002";
patrons.anyPublicacio = 1994;
patrons.disponible = true;
Llibre.llibresCreats++;
Llibre refactoritzacio = new Llibre();
refactoritzacio.titol = "Refactoritzacio";
refactoritzacio.autor = "Martin Fowler";
refactoritzacio.isbn = "978-0000000003";
refactoritzacio.anyPublicacio = 2018;
refactoritzacio.disponible = false;
Llibre.llibresCreats++;
// --- Llistat ---
System.out.println("\nCataleg:");
System.out.printf(" %-22s %-16s %-16s %4s %s%n",
"TITOL", "AUTOR", "ISBN", "ANY", "ESTAT");
imprimir(javaEficac);
imprimir(patrons);
imprimir(refactoritzacio);
System.out.printf("%nLlibres creats: %d%n", Llibre.llibresCreats);
// --- Un prestec que referencia un d'aquests llibres ---
Prestec p = new Prestec();
p.llibre = refactoritzacio; // referencia al MATEIX objecte
p.empleat = "Marta Ruiz";
p.diesTranscorreguts = 20;
Prestec.prestecsCreats++;
System.out.printf("%nPrestec de '%s' a %s (%d dies transcorreguts)%n",
p.llibre.titol, p.empleat, p.diesTranscorreguts);
System.out.printf("Regles vigents: %d dies de termini, %.2f EUR/dia, sostre %.2f EUR%n",
Prestec.DIES_PRESTEC, Prestec.TARIFA_DIARIA,
Prestec.MULTA_MAXIMA);
}
/** Imprimeix una fila del cataleg. Metode auxiliar; els metodes s'expliquen a 03-03. */
private static void imprimir(Llibre llibre) {
String estat = llibre.disponible ? "Disponible" : "Prestat";
System.out.printf(" %-22s %-16s %-16s %4d %s%n",
llibre.titol, llibre.autor, llibre.isbn,
llibre.anyPublicacio, estat);
}
}Sortida:
=== BiblioTech 3.0 - Cataleg d'objectes === Cataleg: TITOL AUTOR ISBN ANY ESTAT Java Eficac Joshua Bloch 978-0000000001 2018 Disponible Patrons de Disseny Erich Gamma 978-0000000002 1994 Disponible Refactoritzacio Martin Fowler 978-0000000003 2018 Prestat Llibres creats: 3 Prestec de 'Refactoritzacio' a Marta Ruiz (20 dies transcorreguts) Regles vigents: 15 dies de termini, 0,25 EUR/dia, sostre 20,00 EUR
Compara aquest codi amb el del mòdul 2. Les dades d'un llibre ja no estan disperses: viatgen juntes dins d'un objecte, i pots passar el llibre sencer a un mètode amb un sol paràmetre (imprimir(javaEficac)) en lloc de cinc. I p.llibre = refactoritzacio; és aliasing intencionat: el préstec apunta al mateix exemplar del catàleg, així que quan a la propera lliçó marquis p.llibre.disponible = false, el catàleg se n'assabentarà sol.
El que encara falta salta a la vista: els objectes neixen buits i cal omplir-los a mà camp a camp (constructors, 03-04), no saben fer res (mètodes, 03-03), i amb més de tres llibres el codi es torna inviable perquè no hi ha on guardar-los: això ho resol el mòdul 5 amb les col·leccions.
Errors Habituals i Consells
- Oblidar el
new.Llibre llibre;declara una referència, no crea res. Si la fas servir, obtens unNullPointerException(si és camp) o un error de compilació per variable no inicialitzada (si és local). - Creure que
=copia l'objecte. És l'error de l'apartat 8.b = a;crea un àlies, no una còpia. Si necessites un objecte independent, cal construir-lo. - Confondre
nullamb""o amb0.nullsignifica "sense objecte". Una cadena buida és un objecteStringperfectament vàlid, sobre el qual pots cridar mètodes; sobrenullno pots cridar res. - Posar el nom del fitxer diferent del de la classe pública.
class Llibreallibre.javano compila en sistemes sensibles a majúscules (tots els Linux, i la JVM en general). Deixa que l'IDE creï les classes per tu. - Declarar camps com a
static"perquè així s'hi accedeix més fàcilment". És la via ràpida a un estat global compartit per tots els objectes: sititolfosstatic, els tres llibres tindrien el mateix títol! Un camp ésstaticnomés si el seu valor és genuïnament comú a tota la classe. - Accedir a un camp
staticmitjançant una instància.p1.prestecsCreatscompila, però enganya el lector. Escriu semprePrestec.prestecsCreats. - Consell: davant d'un
NullPointerException, no busquis on va fallar, busca on va faltar assignar. El missatge "because X is null" et dona el nom exacte de la referència; retrocedeix fins al punt on s'hauria d'haver assignat. - Consell: acostuma't ja a
Alt+Insert/ "Generate" de l'IDE. Generarà constructors, getters itoStringtan bon punt els estudiïs. Però escriu-los a mà les primeres vegades: només així entens què genera.
Exercicis
Exercici 1: la classe Empleat
Crea la classe Empleat al paquet com.nexussoftware.bibliotech.domini amb:
- Camps públics
nom(String),identificador(String) iprestecsAcumulats(int). - Un camp
staticempleatsRegistratsque compti quants se n'han creat. - Una constant
static finalMAX_PRESTECS_SIMULTANISamb valor 3.
Després, a BiblioTechApp, crea els tres empleats de Nexus Software (Marta Ruiz amb identificador EMP-001, Diego Alonso amb EMP-002 i Nuria Vidal amb EMP-003), assigna'ls 2, 0 i 4 préstecs acumulats respectivament, imprimeix una taula amb printf i assenyala amb un asterisc qui superi el màxim permès. Mostra al final el comptador d'empleats registrats.
Exercici 2: demostrar l'aliasing
Escriu un programa que demostri, amb sortida per consola, les quatre situacions de la taula de l'apartat 8, fent servir dues referències Llibre:
- Mutar l'objecte a través d'una referència i veure el canvi des de l'altra.
- Reapuntar una referència a un objecte nou i comprovar que l'altra no canvia.
- Comparar amb
==dues referències al mateix objecte i dos objectes diferents amb ISBN idèntic. - Posar una referència a
nulli comprovar que l'objecte continua accessible des de l'altra.
Explica en comentaris què passa a la memòria a cada pas.
Exercici 3: provocar i llegir un NullPointerException
Escriu un fragment que provoqui deliberadament un NullPointerException sobre un objecte Prestec el camp llibre del qual no s'ha assignat. Copia la traça completa que obtens, identifica-hi la referència nul·la, la línia culpable i el camp al qual s'intentava accedir. Després, corregeix el programa sense fer servir try/catch, de dues maneres diferents: (a) amb una guarda de nul·litat, i (b) assignant el camp abans de fer-lo servir. Raona quina de les dues és millor solució i per què.
Solucions
Solució 1
Empleat.java:
package com.nexussoftware.bibliotech.domini;
/**
* Empleat de Nexus Software autoritzat a prendre llibres en prestec.
* Camps publics de manera provisional (vegeu la llico 03-07).
*/
public class Empleat {
/** Nombre maxim de prestecs simultanis permesos per la politica interna. */
public static final int MAX_PRESTECS_SIMULTANIS = 3;
/** Quants empleats s'han registrat en aquesta execucio. */
public static int empleatsRegistrats = 0;
public String nom;
public String identificador;
public int prestecsAcumulats;
}Ús a BiblioTechApp:
Empleat marta = new Empleat();
marta.nom = "Marta Ruiz";
marta.identificador = "EMP-001";
marta.prestecsAcumulats = 2;
Empleat.empleatsRegistrats++;
Empleat diego = new Empleat();
diego.nom = "Diego Alonso";
diego.identificador = "EMP-002";
diego.prestecsAcumulats = 0;
Empleat.empleatsRegistrats++;
Empleat nuria = new Empleat();
nuria.nom = "Nuria Vidal";
nuria.identificador = "EMP-003";
nuria.prestecsAcumulats = 4;
Empleat.empleatsRegistrats++;
System.out.printf("%n%-14s %-9s %-10s %s%n", "EMPLEAT", "ID", "PRESTECS", "AVIS");
imprimirEmpleat(marta);
imprimirEmpleat(diego);
imprimirEmpleat(nuria);
System.out.printf("%nEmpleats registrats: %d (maxim %d prestecs per persona)%n",
Empleat.empleatsRegistrats,
Empleat.MAX_PRESTECS_SIMULTANIS);private static void imprimirEmpleat(Empleat e) {
// L'asterisc marca qui supera el limit de la politica interna.
String avis = e.prestecsAcumulats > Empleat.MAX_PRESTECS_SIMULTANIS ? "*" : "";
System.out.printf("%-14s %-9s %-10d %s%n",
e.nom, e.identificador, e.prestecsAcumulats, avis);
}Sortida:
EMPLEAT ID PRESTECS AVIS Marta Ruiz EMP-001 2 Diego Alonso EMP-002 0 Nuria Vidal EMP-003 4 * Empleats registrats: 3 (maxim 3 prestecs per persona)
Punts clau: MAX_PRESTECS_SIMULTANIS és static final perquè la política és de l'empresa, no de cada persona; empleatsRegistrats és static (un per a tothom) mentre que prestecsAcumulats és d'instància (un per empleat). Si confonguessis tots dos, els tres empleats compartirien el mateix nombre de préstecs, que és justament l'error de l'apartat de consells.
Solució 2
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.domini.Llibre;
public class DemoAliasing {
public static void main(String[] args) {
// --- Situacio 1: mutar a traves d'un alies ---
Llibre a = new Llibre();
a.titol = "Java Eficac";
a.isbn = "978-0000000001";
a.disponible = true;
Llibre b = a; // b i a apunten al MATEIX objecte del heap
b.disponible = false; // es muta l'objecte compartit
System.out.println("1) a.disponible = " + a.disponible); // false
System.out.println(" b.disponible = " + b.disponible); // false
// A memoria: una sola caixa Llibre amb dues fletxes apuntant-hi.
// --- Situacio 2: reapuntar una referencia ---
b = new Llibre(); // b passa a apuntar a un objecte NOU
b.titol = "Patrons de Disseny";
b.isbn = "978-0000000002";
System.out.println("2) a.titol = " + a.titol); // Java Eficac
System.out.println(" b.titol = " + b.titol); // Patrons de Disseny
// A memoria: dues caixes diferents; la fletxa de b s'ha mogut,
// la d'a no s'ha tocat.
// --- Situacio 3: identitat amb == ---
Llibre c = a; // alies d'a
Llibre d = new Llibre();
d.isbn = a.isbn; // mateix ISBN, objecte diferent
System.out.println("3) a == c : " + (a == c)); // true
System.out.println(" a == d : " + (a == d)); // false
System.out.println(" mateix ISBN: " + a.isbn.equals(d.isbn)); // true
// == compara adreces, no continguts: a i d son dos exemplars
// diferents encara que descriguin el mateix titol. Perque es considerin
// iguals per ISBN caldra sobreescriure equals (llico 03-09).
// --- Situacio 4: anullar una referencia ---
c = null; // nomes s'esborra la fletxa de c
System.out.println("4) c es null: " + (c == null)); // true
System.out.println(" a.titol continua sent: " + a.titol); // Java Eficac
// L'objecte no es recull perque a continua apuntant-hi.
}
}Sortida:
1) a.disponible = false b.disponible = false 2) a.titol = Java Eficac b.titol = Patrons de Disseny 3) a == c : true a == d : false mateix ISBN: true 4) c es null: true a.titol continua sent: Java Eficac
La regla que cal memoritzar: el punt muta l'objecte i es veu des de tots els àlies; el = mou la fletxa d'una sola variable.
Solució 3
Codi que provoca la fallada:
Prestec p = new Prestec();
p.empleat = "Diego Alonso";
p.diesTranscorreguts = 18;
// p.llibre NO s'ha assignat: val null per defecte
System.out.println("Llibre prestat: " + p.llibre.titol); // linia 15Traça obtinguda:
Exception in thread "main" java.lang.NullPointerException:
Cannot read field "titol" because "p.llibre" is null
at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:15)Lectura de la traça:
| Dada | Valor |
|---|---|
| Referència nul·la | p.llibre (no p, que sí que està creat) |
| Camp accedit | titol |
| Línia culpable | 15 de BiblioTechApp.java |
| Causa arrel | El camp llibre no es va assignar mai, així que conserva el seu valor per defecte null |
(a) Guarda de nul·litat, amb el patró del mòdul 2:
if (p.llibre != null) {
System.out.println("Llibre prestat: " + p.llibre.titol);
} else {
System.out.println("Prestec sense llibre associat (registre incomplet).");
}(b) Assignar el camp abans de fer-lo servir:
Prestec p = new Prestec();
p.llibre = javaEficac; // ara el prestec te llibre
p.empleat = "Diego Alonso";
p.diesTranscorreguts = 18;
System.out.println("Llibre prestat: " + p.llibre.titol); // funcionaQuina és millor? La (b), sense cap dubte. L'opció (a) tracta el símptoma: accepta que puguin existir préstecs sense llibre i afegeix una comprovació que caldria repetir a cada punt on es faci servir p.llibre. L'opció (b) ataca la causa: un préstec sense llibre no és un préstec, és un objecte en estat invàlid que mai no hauria d'haver existit.
La versió definitiva d'aquesta idea arriba a la lliçó 03-04: un constructor que exigeixi el llibre com a paràmetre fa que sigui impossible crear un Prestec incomplet, i llavors la guarda de nul·litat sobra perquè el cas no es pot donar. Aquesta és la diferència entre programar defensivament i dissenyar bé: el millor NullPointerException és el que no pot passar.
Conclusió
Has fet el salt de dibuixar classes a escriure-les. Saps declarar una classe i els seus camps, i coneixes la regla —sovint oblidada— que els camps s'inicialitzen sols amb 0, false o null, cosa que fa que un objecte acabat de crear pugui estar perfectament buit i perfectament invàlid. Has vist què fa exactament new: reservar memòria al heap, inicialitzar els camps i tornar una referència que guardes a la pila; i has traçat aquest repartiment de memòria en diagrames. Manegues l'operador punt per llegir i escriure camps, entens null com a absència d'adreça i saps llegir un NullPointerException fins a identificar quina referència va fallar i per què. Has comprès l'aliasing, que és l'origen d'una part enorme dels errors de qui comença: b = a no copia, comparteix. Distingeixes camps d'instància de camps static, has portat les quatre regles de negoci de BiblioTech a constants static final dins de Prestec, on per fi són al seu lloc, i saps que la memòria l'allibera el recol·lector de brossa sense que tu hi intervinguis. El teu catàleg ja són objectes: Llibre, Empleat i un Prestec que els relaciona.
Però els teus objectes encara no saben fer res: són caixes de dades que algú manipula des de fora. Falta la meitat viva de la POO. A la lliçó següent, Mètodes, donaràs comportament a les teves classes: aprendràs l'anatomia completa d'un mètode, descobriràs per què en Java tot es passa per valor —i què implica això, sorprenentment, quan l'argument és un objecte—, dominaràs la sobrecàrrega, this, els varargs i la diferència entre mètodes static i d'instància. I faràs la primera gran refactorització del mòdul: treure de main els càlculs de dies de retard, multa i gravetat, i mudar-los a Prestec, que és el seu amo legítim.
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
