18  Persistència dels objectes

Objectius

  • Explicar què és la persistència i per què la necessitem.
  • Fer que un objecte Java es pugui serialitzar amb Serializable.
  • Guardar i recuperar objectes amb ObjectOutputStream i ObjectInputStream.
  • Controlar la serialització amb transient i serialVersionUID.
  • Entendre què és una base de dades orientada a objectes i comparar-la amb el model relacional.

Fins ara els nostres programes no guarden informació de forma persistent; quan tanques l’aplicació, tota la informació introduïda desapareix.

Per exemple, cada cop que arrencaves BiblioTech tornaves a tenir la biblioteca buida. En aquest apartat farem que les dades sobrevisquin al tancament del programa.

18.1 Què és la persistència?

Persistir vol dir conservar l’estat d’un objecte més enllà de la vida del programa que el va crear. Un objecte viu a la memòria RAM, que és volàtil: quan el procés acaba (o s’apaga l’ordinador), la RAM s’esborra. Per conservar les dades hem d’escriure-les en un suport durador: el disc, una base de dades, el núvol…

ConsellAnalogia

Un objecte a la memòria és com una conversa parlada: existeix mentre parles i després es perd.

Persistir és com prendre notes en un quadern: encara que marxis, les notes segueixen allà quan tornes.

%%{init: {'flowchart': {'rankSpacing': 250, 'nodeSpacing': 50}}}%%
flowchart LR
    A["Objecte<br/>en memòria (RAM)"] -->|"escriure o serialitzar"| B[("Suport durador<br/>Disc o BD")]
    B -->|"llegir o deserialitzar"| A

    classDef blauSuau fill:#cfe2ff,stroke:#0d6efd,stroke-width:1.5px,color:#084298;
    class B blauSuau;

Hi ha diverses estratègies per persistir. En aquest capítol veiem la més directa des de Java —la serialització d’objectes— i al següent veurem la més habitual al món professional: les bases de dades relacionals amb JDBC.

Estratègia Idea Quan va bé
Fitxers de text Escrivim camps com a text (CSV, línies…) Dades senzilles, llegibles per humans
Serialització d’objectes Java guarda l’objecte sencer en binari Guardar/recuperar ràpid des de la mateixa app Java
Base de dades relacional Taules, files i SQL Molts registres, consultes, concurrència
Base de dades orientada a objectes Guarda objectes directament Models molt orientats a objectes

18.2 La serialització a Java

Serialitzar és convertir un objecte (i tot el que conté) en una seqüència de bytes que es pot escriure en un fitxer o enviar per la xarxa. El procés invers —reconstruir l’objecte a partir dels bytes— s’anomena deserialitzar.

flowchart LR
    O["Objecte Llibre<br/>titol, isbn, anys"] -->|serialitzar| S["01001010...<br/>(bytes)"]
    S -->|"desar en fitxer"| F[("llibres.dat")]
    F -->|"llegir"| S2["01001010...<br/>(bytes)"]
    S2 -->|deserialitzar| O2["Objecte Llibre<br/>reconstruït"]

Perquè una classe es pugui serialitzar només cal que implementi la interfície Serializable.

És una interfície de marcatge: no té cap mètode, només indica a Java que autoritzem a convertir els seus objectes en bytes.

import java.io.Serializable;

public class Llibre implements Serializable {

    // Identificador de versió de la classe (recomanat, vegeu més avall)
    private static final long serialVersionUID = 1L;

    private String titol;
    private String isbn;
    private int anyPublicacio;

    public Llibre(String titol, String isbn, int anyPublicacio) {
        this.titol = titol;
        this.isbn = isbn;
        this.anyPublicacio = anyPublicacio;
    }

    @Override
    public String toString() {
        return titol + " (" + anyPublicacio + ") ISBN " + isbn;
    }
}


ImportantRegla d’or de la serialització

Perquè un objecte es pugui serialitzar, tots els seus atributs també han de ser serialitzables.

Els tipus primitius (int, double, boolean…) i String ja ho són.

Si Llibre tingués un atribut d’una classe pròpia (per exemple Autor autor), la classe Autor també hauria d’implementar Serializable o obtindràs una NotSerializableException en temps d’execució.

Escriure objectes

Per guardar un objecte fem servir un ObjectOutputStream encadenat a un FileOutputStream.

