Serialización
Nesta páxina imos ver como recuperar obxectos dun ficheiro e tamén como almacenalos nel.
¿Que é a serialización de obxectos?
A serialización é un proceso de transformación que nos permite codificar obxectos da nosa arquitectura orientada a obxectos. Esta codificación sírvenos para transmitir ou almacenar obxectos. No noso caso vamos a traballar nunha forma de almacenamento e recuperación de obxectos en ficheiros, pero a serialización tamén é usada en sockets, en modelos cliente servidor etc… e decir fluxos (stream).
A necesidade de serialización xurde no propio paradigma de programación orientada a obxectos. E moito máis cómodo e natural andar a manexar obxectos, que non datos primitivos ou cadeas de alfanuméricos obtidos de ficheiros. Entón Java permítenos gardar e recuperar obxectos en ficheiros binarios. Para a serialización de obxectos (que non é máis que escribir ou ler un obxecto completo) fai falla un obxecto serializable: unha secuencia de bytes que contén datos del propio obxecto e metadatos do mesmo. Estes metadatos son o tipo de obxecto que é e a sua arquitectura interna (datos primitivos ou referencias a outros obxectos). Para indicar a Java que un obxecto é serializable a sua clase ten que implementar a interfaz Serializable, desta forma:
Public class Agenda implements Serializable {
private String nombre;
private String p_Apellido;
private transient String s_Apellido;
…
…
public Agenda(String nombre, String p_Apellido, String s_Apellido){
this.nombre = nombre;
this.p_Apellido = p_Apellido;
this.s_Apellido = s_Apellido;
}}
Aspectos útiles a ter en conta na serialización:
- Coa herdanza as clases fillo son tamén serializable se o son no pai.
- Todo os atributos de clase tipo primitivo son serializables.
- Se non se desexa serializar algún atributo úsase o modificador transient.
- Unha interfaz non se pode serializar.
- As colecciones de obxectos pódense serializar.
- SerialVersionUID: é un identificador de clases cando serializamos os seus obxectos. Vai ser un atributo da clase de tipo long, estático e inmutable (final): private static final long serialVersionUID = 4L;
Para todo o proceso de serialización e deserialización temos dúas clases ObjectInputStream e ObjectOutputStream, que se expoñen a continuación.
Escritura e serialización de obxectos
Na escritura de un obxecto nun ficheiro necesitamos crear un fluxo de saída primeiro. O máis frecuente é coa clase FileOutputStream, a partires do fluxo xa podemos crear un obxecto ObjectOutputStream, que ten o método de escritura: void writeObject(Object o): método de escritura dun obxecto no fluxo que se lle pasa como parámetro. Exemplo:
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.NoSuchElementException;
import java.util.Scanner;
//Clase para serializar e gardar obxectos da clase Account
public class CreateSequentialFile
{
private static ObjectOutputStream output; // obxecto con fluxo de saída
public static void main(String[] args)
{
openFile();
addRecords();
closeFile();
}
//método de abrir ficheiro
public static void openFile()
{
try {
output = new ObjectOutputStream(
Files.newOutputStream(Paths.get("clients.ser")));
}
catch (IOException ioException)
{
System.err.println("Error na apertura de ficheiro. Pechando.");
System.exit(1);
}
}
// método de agregar obxectos
public static void addRecords()
{
boolean fin= false;
Scanner input = new Scanner(System.in);
System.out.println("Introduza o número de conta, primer nome, apelido e balance.");
while (!fin)
{
try
{
// crease un novo obxecto; asúmese datos correctos
Account record = new Account(input.nextInt(),
input.next(), input.next(), input.nextDouble());
// serializa e garda o obxecto no ficheiro
output.writeObject(record);
System.out.println("Desexa continuar? S|N");
String sair= input.next();
if (sair.equalsIgnoreCase("N")) fin = true;
}
catch (NoSuchElementException elementException)
{
System.err.println("Datos incorrectos.");
input.nextLine(); // descartamos datos de entrada
}
catch (IOException ioException)
{
System.err.println("Error na escritura. Pechando.");
break;
}
}
}
//método de pechar ficheiro
public static void closeFile()
{
try
{
if (output != null)
output.close();
}
catch (IOException ioException)
{
System.err.println("Erro pechando ficheiro. Pechando.");
} } }
Pojo do exemplo:
import java.io.Serializable;
// clase na que os seus obxectos gardaranse nun ficheiro
public class Account implements Serializable
{
private int account;
private String firstName;
private String lastName;
private double balance;
// inicializacion do obxecto cos valores por defecto
public Account()
{
this(0, "", "", 0.0);
}
public Account(int account, String firstName,
String lastName, double balance)
{
this.account = account;
this.firstName = firstName;
this.lastName = lastName;
this.balance = balance;
}
public void setAccount(int acct)
{
this.account = account;
}
public int getAccount()
{
return account;
}
public void setFirstName(String firstName)
{
this.firstName = firstName;
}
public String getFirstName()
{
return firstName;
}
public void setLastName(String lastName)
{
this.lastName = lastName;
}
public String getLastName()
{
return lastName;
}
public void setBalance(double balance)
{
this.balance = balance;
}
public double getBalance()
{
return balance;
}
}
Lectura e deserialización de obxectos
A lectura é moi semellante a escritura; é necesario crear un obxecto de lectura de fluxo co FileInputStream. Despois xeramos ObjectInputStream que ten o método de lectura: Object readObject(): obtense un obxecto do ficheiro e son deserializados, de maneira secuencial, un detrás doutro. Exemplo:
import java.io.EOFException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
//Clase para deserializar e ler obxectos clase Account do ficheiro
public class ReadSequentialFile
{
private static ObjectInputStream input;
public static void main(String[] args)
{
openFile();
readRecords();
closeFile();
}
// abrir ficheiro
public static void openFile()
{
try
{
input = new ObjectInputStream(
Files.newInputStream(Paths.get("clients.ser")));
}
catch (IOException ioException)
{
System.err.println("Erro abrindo ficheiro.");
System.exit(1);
}
}
// método de ler obxectos
public static void readRecords()
{
System.out.printf("%-10s%-12s%-12s%10s%n", "Conta",
"Primer nome", "Apelidos", "Balance");
try
{
while (true) // bucle ata EOFException
{
// lectura e cast
Account record = (Account) input.readObject();
// mostramos os datos
System.out.printf("%-10d%-12s%-12s%10.2f%n",
record.getAccount(), record.getFirstName(),
record.getLastName(), record.getBalance());
}
}
catch (EOFException endOfFileException)
{
System.out.printf("%nNon hai mais datos.%n");
}
// Excepcion que xorde cando nos chega un obxecto que non esperamos
catch (ClassNotFoundException classNotFoundException)
{
System.err.println("Tipo de obxecto inválido.");
}
catch (IOException ioException)
{
System.err.println("Erro lendo o ficheiro.");
}
}
// método de peche
public static void closeFile()
{
try
{
if (input != null)
input.close();
}
catch (IOException ioException)
{
System.err.println("Error closing file. Terminating.");
System.exit(1);
}
} }
Problema na sobrescritura de ficheiros na serialización
Se intentas abrir e agregar novos obxectos nun ficheiro xa utilizado, é decir agregar obxectos, o máis probable é que cando queras ler che apareza o seguinte erro: Error → IOException: invalid type code: AC É provocado porque se crea unha cabeceira extra sempre que se abre o ficheiro en modo escritura. Esa cabeceira ten a información dos metadatos dos obxectos almacenados e non pode estar duplicada. Para arranxar este problema hai que programar un propio ObjectOutputStream e sobrescribir o método writeStreamHeader:
import java.io.*;
//clase de proba
class Persona implements Serializable{
String nombre;
int edad;
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
}
// Creamos o noso ObjectOutputStream
class MiObjectOutputStream extends ObjectOutputStream{
/** Constructor que recibe OutputStream */
public MiObjectOutputStream(FileOutputStream out) throws IOException{
super(out);
}
//Re definición do método de escribir a cabeceira para que non faga nada. */
@Override
public void writeStreamHeader() throws IOException{
}
}
class SobreEscrituraSer{
public static void main(String[] args) throws IOException, ClassNotFoundException{
//Primeira escritura normal
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("persoas.dat"));
Persona p;
p = new Persona("Juan Lopez", 30);
oos.writeObject(p);
oos.close();
//Aa segunda e de adelante usamos noso MiobjectOutputStream
//Parametro true para a agregación de objetos
MiObjectOutputStream moos = new MiObjectOutputStream(new FileOutputStream("persoas.dat",true));
p = new Persona("Jose Fernandez", 28);
moos.writeObject(p);
moos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("persoas.dat"));
p=(Persona)ois.readObject();
System.out.println(p.nombre+", "+p.edad);
p=(Persona)ois.readObject();
System.out.println(p.nombre+", "+p.edad);
ois.close();
}
}
Aspectos importantes a ter en conta na serialización
- Os datos primitivos sempre son serializables, non fai falta implementar a interfaz.
- Si hai composición de clases, todas as clases implicadas deben ser serializables:
import java.io.*;
class Punto{
int x;
int y;
public Punto(int x, int y) {
this.x = x;
this.y = y;
}
}
class Rectangulo implements Serializable{
Punto origen;
int base;
int altura;
Rectangulo(int x, int y, int base, int altura){
origen=new Punto(x,y);
this.base=base;
this.altura=altura;
}
public String toString(){
return origen.x+","+origen.y+","+base+","+altura;
}
}
class Unidad6{
public static void main(String[] args) throws IOException, ClassNotFoundException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("rectangulos.dat"));
Rectangulo r;
r = new Rectangulo(0,0,2,3);
oos.writeObject(r);
r = new Rectangulo(2,3,10,10);
oos.writeObject(r);
oos.close();
}
}
Xenera este error: Exception in thread “main” java.io.NotSerializableException: Punto
- En herencia é suficiente que a clase pai ou base sexa serializable para que os fillos o sexan.
- Menos as interfaces todas as coleccións típicas son serializables.
- Si un atributo da clase non desexas que se serialice (motivos de seguridade por exemplo), palabra reservada transient.
- Podemos xenerar un id para a serialización das clases. Chámase SerialVersionUID é o mellor que sexa tipo long, static e final. Proba a xeneralo dende o IDE ou podes asignalo a man así:
private static final long serialVersionUID = 1L;
Exercicio 1
Crea un programa que almacene nunha lista obxectos Employee e se almacene nun ficheiro. Logo debes recuperar esa lista de obxectos do ficheiro. Employee.java:
import java.io.Serializable;
// Sempre aínda que se introduza nunha colección
public class Employee implements Serializable {
String id;
String firstName;
String lastName;
public Employee(String id, String firstName, String lastName) {
super();
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return "Employee [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + "]";
}
}
Exercicio 2
Partindo de este exercicio de mapas enlace Serializa o mapa cos datos e gárdaos nun ficheiro. A continuación deserealiza dende o ficheiro e lee o mapa.
Exercicio 3
Temos que gardar obxectos dunha clase persoa co serialVersionUID, password e nome. Pero nos dicen que por motivos de seguridad o campo password no se pode gardar. Fai a proba de gardar e despois ler do ficheiro.
Exercicio extra (podeis usar coleccións)
Quérese gardar información sobre animais para realizar un xogo infantil. Os animais a mostrar no xogo son cans ou gatos.
A información que se quere gardar é o nome e do animal e a sua idade.
No caso dos cans, quérese saber se teñen o rabo longo ou non, e no caso dos gatos se teñen bigotes longos ou non.
Tamén se desexa saber a forma de expresarse de cada un. Os gatos farán “MIAU” e os cans “GUAU”.
Vamos a necesitar un método (getTipo) que nos indique que clase de animal é (can ou gato) xa que haberá momentos no que traballaremos con animais.
Unha vez definidas as clases para gardar a información anterior, crea una clase de nome OperacionesAnimales: Dicha clase poderá gardar información sobre cinco Cans e cinco gatos. Debes facer uso de arrays estáticos de tamaño 5. Disporá dun método addPerro(Perro perro) que permitirá agregar un perro (dos cinco posibles). Necesitarás un contador que che indique en qué posición do array debes de añadir o perro novo. Dicho método devolverá un boolean indicando se o can foi engadido correctamente. Se intentamos engadir un novo can e o array está ocupado, devolverá false.
O mesmo para o caso dos gatos método (addGato(Gato gato)):
- Disporá dun método gardarAnimaisADisco(String fichero) no que se gardarán todos os obxectos que se encontren no array (perros / gatos). Escribiremos no ficheiro, antes dos obxectos, o número de obxectos total que imos a escribir.
- Disporá dun método lerAnimaisDisco(String fichero) que lerá de disco os obxectos gardados do paso anterior. Primeiro leremos o número de animais gardados no disco para dar memoria o array que debe devolver o método. Despois leremos todos os animais que iranse gardando no array que temos que devolver. Se actualizarán os datos da clase (o array que gardan os gatos e os cans).