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

  1. Declarar una classe: sintaxi mínima
  2. Camps d'instància i valors per defecte
  3. Un fitxer per classe pública i organització en paquets
  4. Crear objectes amb new
  5. Què passa a la memòria: pila i heap
  6. Accedir als camps amb el punt
  7. La referència null i el NullPointerException
  8. Aliasing: diverses referències al mateix objecte
  9. Camps static enfront de camps d'instància
  10. Constants static final
  11. Cicle de vida d'un objecte i recol·lecció de brossa
  12. BiblioTech: primera versió de Llibre en funcionament
  13. Errors Habituals i Consells
  14. Exercicis

  1. Declarar una classe: sintaxi mínima

Una classe és, en la seva forma més simple, un nom i un bloc:

public class Llibre {
    // aqui dins van els camps i els metodes
}

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, en UpperCamelCase.
  • 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:

public class Llibre {
    System.out.println("Hola");   // ERROR: sentencia fora d'un metode
}

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.

  1. 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 public només de manera provisional, perquè puguis manipular-los amb el punt mentre n'aprens el mecanisme. A la lliçó 03-07 els faràs private i els exposaràs mitjançant operacions controlades, que és com s'escriu codi professional. Considera aquest public una 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);      // false

Aquesta 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à.

  1. Un fitxer per classe pública i organització en paquets

Regles del llenguatge, no d'estil:

  • Un fitxer .java pot contenir com a màxim una classe public.
  • Si conté una classe public, el fitxer s'ha d'anomenar igual que la classe, amb la mateixa capitalització: Llibre va a Llibre.java.
  • El package declarat 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.java

El 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.

  1. Crear objectes amb new

La classe és el plànol; new és la constructora que aixeca la casa:

Llibre javaEficac = new Llibre();

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:

new Llibre();   // compila: crea un objecte i el perd immediatament

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).

  1. 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:

  1. 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.
  2. Cada new produeix un objecte diferent, encara que els valors fossin idèntics.
  3. Els camps viuen dins de l'objecte, al heap, no a la pila.
  4. Quan main acabi, a i b desapareixen de la pila; els objectes del heap queden sense referències i es converteixen en brossa.

  1. 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:

prestec.llibre.titol   // el titol del llibre del prestec

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");
}

  1. La referència null i el NullPointerException

null é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.

Llibre senseAssignar = null;
System.out.println(senseAssignar);          // null

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 execucio

El 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.

  1. 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);   // true

Dos 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;

  1. Camps static enfront de camps d'instància

Tots 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);   // 2

Diferè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.

  1. Constants static final

Combinant 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).

  1. 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 recollit

Tres 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.

  1. BiblioTech: primera versió de Llibre en funcionament

Reunim-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 un NullPointerException (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 null amb "" o amb 0. null significa "sense objecte". Una cadena buida és un objecte String perfectament vàlid, sobre el qual pots cridar mètodes; sobre null no pots cridar res.
  • Posar el nom del fitxer diferent del de la classe pública. class Llibre a llibre.java no 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: si titol fos static, els tres llibres tindrien el mateix títol! Un camp és static només si el seu valor és genuïnament comú a tota la classe.
  • Accedir a un camp static mitjançant una instància. p1.prestecsCreats compila, però enganya el lector. Escriu sempre Prestec.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 i toString tan 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) i prestecsAcumulats (int).
  • Un camp static empleatsRegistrats que compti quants se n'han creat.
  • Una constant static final MAX_PRESTECS_SIMULTANIS amb 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:

  1. Mutar l'objecte a través d'una referència i veure el canvi des de l'altra.
  2. Reapuntar una referència a un objecte nou i comprovar que l'altra no canvia.
  3. Comparar amb == dues referències al mateix objecte i dos objectes diferents amb ISBN idèntic.
  4. Posar una referència a null i 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 15

Traç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);   // funciona

Quina é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

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