El mètode clau és writeObject.

import java.io.*;

public class GuardarLlibre {
    public static void main(String[] args) {
        Llibre llibre = new Llibre("El nom del vent", "978-8401352836", 2007);

        // try-with-resources: tanca el flux automàticament
        try (ObjectOutputStream sortida =
                 new ObjectOutputStream(new FileOutputStream("llibre.dat"))) {

            sortida.writeObject(llibre);
            System.out.println("Objecte guardat correctament.");

        } catch (IOException e) {
            System.out.println("Error en guardar: " + e.getMessage());
        }
    }
}

El fitxer llibre.dat és binari: si l’obres amb un editor de text veuràs símbols estranys. No està pensat per llegir-lo a mà, sinó per tornar-lo a carregar des de Java.

Llegir objectes

La lectura és simètrica. readObject retorna un Object, així que hem de fer un càsting al tipus real.

import java.io.*;

public class CarregarLlibre {
    public static void main(String[] args) {
        try (ObjectInputStream entrada =
                 new ObjectInputStream(new FileInputStream("llibre.dat"))) {

            Llibre llibre = (Llibre) entrada.readObject();                          // Càsting necessari
            System.out.println("Objecte recuperat: " + llibre);

        } catch (FileNotFoundException e) {
            System.out.println("El fitxer no existeix encara.");
        } catch (IOException | ClassNotFoundException e) {
            System.out.println("Error en carregar: " + e.getMessage());
        }
    }
}
AlertaError típic: ClassNotFoundException

readObject pot llançar ClassNotFoundException perquè, en deserialitzar, la JVM necessita trobar la classe Llibre per reconstruir l’objecte.

Si has canviat el paquet o el nom de la classe entre guardar i carregar, no la trobarà. Recorda capturar aquesta excepció, com al codi anterior.

Guardar diversos objectes alhora

El més habitual no és guardar un objecte sol, sinó una col·lecció sencera.

Com que ArrayList ja implementa Serializable, podem serialitzar tota la llista d’un sol cop:

import java.io.*;
import java.util.*;

public class PersistenciaBiblioteca {

    public static void desar(List<Llibre> llibres, String fitxer) throws IOException {
        try (ObjectOutputStream out =
                 new ObjectOutputStream(new FileOutputStream(fitxer))) {
            out.writeObject(llibres);               // Guardem la llista sencera
        }
    }

    @SuppressWarnings("unchecked")
    public static List<Llibre> carregar(String fitxer) throws IOException, ClassNotFoundException {
        File f = new File(fitxer);
        if (!f.exists()) {
            return new ArrayList<>();               // Primera execució: llista buida
        }
        try (ObjectInputStream in =
                 new ObjectInputStream(new FileInputStream(fitxer))) {
            return (List<Llibre>) in.readObject();
        }
    }
}

Fixa’t com aquest patró resol el problema de la pèrdua de la informació en memòria:

  • En arrencar cridem carregar (i si no hi ha fitxer, comencem amb la llista buida).
  • En tancar cridem desar.

18.3 Controlar la serialització

A vegades no volem guardar un atribut: perquè és sensible (una contrasenya), perquè és un valor calculat, o perquè no té sentit fora d’aquesta execució (una connexió a la xarxa).

La paraula clau transient marca un camp perquè s’ometi en serialitzar.

public class Usuari implements Serializable {
    private static final long serialVersionUID = 1L;

    private String nom;
    private transient String contrasenya;               // NO es guarda al fitxer
    private transient int intentsFallits;               // Valor de sessió, no persistent
}

En deserialitzar, un camp transient recupera el seu valor per defecte (null per a objectes, 0 per a numèrics, false per a booleans).

Identificador serialVersionUID

Quan Java serialitza una classe, hi grava un número de versió: el serialVersionUID.

En deserialitzar, comprova que el número del fitxer coincideixi amb el de la classe actual. Si no declares aquest camp, Java en calcula un automàticament a partir de l’estructura de la classe… i canvia si modifiques la classe.

Resultat: un fitxer guardat ahir pot deixar de carregar-se avui amb una InvalidClassException.

ConsellBona pràctica

Declara sempre un serialVersionUID explícit a les classes serialitzables:

private static final long serialVersionUID = 1L;

Així controles tu la versió i evites sorpreses quan la classe evoluciona.

