%%{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;
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
ObjectOutputStreamiObjectInputStream. - Controlar la serialització amb
transientiserialVersionUID. - 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…
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.
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;
}
}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());
}
}
}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.
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 |
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
- Oblidar
implements Serializable→NotSerializableExceptionen cridarwriteObject. - Un atribut no serialitzable (una classe pròpia sense
Serializable) → tambéNotSerializableException. - No capturar
ClassNotFoundExceptionen llegir → no compila. - No comprovar si el fitxer existeix abans de carregar →
FileNotFoundExceptiona la primera execució. - Canviar la classe sense
serialVersionUID→InvalidClassExceptionen carregar fitxers vells.
18.6 Per practicar
- Fes que la classe
Soci(nom, número de carnet,transientmulta acumulada) sigui serialitzable ambserialVersionUID. - Escriu dos mètodes
desarSocis(List<Soci>, String)icarregarSocis(String)seguint el patró dePersistenciaBiblioteca. - 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 valer0. - 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. transientomet camps;serialVersionUIDcontrola la versió.- Les BDOO guarden objectes; les BDR (relacionals) guarden taules i són l’estàndard professional actualment.