Serialització – Transferint Objectes
La serialització és el procés de convertir la informació d’estat d’una instància d’objecte en una forma binària o textual per persistir en un medi d’emmagatzematge o transportat per una xarxa.
És a dir, Serialitzar és convertir un objecte en una successió de bytes, amb l’objectiu de guardar-lo o enviar-lo per xarxa per a posteriorment reconstruir-lo.
La Deserialització seria el procés invers, tornar a reconstruir l’objecte a partir d’un flux de bytes. El flux de bytes creat és independent de la plataforma. Per tant, l’objecte serialitzat en una plataforma es pot deserialitzar en una plataforma diferent.
Avantatges de la serialització:
- Per desar/conservar l’estat d’un objecte.
- Viatjar un objecte a través d’una xarxa.
- Per a la comunicació entre processos. Com per exemple, en la comunicació entre un servidor i un client.
- Per a la persistència de dades.
- Per a la clonació d’objectes, amb l’objectiu de crear una còpia exacta d’un objecte per a ser utilitzat en un altre context.
Inconvenients:
- Malauradament, el procés de serialització segueix unes configuracions específiques de Java, i no serveix per intercanviar dades amb programes escrits en altres llenguatges.
- Els canvis en el codi dels objectes poden fer que no es pugui recuperar un objecte d’una versió anterior serialitzat.
- La serialització pot ser lenta i ineficient en termes de memòria, ja que es guarden tots els atributs de l’objecte, incloent els que no són necessaris.
Quines classes utilitzem en Java per a Serialitzar-Deserialitzar?
Aspecte | Descripció |
---|---|
Objecte Serialitzable | Interfície Serializable . |
Per a Serialitzar | ObjectOutputStream writeObject() |
Per a Deserialitzar | ObjectInputStream readObject() |
Objecte Serialitzable:
- Interfície java.io.Serializable: Els objectes a serialitzar han d’implementar la interfície
java.io.Serializable
. És una interfície sense mètodes, que només serveix com a marcador. Tots els tipus bàsics la implementen implícitament, i també la classe String i els contenidors i objectes Array.
Per a serialitzar:
- ObjectOutputStream
writeObject()
. Este mètode serialitza un objecte i el guarda en un fitxer.
Per a deserialitzar:
- ObjectInputStream
readObject()
. Este mètode deserialitza un objecte i el carrega en memòria.
Excepcions durant la serialització:
- NotSerializableException: Quan un objecte no implementa
java.io.Serializable.
- InvalidClassException: Quan hi ha problemes amb la classe a serialitzar.
- IOException: Qualsevol error que es puga produir en el stream.
Excepcions durant la deserialització:
- InvalidClassException: Quan hi ha problemes amb la classe a deserialitzar.
- ClassCastException: La classe deserialitzada no es correspon amb la classe que es vol deserialitzar, es a dir, no coincideixen els noms de les classes o els atributs o els tipus.
- ClassNotFoundException: No existeix codi compilat per la classe que s’està deserialitzant.
- IOException: Qualsevol error que es pugui produir en el stream.
Modificador de camp transient
En Java, el modificador de camp transient
s’utilitza per evitar que certs camps d’una classe es serialitzen. És a dir, quan un objecte és serialitzat (per exemple, emmagatzemat en un fitxer o enviat per la xarxa), els camps marcats com transient
no seran inclosos en aquest procés. Aquesta característica és útil per camps que són temporals, calculables o que no necessiten ser persistits.
-
Quan definim un camp transient, indiquem que, en cas de serialitzar l’objecte, el camp definit com transient, el seu valor no es serialitzarà, i en la deserialització prendran el valor per defecte.
-
Tampoc es serialitzen els camps static
Exemple:
Imagina una classe Usuari
que té informació sobre un usuari, incloent una contrasenya. No té sentit serialitzar la contrasenya perquè és informació sensible que no hauria de ser desada en el mateix format que altres dades.
import java.io.Serializable;
public class Usuari implements Serializable {
private String nom;
private transient String contrasenya; // No es serialitzarà
public Usuari(String nom, String contrasenya) {
this.nom = nom;
this.contrasenya = contrasenya;
}
@Override
public String toString() {
return "Nom: " + nom + ", Contrasenya: " + contrasenya;
}
}
En aquest exemple:
- La variable
nom
es serialitzarà normalment. - La variable
contrasenya
està marcada com atransient
, de manera que no es desarà quan es serialitzi l’objecte.
Serialització de l’objecte
Quan serialitzem un objecte d’aquesta classe, només el camp nom
serà inclòs. La contrasenya es “perd” perquè està marcada com a transient
.
import java.io.*;
public class ExempleSerialitzacio {
public static void main(String[] args) {
Usuari usuari = new Usuari("Joan", "superSecret123");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("usuari.dat"))) {
oos.writeObject(usuari);
} catch (IOException e) {
e.printStackTrace();
}
// Ara llegim l'objecte des del fitxer
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("usuari.dat"))) {
Usuari deserialitzat = (Usuari) ois.readObject();
System.out.println(deserialitzat); // La contrasenya serà null
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Resultat:
Quan llegeixis l’objecte des del fitxer, veuràs que el camp contrasenya
és null
, perquè no es va serialitzar:
Nom: Joan, Contrasenya: null
En definitiva, el modificador transient
s’utilitza per evitar la serialització de camps que no són necessaris o que són sensibles, com les contrasenyes. És una eina útil per optimitzar la serialització i protegir la privacitat de les dades.
Exemples de serialització
1. Objecte Serialitzable
public class Employee implements java.io.Serializable {
public String name;
public String address;
public transient int SSN;
// El camp transient, que no se serialitzarà, no podrem recuperar el valor en la deserialització.
public int number;
}
2. Serialització
import java.io.*;
public class SerializeDemo {
public static void main(String[] args) {
// Crea un objecte Employee
Employee e = new Employee();
e.name = "Reyan Ali";
e.address = "Phokka Kuan, Ambehta Peer";
e.SSN = 11122333;
e.number = 101;
// El serialitza
try (FileOutputStream fileOut = new FileOutputStream("employee.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(e);
System.out.printf("Serialized data is saved in employee.ser");
} catch (IOException i) {
i.printStackTrace();
}
}
}
3. Deserialització
import java.io.*;
public class DeserializeDemo {
public static void main(String[] args) {
Employee e = null;
// Deserialitza un objecte Employee
try (FileInputStream fileIn = new FileInputStream("employee.ser");
ObjectInputStream in = new ObjectInputStream(fileIn)) {
e = (Employee) in.readObject();
} catch (IOException i) {
i.printStackTrace();
} catch (ClassNotFoundException c) {
System.out.println("Employee class not found");
c.printStackTrace();
}
// El mostra a pantalla
System.out.println("Deserialized Employee...");
System.out.println("Name: " + e.name);
System.out.println("Address: " + e.address);
System.out.println("SSN: " + e.SSN);
System.out.println("Number: " + e.number);
}
}
L’eixida per pantalla seria:
Serialized data is saved in employee.ser
Deserialized Employee...
Name: Reyan Ali
Address: Phokka Kuan, Ambehta Peer
SSN: 0
Number: 101
En resum…
- Si una classe principal ha implementat una interfície serialitzable, la classe secundària no necessita implementar-la, però viceversa no és certa.
- Només els membres de dades no estàtiques es guarden mitjançant el procés de serialització.
- Els membres de dades estàtiques i els membres de dades transitories no es guarden mitjançant el procés de serialització.
- No es crida mai el constructor d’objecte quan es deserialitza un objecte.
- Els objectes associats han d’implementar una interfície serialitzable.
Exemple 2
import java.io.*;
class Demo implements java.io.Serializable {
public int a;
public String b;
// Constructor per defecte
public Demo(int a, String b) {
this.a = a;
this.b = b;
}
}
class Exemple2 {
public static void main(String[] args){
Demo object = new Demo(1, "Hola xics, bon dia");
String filename = "fitxer.ser";
// Serialització
try {
FileOutputStream file = new FileOutputStream(filename);
ObjectOutputStream out = new ObjectOutputStream(file);
out.writeObject(object);
out.close();
file.close();
System.out.println("L'Objecte ha segut serialitzat");
} catch(IOException ex) {
System.out.println("S'ha produït una IOException");
}
Demo object1 = null;
// Deserialització
try {
FileInputStream file = new FileInputStream(filename);
ObjectInputStream in = new ObjectInputStream(file);
object1 = (Demo) in.readObject();
in.close();
file.close();
System.out.println("L'Objecte ha segut deserialitzat ");
System.out.println("a = " + object1.a);
System.out.println("b = " + object1.b);
} catch(IOException ex) {
System.out.println("S'ha produït una IOException");
} catch(ClassNotFoundException ex) {
System.out.println("S'ha produït una ClassNotFoundException");
}
}
}
L’eixida per pantalla seria:
L'Objecte ha segut serialitzat
L'Objecte ha segut deserialitzat
a = 1
b = Hola xics, bon dia
Exemple 3
import java.io.*;
class Emp implements Serializable {
private static final long serialversionUID = 129348938L;
transient int a;
static int b;
String name;
int age;
// Constructor per defecte
public Emp(String name, int age, int a, int b) {
this.name = name;
this.age = age;
this.a = a;
this.b = b;
}
}
public class Exemple3 {
public static void printdata(Emp object1) {
System.out.println("name = " + object1.name);
System.out.println("age = " + object1.age);
System.out.println("a = " + object1.a);
System.out.println("b = " + object1.b);
}
public static void main(String[] args) {
Emp object = new Emp("ab", 20, 2, 1000);
String filename = "exemple3.txt";
// Serialització
try {
// Guardant un objecte en un File
FileOutputStream file = new FileOutputStream(filename);
ObjectOutputStream out = new ObjectOutputStream(file);
// Mètode per Serialitzar un Objecte
out.writeObject(object);
out.close();
file.close();
System.out.println("L'objecte ha sigut Serialitzat\n"
+ "Dades abans de la Deserialitzacio.");
printdata(object);
// Valor de la variable estàtica canviat
object.b = 2000;
} catch (IOException ex) {
System.out.println("S'ha produït una IOException");
}
// Deserialització
try {
// Llegint l'objecte des del fitxer
FileInputStream file = new FileInputStream(filename);
ObjectInputStream in = new ObjectInputStream(file);
// Mètode per deserialitzar un objecte
object = (Emp) in.readObject();
in.close();
file.close();
System.out.println("L'objecte ha sigut Deserialitzat\n"
+ "Dades despres de la Deserialitzacio.");
printdata(object);
} catch (IOException ex) {
System.out.println("S'ha produït una IOException");
} catch (ClassNotFoundException ex) {
System.out.println("S'ha produït una ClassNotFoundException");
}
}
}
L’eixida per pantalla seria:
L'objecte ha sigut Serialitzat
Dades abans de la Deserialitzacio.
name = ab
age = 20
a = 2
b = 1000
L'objecte ha sigut Deserialitzat
Dades despres de la Deserialitzacio.
name = ab
age = 20
a = 0
b = 2000
Descripció per a la sortida:
S’ha vist en deserialitzar l’objecte que els valors de a
i b
han canviat. La raó es deu al fet que a
es va marcar com a transitòria i b
com a estàtica.
-
En cas de variables transitòries: Una variable definida amb la paraula clau transient no es serialitza durant el procés de serialització. Aquesta variable s’inicialitzarà amb el valor predeterminat durant la deserialització. (Per exemple: per a objectes és
null
, per aint
és0
). -
En cas de variables estàtiques: Una variable definida amb la paraula clau static no es serialitza durant el procés de serialització. Aquesta variable es carregarà amb el valor actual definit a la classe durant la deserialització.
serialVersionUID
El serialVersionUID
és un camp estàtic que s’utilitza durant la serialització i deserialització d’objectes en Java per assegurar que la versió de la classe que està deserialitzant l’objecte sigui compatible amb la versió que es va utilitzar per serialitzar-lo. Si les versions no coincideixen, es llançarà una excepció de tipus InvalidClassException
.
serialVersionUID
: És un identificador de versió únic per a una classe serialitzable. Este identificador assegura que l’objecte deserialitzat és compatible amb la classe en què es deserialitza.
Exemple basat en l’exercici anterior:
En l’exercici anterior, la classe Emp
té el següent camp declarat:
private static final long serialversionUID = 129348938L;
Este serialVersionUID
garanteix que, quan es deserialitze l’objecte, es comprove si l’objecte desat (el que es va serialitzar) és compatible amb la classe actual (la que deserialitza). Si el valor de serialVersionUID
és diferent entre les dues versions, es produeix una excepció.
Per què és important el serialVersionUID?
Quan es modifica una classe, com per exemple afegint o eliminant atributs, això pot afectar el procés de deserialització. Si es canvia la definició de la classe però no es defineix un serialVersionUID
explícit, Java generarà automàticament un valor basat en la definició de la classe. Això vol dir que si la definició canvia, el serialVersionUID
generat també canviarà, i els objectes desats amb la versió antiga de la classe no es podran deserialitzar amb la nova versió, donant lloc a una InvalidClassException
.
Definir el serialVersionUID
manualment ajuda a mantenir la compatibilitat entre diferents versions d’una classe serialitzable.
Exemple de classe sense serialVersionUID:
class Emp implements Serializable {
int a;
static int b;
String name;
int age;
public Emp(String name, int age, int a, int b) {
this.name = name;
this.age = age;
this.a = a;
this.b = b;
}
}
En aquest cas, si modifiquem la classe Emp
afegint nous camps i intentem deserialitzar un objecte creat amb la versió antiga, obtindrem una InvalidClassException
perquè el serialVersionUID
generat automàticament per Java no coincidirà amb el de la nova versió.
Exemple d’excepció:
Exception in thread "main" java.io.InvalidClassException: Emp; local class incompatible:
stream classdesc serialVersionUID = 123456789, local class serialVersionUID = 987654321
Esta excepció indica que els valors del serialVersionUID
de la classe deserialitzada i de la classe local no coincideixen.
Com assignar un serialVersionUID?
-
Assignació manual: Pots definir un
serialVersionUID
manualment per assegurar que la classe manté la compatibilitat entre diferents versions.Exemple:
class Emp implements Serializable { private static final long serialVersionUID = 129348938L; int a; static int b; String name; int age; public Emp(String name, int age, int a, int b) { this.name = name; this.age = age; this.a = a; this.b = b; } }
-
Generació automàtica: Si no es defineix el
serialVersionUID
, Java en genera un automàticament basant-se en els atributs, mètodes, modificadors, etc. de la classe. Això no és recomanable si la classe està subjecta a modificacions, ja que podria trencar la compatibilitat en futures deserialitzacions.
Quan utilitzar un serialVersionUID?
- Sempre que una classe sigua serialitzable, és recomanable definir un
serialVersionUID
. Això proporciona control sobre la compatibilitat entre diferents versions de la classe. - És especialment important quan tens l’objectiu de mantenir la compatibilitat entre versions del programari en què els objectes serialitzats poden viatjar a través de diferents versions de la mateixa aplicació.
En Resum:
- El
serialVersionUID
és un identificador de versió únic per a una classe serialitzable. - Assegura la compatibilitat entre la classe serialitzada i la deserialitzada.
- Si no es defineix manualment, Java en genera un automàticament, cosa que pot provocar incompatibilitats si es modifica la classe.
- Definir un
serialVersionUID
manualment ajuda a evitar problemes de deserialització quan es fan canvis a la classe.
Exemple de deserialització fallida sense serialVersionUID:
Si afegíssim un nou camp a la classe Emp
sense definir un serialVersionUID
, un objecte creat amb la versió anterior de la classe llançaria una excepció quan intentem deserialitzar-lo:
class Emp implements Serializable {
private static final long serialVersionUID = 129348938L;
transient int a;
static int b;
String name;
int age;
// Nou camp afegit
String address;
}
Quan intentem deserialitzar un objecte creat amb la versió antiga de Emp
, si no hem definit un serialVersionUID
consistent, obtindrem una InvalidClassException
.
Definir i gestionar correctament el serialVersionUID
és una pràctica essencial per garantir la compatibilitat entre versions de classes serialitzables, especialment quan s’espera que les dades serialitzades es facen servir a llarg termini o es comparteixen entre versions diferents d’una aplicació.
EXERCICIS:
Exercici 1:
Realitza un programa on es demanen les teues notes de les assignatures de 1r de DAM i les guarde en un fitxer. A continuació, el programa llegirà el fitxer i calcularà la nota mitjana del curs.
A més, aquest exercici requereix que les notes i assignatures es serialitzen i deserialitzen utilitzant objectes. És a dir, les dades de les assignatures i les teues notes es guardaran en un fitxer mitjançant la serialització d’objectes i es recuperaran mitjançant la deserialització dels mateixos.
Requisits:
- Cadascuna de les assignatures serà un objecte que es troba en un array d’objectes de 6 posicions, els atributs del qual seran el nom i la nota.
- El nom de l’assignatura s’assignarà directament amb el constructor en crear l’objecte, mentre que l’atribut nota serà introduït per l’usuari a través d’un mètode que assegura que el valor estigui entre 0 i 10.
- Els objectes es serialitzaran en un fitxer i es deserialitzaran posteriorment per calcular la nota mitjana.
Exemple d’execució:
Introdueix la nota de Programació: 6,5
Introdueix la nota de Llenguatges de Marques: 7,5
Introdueix la nota de Bases de Dades: 7,5
Introdueix la nota d'Entorns de Desenvolupament: 8
Introdueix la nota de Sistemes Informàtics: 6,5
Finalment, Introdueix la nota de FOL: 6
Exemple d’eixida per pantalla:
***** Notes emmagatzemades a l'array *****
……… Bolcant l'array al fitxer mitjançant serialització ………
***** Bolcat finalitzat amb èxit *****
………Llegint el fitxer mitjançant deserialització i calculant mitjana………
La teua nota mitjana del curs va ser de: 7
Exercici 2:
Modifica l’exercici anterior utilitzant una col·lecció més flexible i eficient. Aquest exercici continua utilitzant la serialització i deserialització d’objectes, però ara les assignatures i les notes es gestionen amb una col·lecció del tipus HashMap<String, Double>
.
Realitza un programa on es demanen les teues notes de les assignatures de 1r de DAM i les guarde en un fitxer utilitzant un HashMap
. El programa llegirà el fitxer i calcularà la nota mitjana del curs.
A més, la col·lecció HashMap
es serialitzizarà i deserialitzarà per guardar i recuperar les dades d’assignatures i notes.
Requisits:
- Les assignatures i les seves notes s’emmagatzemaran en una col·lecció
HashMap
, on la clau és el nom de l’assignatura i el valor és la nota. - Controla que les notes tinguen un valor entre 0 i 10.
- El
HashMap
es serialitzarà en un fitxer i posteriorment es deserialitzarà per calcular la nota mitjana del curs.
Exemple d’execució:
Introdueix la nota de Programació: 6,5
Introdueix la nota de Llenguatges de Marques: 7,5
Introdueix la nota de Bases de Dades: 7,5
Introdueix la nota d'Entorns de Desenvolupament: 8
Introdueix la nota de Sistemes Informàtics: 6,5
Finalment, Introdueix la nota de FOL: 6
Exemple d’eixida per pantalla:
***** Notes emmagatzemades en el HashMap *****
……… Bolcant el HashMap al fitxer mitjançant serialització ………
***** Bolcat finalitzat amb èxit *****
………Llegint el fitxer mitjançant deserialització i calculant mitjana………
La teua nota mitjana del curs va ser de: 7