Un cas complet resumit

%%{init: {'sequence': {'actorMargin': 90, 'messageMargin': 50, 'noteMargin': 15, 'diagramMarginX': 60, 'diagramMarginY': 40, 'wrap': true, 'wrapPadding': 5, 'messageAlign': 'center'}, 'themeVariables': {'actorBkg': '#e9f2fb', 'actorBorder': '#2e75b6', 'actorTextColor': '#084298', 'signalColor': '#333333', 'signalTextColor': '#333333', 'noteBkgColor': '#fff3cd', 'noteBorderColor': '#ffd43b', 'noteTextColor': '#664d03', 'labelBoxBkgColor': '#e9f2fb', 'labelBoxBorderColor': '#2e75b6', 'labelTextColor': '#084298'}}}%%
sequenceDiagram
    participant App as Aplicació
    participant OOS as ObjectOutputStream
    participant Disc as llibres.dat
    App->>OOS: writeObject(llista)
    OOS->>Disc: bytes serialitzats
    Note over App,Disc: --- L'aplicació es tanca ---
    App->>Disc: nova execució
    Disc->>App: readObject() → llista reconstruïda

18.4 Bases de dades orientades a objectes

La serialització està molt bé per a aplicacions petites però presenta alguns límits. NO permet:

  • Cercar dins les dades sense carregar-ho tot.
  • Que hi accedeixin diversos usuaris alhora.
  • Fer consultes complexes.

Per a això existeixen les bases de dades.

Una base de dades orientada a objectes (BDOO) guarda directament els objectes —amb els seus atributs, mètodes i relacions— sense haver de trencar-los en taules. Exemples: db4o, ObjectDB, Versant.

L’alternativa, i la dominant al món professional, és la base de dades relacional, on la informació es desa en taules de files i columnes i es consulta amb SQL.

Aspecte BD orientada a objectes BD relacional
Unitat d’emmagatzematge Objecte (amb atributs i relacions) Fila d’una taula
Model Igual que el codi (classes, herència) Taules, files, columnes
Llenguatge de consulta API d’objectes / consultes pròpies SQL (estàndard)
Desajust amb el codi OO Cap: guardes objectes tal qual Cal traduir objecte ↔︎ taula
Maduresa i suport Nínxol, poques eines Enorme: molts SGBD, eines, comunitat
Escalabilitat i concurrència Limitada Molt provada
Exemples db4o, ObjectDB SQLite, MySQL, PostgreSQL, Oracle
NotaEl desajust objecte-relacional

Quan guardes objectes Java en una base de dades relacional, has de traduir cada objecte a files i columnes i a l’inrevés. Es coneix com a desajust d’impedància.

  • Les BDOO l’eviten guardant l’objecte directament.
  • Els frameworks ORM (com Hibernate o JPA) l’automatitzen sobre bases de dades relacionals.

En aquest curs el resoldrem manualment amb el patró DAO.

18.5 Errors típics

Alerta
  • Oblidar implements SerializableNotSerializableException en cridar writeObject.
  • Un atribut no serialitzable (una classe pròpia sense Serializable) → també NotSerializableException.
  • No capturar ClassNotFoundException en llegir → no compila.
  • No comprovar si el fitxer existeix abans de carregar → FileNotFoundException a la primera execució.
  • Canviar la classe sense serialVersionUIDInvalidClassException en carregar fitxers vells.

18.6 Per practicar

  1. Fes que la classe Soci (nom, número de carnet, transient multa acumulada) sigui serialitzable amb serialVersionUID.
  2. Escriu dos mètodes desarSocis(List<Soci>, String) i carregarSocis(String) seguint el patró de PersistenciaBiblioteca.
  3. Guarda una llista de 3 socis, tanca el programa, torna’l a executar i comprova que els recuperes. Verifica que la multa (transient) torna a valer 0.
  4. Explica amb les teves paraules per què una base de dades relacional s’escala millor que la serialització quan tens 100.000 llibres.

18.7 Resum

  • La persistència és conservar l’estat més enllà de l’execució.
  • Per serialitzar, usem Serializable + ObjectOutputStream/ObjectInputStream. Guarda objectes sencers en binari.
  • transient omet camps; serialVersionUID controla la versió.
  • Les BDOO guarden objectes; les BDR (relacionals) guarden taules i són l’estàndard professional actualment.