Vamos con un ejemplo tonto de cómo escribir objetos en un fichero y leerlos luego.
Usaremos un ObjetOutputStream y al final aprovecharé para comentar un par de cosas raras que he visto en esta clase y que algo tan sencillo como leer y escribir de un fichero, nos pueda traer de cabeza durante un buen rato.
Primero definimos las clases de datos que vamos a escribir y leer en el fichero. Estas clases deben implementar la interface Serializable. También todos los atributos de estas clases deben ser tipos primitivos (int, double, float, etc) o bien clases que a su vez implementen la interface Serializable.
Implementar esta interface es sencillo. Simplemente ponemos que la implementa y ya está, no es necesario implementar ningún método.
Como clases para el ejemplo vamos a usar una clase Persona con una serie de datos y que a su vez, dentro, tiene una clase Mascota, también con una serie de datos.
Estas son las clases:
Aunque por simplicidad no los he puesto, si descargas los fuentes verás que he puesto un constructor y un método setPersona() para rellenar fácilmente los campos. Lo importante de esta clase es que implementa Serializable y que todos sus atributos (incluido Mascota), también.
Para escribir en el fichero, simplemente hay que crear un ObjectOutputStream sobre el fichero
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fichero));
Y ahora hay que ir instanciando datos y metiéndolos en el ObjectOutputStream. Ojo, si no quieres problemas raros, haz un new por cada objeto que quieras meter, no reaproveches la misma instancia cambiándole los datos.
for (int i = 0; i <5; i++)
{
// ojo, se hace un new
por cada Persona. El new dentro del bucle.
Persona p = new Persona(i);
oos.writeObject(p);
}
oos.close(); // Se cierra al terminar.
Para leer del fichero, creamos un ObjectInputStream sobre el fichero
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fichero));
y nos dedicamos a ir leyendo objetos.
// Se lee el primer objeto
Object aux = ois.readObject();
// Mientras haya objetos
while (aux!=null)
{
if (aux instanceof Persona)
System.out.println(aux);
// Se escribe en pantalla el objeto
aux = ois.readObject();
}
ois.close();
Hay una cosa curiosa con el ObjectOutputStream. Supongo que por hacerlo eficiente, cuando le damos un objeto para escribir, es como si guardara el array de bytes en el interior. Si cambiamos los valores de los atributos de ese objeto y volvemos a escribirlo ... el ObjetOutputStream lo escribe nuevamente, pero con los datos antiguos. Da la impresión de que no se entera del cambio y no recalcula los bytes que va a escribir en el fichero. Si escribimos así, con un solo new
Persona p = new Persona(0); //
Un único new fuera del bucle
for (int i = 0; i <5; i++)
{
p.setPersona(i); //
cambiamos los datos de p, pero no hacemos new.
oos.writeObject(p);
}
oos.close(); // Se cierra al terminar.
Cuando leamos, obtendremos cinco veces la primera persona.
Esto puede evitarse de tres formas:
Un segundo problema que he visto en el ObjectOutputStream es que al instanciarlo, escribe unos bytes de cabecera en el fichero, antes incluso de que escribamos nada. Como el ObjectInputStream lee correctamente estos bytes de cabecera, aparentemente no pasa nada y ni siquiera nos enteramos que existen.
El problema se presenta si escribimos unos datos en el fichero y lo cerramos. Luego volvemos a abrirlo para añadir datos, creando un nuevo ObjectOutputStream así
/* El true del final indica
que se abre el fichero para añadir datos al final del fichero.*/
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fichero,true));
Esto escribe una nueva cabecera justo al final del fichero. Luego se irán añadiendo los objetos que vayamos escribiendo. El fichero contendrá lo del dibujo, con dos cabeceras.
Primera sesión
con el fichero |
Segunda sesión
con el fichero. Le añadimos datos |
|||||||
cabecera |
Persona |
Persona |
Persona |
Persona |
cabecera |
Persona |
Persona | Persona |
¿Qué pasa cuando leamos el fichero?. Al crear el ObjectInputStream,
este lee la cabecera del principio y luego se pone a leer objetos. Cuando
llegamos a la segunda cabecera que se añadió al abrir por
segunda vez el fichero para añadirle datos, obtendremos un error
StreamCorruptedException y no podremos leer más
objetos.
Una solución es evidente, no usar más que un solo ObjectOuptutStream para escribir todo el fichero. Sin embargo, esto no es siermpre posible. Por ejemplo, si nuestro programa es una agenda, un día escribimos tres amigos, cerramos la agenda, apagamos el ordenador y nos vamos de juerga. Al día siguiente, queremos meter a los dos borrachines que conocimos en la juerga anterior o a la chica con la que creemos que hemos ligado, encendemos el ordenador, arrancamos la agenda y por más que buscamos, de nuestro antiguo ObjectOutputStream no queda ni rastro. Hay que abrir uno nuevo. No se puede pretender en una agenda que metamos a todos nuestros amigos de una sola vez y que no volvamos a meter a nadie más.
La única solución que he encontrado (que seguramente no es la única) es hacernos nuestro propio ObjectOutputStream, heredando del original y redefiniendo el método writeStreamHeader() como en la figura, vacío, para que no haga nada.
protected void writeStreamHeader()
throws IOException
{
// No hacer nada.
}
En ejemplo.zip tienes un pequeño programa con lo comentado hasta aquí: Las clases Persona y Mascota. Un MiObjectOutputStream con el método writeStreamHeader() redefinido para que no haga nada y una clase con main() que escribe 10 Personas en un fichero y las lee.
Esta última clase escribe las 5 primeras personas de la forma normal y cierra el fichero. Luego lo vuelve a abrir y escribe otras 5, usando los trucos aquí mencionados: un ObjectOutputStream sin cabecera y el método writeUnshared().