Você está na página 1de 30

Ficheros

Autor: Antonio J. Martín


ÍNDICE

1. INTRODUCCIÓN....................................................................................................... 3
1.1. TIPOS DE FICHEROS ............................................................................................. 3

1.2. EL PAQUETE JAVA.IO ............................................................................................ 3

2. MANIPULACIÓN DE FICHEROS DE TEXTO.......................................................... 4


2.1. LECTURA DE CADENAS DE UN FICHERO DE TEXTO .................................................. 4

2.1.1. Lectura de los datos .................................................................................... 5

2.1.2. Cierre del stream ......................................................................................... 5

EJERCICIO 1.................................................................................................... 5

2.2. ESCRITURA EN UN ARCHIVO DE TEXTO .................................................................. 7

2.2.1. La clase PrintWriter ..................................................................................... 7

2.2.2. La clase FileWriter ....................................................................................... 7

EJERCICIO 2.................................................................................................... 8

3. ALMACENAMIENTO DE TIPOS PRIMITIVOS EN UN FICHERO BINARIO ........ 10


3.1. ESCRITURA DE DATOS ........................................................................................ 11

3.1.1. Métodos de escritura ................................................................................. 11

3.2. RECUPERACIÓN DE DATOS ................................................................................. 12

3.2.1. Métodos de lectura .................................................................................... 12

EJERCICIO 3.................................................................................................. 13

4. ALMACENAMIENTO DE OBJETOS EN DISCO ................................................... 16


4.1. LA INTERFAZ SERIALIZABLE ................................................................................ 16

4.2. ESCRITURA DEL OBJETO ..................................................................................... 18

4.2.1. Métodos de escritura ................................................................................. 18

4.3. LECTURA DE OBJETOS ........................................................................................ 18

EJERCICIO 4.................................................................................................. 19

5. ACCESO ALEATORIO A UN FICHERO. CLASE RANDOMACCESSFILE ......... 24


5.1. MÉTODOS DE LA CLASE RANDOMACCESSFILE ..................................................... 25

EJERCICIO 5.................................................................................................. 25
1. INTRODUCCIÓN
En las aplicaciones que hemos realizado hasta el momento, los datos que se han
manejado tenían un periodo de vida muy corto, limitado como máximo a la duración del tiempo
de ejecución de la aplicación.

Pero en la mayoría de las aplicaciones reales se trabaja con datos existentes en algún
dispositivo de almacenamiento permanente, como un fichero o una base de datos. Estos
programas, deberán tener capacidad tanto para recuperar datos de estos dispositivos, como
para almacenar información en los mismos.

En este tema vamos a tratar el acceso a ficheros, dejando para temas posteriores el
tratamiento de bases de datos, pues las clases que proporciona Java para trabajar con uno y
otro tipo de dispositivo de almacenamiento son diferentes.

1.1. TIPOS DE FICHEROS

Existen diferentes clasificaciones que se pueden realizar de los ficheros que


determinan el tipo de clases que se utilizarán para su manipulación.

Según el tipo de información que almacenan, los ficheros se pueden clasificar:

ƒ Ficheros de texto. Almacenan información en modo texto. La ventaja que


tienen este tipo de ficheros es la compatibilidad entre lenguajes de
programación. Por ejemplo, un fichero de texto cuyos datos hayan sido
grabados desde una aplicación Java podrá ser leído posteriormente por una
aplicación C# o VB.NET y viceversa.

ƒ Ficheros binarios. En un fichero binario la información se almacena en un


formato nativo que solo puede ser recuperada utilizando el lenguaje o
tecnología con la que se grabó. La ventaja de este tipo de ficheros es que en
ellos podemos grabar directamente los datos en el formato en que están
siendo manejados por el programa, es decir, como un tipo primitivo o como un
objeto Java.

Según la forma de acceso al fichero, los podemos clasificar:

ƒ Ficheros de acceso secuencial. El acceso al fichero se realiza desde el


comienzo del mismo, es decir, si se quiere acceder a una zona intermedia del
fichero habrá que recorrer primero todo el contenido anterior.

ƒ Ficheros de acceso aleatorio. Se puede acceder a una zona concreta del


fichero sin necesidad de recorrerlo desde el principio.

1.2. EL PAQUETE JAVA.IO

El paquete java.io del Java estándar incluye todas las clases necesarias para acceder a
ficheros de disco desde una aplicación Java. Algunas de estas clases son sólo utilizadas con
ficheros, sino para realizar operaciones de entrada/salida de datos con diferentes tipos de
dispositivos externos. Este es el caso de BufferedReader que, aunque ha sido utilizada hasta el
momento para recuperar datos del teclado, puede ser empleada también para recuperar datos
de un fichero de texto.
Algunas de las clases más importantes incluidas en este paquete para el tratamiento de
ficheros son:

ƒ File. Representa una referencia a un fichero y proporciona métodos para


obtener información sobre el mismo.

ƒ FileReader. Permite realizar la lectura de caracteres de un fichero de texto.

ƒ FileWriter. Proporciona métodos para escribir caracteres en un fichero de


texto.

ƒ BufferedReader. Esta clase ya la hemos utilizado anteriormente para leer


cadenas de caracteres del teclado. En general, esta clase permite recuperar
cadenas de caracteres de cualquier flujo de entrada, incluido ficheros.

ƒ PrintWriter. Proporciona métodos para la escritura de datos de diferentes


tipos en un flujo de salida, incluido un fichero de texto.

ƒ ObjectInputStream. Permite recuperar objetos de un flujo de entrada, incluido


ficheros.

ƒ ObjectOutputStream. Permite enviar un objeto a un flujo de salida, incluido


ficheros.

2. MANIPULACIÓN DE FICHEROS DE TEXTO


Una de las operaciones más habituales que llevan a cabo las aplicaciones que tratan
con ficheros es la lectura y escritura de información en un fichero de texto. Estas operaciones
pueden ser realizadas utilizando las clases FileReader, BufferedReader, PrintWriter y
FileWriter.

2.1. LECTURA DE CADENAS DE UN FICHERO DE TEXTO

La lectura de cadenas de caracteres de un fichero de texto no difiere demasiado de la


lectura de datos de la consola, ya que la clase encargada de realizar la operación es la misma:
BufferedReader.

Como sabemos, el constructor de la clase BufferedReader tiene el formato:

BufferedReader (Reader r)

Donde el objeto Reader tendrá que ser un lector de entrada asociado al fichero des que
se quiere realizar la lectura. Este lector de entrada podrá ser un objeto FileReader, cuyo
constructor:

FileReader (String dirección)

nos permite crear un objeto de este tipo a partir de la localización del fichero.

Por ejemplo, para poder realizar la lectura de un fichero de texto localizado en la


dirección c:\ficheros\info.txt, la creación del objeto BuffredReader se realizaría como se indica
en las siguientes instrucciones:

FileReader reader = new FileReader (“c:\\ficheros\\info.txt”);


BufferedReader buffer = new BufferedReader (reader);
Obsérvese como la barra de separación de directorios debe indicarse mediante la
secuencia de escape “\\”.

Hay que tener en cuenta también que la creación del objeto FileReader puede provocar
una excepción de tipo FileNotFoundException que habrá que capturar. Esta excepción se
produce si el fichero especificado en la ruta no existe.

2.1.1. Lectura de los datos

Una vez que se dispone del objeto BufferedReader se puede invocar a su método
readLine() para recuperar su contenido línea a línea. Cada llamada a readLine() devuelve la
línea actual y sitúa el puntero de lectura en la siguiente línea del fichero. Después de leer la
última línea el puntero se situará al final del fichero; una vez en esa posición, la llamada al
método readLine() devolverá null.

El siguiente bloque de instrucciones de ejemplo mostraría en pantalla todas la líneas de


caracteres contenidas en el fichero de texto especificado:

FileReader reader = new FileReader ("c:\\syncrom\\info.txt");


BufferedReader buffer = new BufferedReader (reader);
String cadena;
//cuando se lea null finaliza la lectura
while((cadena=buffer.readLine())!=null){
System.out.println(cadena);
}

2.1.2. Cierre del stream

Una vez que se han terminado de realizar las operaciones de lectura sobre el fichero,
conviene cerrar el flujo de entrada para que se liberen los recursos utilizados. Esta operación
se lleva a cabo mediante el método close() de la clase FileReader:

reader.close();

Cualquier operación de lectura que se intente realizar después de haber cerrado el


fichero provocará una excepción IOException.

EJERCICIO 1

A continuación vamos a realizar un programa que, a partir de un fichero llamado


notas.txt que contiene un listado de notas de alumnos, realice la lectura de todas las notas y
nos muestre en pantalla la nota media total. Cada nota estará separada de la siguiente por una
barra vertical (“|”) y podrá tener cifras decimales, además, el conjunto total de notas puede
ocupar varias líneas dentro del archivo, por ejemplo:

4.5|3|3.75|

8|6.8

En este ejemplo se supone que el fichero se encuentra en el directorio del proyecto. El


código de la clase quedará como se indica a continuación:
import java.io.*;
public class LecturaNotas {
public static void main(String[] args) {
try{
//recuperación de las líneas de texto existentes en
// el fichero y fusión en una única cadena
FileReader reader = new FileReader ("notas.txt");
BufferedReader buffer = new BufferedReader (reader);
StringBuilder cadenacompleta=new StringBuilder();
String cadena;
while((cadena=buffer.readLine())!=null){
cadenacompleta.append(cadena);
}
reader.close();
//cálculo de la nota media
String []notas=
cadenacompleta.toString().split("[|]");
double media=0.0;
for(String s:notas){
media+=Double.parseDouble(s);
}
media=media/notas.length;
System.out.println("La nota media es: "+media);
}
catch(FileNotFoundException e){
System.out.println("El fichero indicado no existe");
}
catch(IOException e){
System.out.println("No se permiten más operaciones
sobre el fichero");
}
catch(NumberFormatException e){
System.out.println("El formato de alguno de los
datos es incorrecto");
}
}
}
2.2. ESCRITURA EN UN ARCHIVO DE TEXTO

2.2.1. La clase PrintWriter

La clase PrintWriter es la única que necesitamos para poder escribir datos en un


fichero de texto. Utilizando el constructor:

PrintWriter (String nombrefichero)

podemos crear un objeto PrintWriter asociado al fichero cuya dirección se suministra como
argumento. Si el fichero no existiera, se crearía al crear el objeto.

Una vez creado el objeto PrintWriter se puede recurrir a sus métodos de escritura para
enviar información al fichero. Entre estos métodos destacan:

ƒ print (tipo dato). El juego de métodos print nos permite enviar cualquier tipo
de dato primitivo Java y String al fichero. Este método se encuentra
sobrecargado, existiendo un método print por cada tipo primitivo (a excepción
de byte y short) además de String. Tras escribir el dato, el puntero de escritura
se situaría en la siguiente posición libre dentro del fichero.

ƒ println (tipo dato). Juego de métodos similar al anterior, introduciendo un


salto de línea al final de cada dato que se escribe.

ƒ printf (String formato, Object ... datos). Este método también lo estudiamos
en los primeros temas de Java. A través de él podemos escribir un texto
formateado en el fichero.

ƒ write (String cadena). Escribe una cadena de caracteres en el fichero. Su


funcionamiento es similar a print (String dato).

Al igual que sucede al realizar las operaciones de lectura del fichero, tras escribir los
datos en el fichero, el flujo de salida debería cerrarse para liberar los recursos utilizados. El
método close() de PrintWriter lleva a cabo esta tarea.

El siguiente bloque de instrucciones escribiría en un fichero los números pares entre 1


y 100 separando cada número del siguiente por un espacio:

PrintWriter pw = new PrintWriter("pares.txt");


for(int i=0;i<=100;i++){
if(i%2==0){
pw.write(i+" ");
}
}
pw.close();

2.2.2. La clase FileWriter

Un problema que se nos presenta al utilizar la clase PrintWriter para escribir en un


fichero es que cada vez que se crea el objeto, el puntero de escritura se sitúa al principio del
fichero, por lo que cualquier llamada a los métodos de escritura provocaría que los nuevos
datos sobrescribieran a los ya existentes.
Para solucionar este problema podemos hacer uso de la clase FileWriter. Esta clase,
desarrollada específicamente para escribir cadenas de caracteres en un fichero, permite decidir
a la hora de crear el objeto si queremos añadir la información al fichero o sobrescribir su
contenido:

FileWriter (String fichero, boolean append)

Si el parámetro append se establece al valor true, los métodos de escritura añadirán la


información al final del fichero, mientras que si es false los nuevos datos sobrescribirán a los
anteriores.

Una vez creado el objeto FileWriter en el modo deseado, las operaciones de escritura
se realizarán a través de su método write().

EJERCICIO 2

En este ejercicio vamos a realizar el módulo del programa de gestión de notas


encargado de la grabación de estas en el fichero.

A través de una interfaz gráfica (figura 1) se solicitarán al usuario de esta parte de la


aplicación las notas a grabar.

Figura. 1.

El código de la clase que implementa la interfaz gráfica se muestra en el siguiente


listado:

package graficos;
import javax.swing.*;
public class VentanaSwing extends JFrame{
JTextField jnota;
public VentanaSwing(String title, int x, int y, int w,
int h){
super(title);
this.getContentPane().setLayout(null);
this.setBounds(x, y, w, h);
JLabel jl = new JLabel ("Introduce la nota: ");
jl.setBounds(50, 100, 140, 40);
jnota = new JTextField ();
jnota.setBounds(200, 100, 100, 40);
JButton jb=new JButton("Añadir");
jb.setBounds(150, 200, 120, 40);
//se añaden los controles al panel de contenido
this.getContentPane().add(jl);
this.getContentPane().add(jnota);
this.getContentPane().add(jb);
//asocia el botón con su manejador
jb.addActionListener(new GestionFichero(this));
this.setVisible(true);
}
}

Como ya sabemos, cada nota tendrá que ser separada de la siguiente por el carácter
“|”, además, tras la escritura de cinco notas seguidas se introducirá un salto de línea. Para
introducir dicho salto de línea, haremos uso de la propiedad del sistema line.separator en vez
de indicar directamente “\n”, ya que esta cadena puede no representa el carácter de salto de
línea en todos los sistemas.

Con todo ello, el código de la clase manejadora del botón “Añadir” quedaría:

package graficos;
import java.awt.event.*;
import java.io.*;
public class GestionFichero implements ActionListener{
private VentanaSwing v;
private int contador;
public GestionFichero(VentanaSwing v){
this.v=v;
contador=0;
}
public void actionPerformed(ActionEvent e){
try{
FileWriter writer = new FileWriter("notas.txt",true);
writer.write(v.jnota.getText()+"|");
contador++;
//se incluye un salto de línea
//tras escribir cinco notas
if(contador==5){
writer.write(System.getProperty ("line.separator"));
contador=0;
}
writer.close();
}
catch(FileNotFoundException ex){
System.out.println("El fichero indicado no existe");
}
catch(IOException ex){
System.out.println("No se permiten más operaciones
sobre el fichero");
}
}
}

Para finalizar con la clase FileWriter, comentar que es posible utilizar el siguiente
constructor de la clase PrintWriter para crear un objeto de este tipo a partir de un objeto
FileWriter:

PrintWriter (Writer out)

De esta manera, se podrían utilizar los métodos de PrintWriter para escribir sobre un
fichero de texto permitiendo que los nuevos datos se añadieran al final del mismo.

3. ALMACENAMIENTO DE TIPOS PRIMITIVOS EN UN


FICHERO BINARIO
Utilizando los métodos print de clase PrintWriter podemos enviar a un fichero de texto
tanto objetos de tipo String como tipos primitivos Java. En cualquier caso, la información es
grabada como texto; esto significa que si, como en el caso del ejercicio de las notas, hemos
almacenado datos numéricos en el fichero y queremos recuperarlos para realizar algún tipo de
operación con los mismos, tendremos que recurrir a los métodos parse de las clases de
envoltorio antes de poder operar con los datos.

Si vamos a trabajar con tipos primitivos, por ejemplo números, puede ser resultar más
cómodo almacenar la información en un fichero binario en vez de en un fichero de texto. Al
utilizar ficheros binarios, la información puede recuperarse directamente en su tipo original,
evitando de este modo el empleo de métodos de conversión.

Para almacenar/recuperar datos primitivos Java de cualquier flujo binario el paquete


java.io dispone de las clases DataOutputStream y DataInputStream. Para trabajar con flujos
binarios asociados a un fichero, las clases anteriores necesitan apoyarse además en
FileOutputStream y FileInputStream.
3.1. ESCRITURA DE DATOS

La clase DataOutputStream proporciona una serie de métodos para realizar la escritura


de datos primitivos Java en un flujo binario. Para crear un objeto de esta clase recurriremos al
siguiente constructor:

DataOutputStream (OutputStream salida)

Donde el objeto OutputSteam que recibe como parámetro el constructor representa el


flujo de salida donde se enviarán los datos. La clase FileOuputStream representa un
OutputStream asociado a un fichero, por tanto, si queremos escribir los datos en un fichero
habrá que proporciona al constructor de DataOutputStream un objeto de dicha clase.

Mediante el siguiente constructor de FileOutputStream, además de la ruta del fichero


de escritura, podemos indicar a través del parámetro append si queremos añadir datos al
fichero (true) o sobrescribir su contenido (false):

FileOutputStream (String dirección, boolean append)

Si el fichero especificado no existiera se crearía y si en vez de ser una ruta de fichero


fuera la dirección de un directorio se produciría una excepción FileNotFoundException.

El siguiente bloque de instrucciones crearía un DataOutputStream para operaciones de


adición de datos en el fichero “contenido.txt”:

FileOutputStream f = new FileOutputStream (


“contenido.txt”, true);
DataOutputStream salida = new DataOutputStream (f);

3.1.1. Métodos de escritura

La clase DataOutputStream dispone de un juego de métodos writeXxx para la escritura


de los datos en el flujo de salida, donde Xxx representa el nombre del tipo primitivo Java. Por
ejemplo, para escribir un número de tipo int utilizaríamos el método writeInt (int dato), mientras
que para un tipo double emplearemos writeDouble (double dato).

El siguiente bloque de instrucciones almacenarían los números pares comprendidos


entre 1 y 100 en el stream “salida”:

FileOutputStream fs = new FileOutputStream (


"contenido.dat", true);
DataOutputStream salida = new DataOutputStream (fs);
for(int i=1;i<=100;i++){
if(i%2==0){
salida.writeInt(i);
}
}
salida.close();

Como en el caso de los ficheros de texto, el objeto de escritura debe cerrarse una vez
finalizadas las operaciones para liberar los recursos.
3.2. RECUPERACIÓN DE DATOS

La recuperación de datos de tipos primitivos Java almacenados en un fichero se lleva a


cabo a través de los métodos definidos en la clase DataInputStream. Primeramente, será
necesario crear un objeto de esta clase asociado al flujo de entrada (InputStream) del que se
quieren recuperar los datos que, en caso de tratarse de un fichero binario, será un objeto de la
clase FileInputStream:

FileInputStream fi = new FileInputStream("c:\\contenido.dat");


DataInputStream entrada = new DataInputStream(fi);

En caso de que el fichero no exista, la creación del objeto FileInputStream lanzará una
excepción FileNotFoundException.

3.2.1. Métodos de lectura

Los métodos readXxx() de la clase DataInputStream nos permitirán recuperar en su


tipo original los datos almacenados en el fichero a través de DataOutputStream. Por cada tipo
primitivo Java existe un método readXxx(), siendo Xxx el nombre del tipo. Por ejemplo, el
método readInt() lo utilizaremos para leer un dato almacenado como int, mientras que
readBoolean() nos permitirá recuperar datos de tipo boolean.

Cuando se hace la llamada a uno de los métodos de lectura, se recupera el dato que
se encuentra en la posición actual del puntero, pasando después a apuntar al siguiente dato.
Una vez leído el último dato, el puntero se situará en la posición EOF (final de fichero); a partir
de ahí, cualquier nueva llamada a los métodos de lectura provocará una excepción de tipo
EOFException, que es una subclase de IOException. Así pues, si quisiéramos recuperar todos
los datos contenidos en el fichero, deberíamos incluir las instrucciones de lectura y
manipulación de los datos en un bucle infinito, del cual se saldría en el momento en que se
produjera la excepción EOFException.

El siguiente código recuperaría todos los números almacenados en contenido.dat y los


mostraría en pantalla:

try{
FileInputStream fi = new FileInputStream(
"c:\\contenido.dat");
DataInputStream entrada=new DataInputStream(fi);
while(true){
System.out.println(entrada.readInt());
}
}
catch(FileNotFoundException e){
System.out.println("El fichero indicado no existe");
}
catch(EOFException e){
System.out.println("Se han leído todos los datos");
}
catch(IOException e){
System.out.println("No se permiten más operaciones
sobre el fichero");
}

Los métodos de lectura de DataInputStream pueden lanzar las excepciones


EOFException e IOException y, dado que la primera es una subclase de la segunda,
tendremos que definir el catch de EOFException antes que el de IOException.

EJERCICIO 3

En este ejercicio vamos a realizar una nueva versión de la aplicación de notas,


utilizando en este caso ficheros binarios para el almacenamiento de los datos en su tipo
primitivo.

Por otro lado, realizaremos un modificación el interfaz gráfica para que además de la
introducción de los datos, disponga de un botón que al ser pulsado nos muestre la nota media
en un JLabel de la propia interfaz (figura 2).

Figura. 2.

Puesto que los datos se almacenarán directamente en formato binario, no será


necesario en este caso introducir el carácter “|” como separador de datos.

A continuación se muestra el código actualizado de la clase que implementa la interfaz


gráfica (VentanaSwing):

package graficos;
import javax.swing.*;
public class VentanaSwing extends JFrame{
JTextField jnota;
JLabel jlmedia;
public VentanaSwing(String title, int x, int y, int w,
int h){
super(title);
this.getContentPane().setLayout(null);
this.setBounds(x, y, w, h);
JLabel jl = new JLabel ("Introduce la nota: ");
jl.setBounds(50, 100, 140, 40);
jnota = new JTextField ();
jnota.setBounds(200, 100, 100, 40);
JButton jb=new JButton("Añadir");
jb.setBounds(150, 200, 120, 40);
JButton jbmedia=new JButton("Mostrar media");
jbmedia.setBounds(100, 300, 150, 40);
jlmedia=new JLabel("");
jlmedia.setBounds(300, 300, 120, 30);
//se añaden los controles al panel de contenido
this.getContentPane().add(jl);
this.getContentPane().add(jnota);
this.getContentPane().add(jb);
this.getContentPane().add(jbmedia);
this.getContentPane().add(jlmedia);
//asocia el botón Añadir con su manejador
jb.addActionListener(new GestionFichero(this));
//asocia el botón Media con su manejador
jbmedia.addActionListener(new GestionMedia(this));
this.setVisible(true);
}
}
En cuanto a la clase GestionFichero, el nuevo código será el que se muestra en el
siguiente listado:

import java.awt.event.*;
import java.io.*;
public class GestionFichero implements ActionListener{
private VentanaSwing v;
public GestionFichero(VentanaSwing v){
this.v=v;
}
public void actionPerformed(ActionEvent e){
try{
FileOutputStream salida = new
FileOutputStream("notas.bin",true);
DataOutputStream fichero=new DataOutputStream(salida);
fichero.writeInt(Integer.parseInt(v.jnota.getText()));
salida.close();
}
catch(FileNotFoundException ex){
System.out.println("El fichero indicado no existe");
}
catch(IOException ex){
System.out.println("No se permiten más operaciones
sobre el fichero");
}
}
}
Finalmente, la clase que gestiona la pulsación del botón “Mostrar media” quedará como
se indica en el siguiente listado:

package graficos;
import java.awt.event.*;
import java.io.*;
public class GestionMedia implements ActionListener{
private VentanaSwing v;
public GestionMedia(VentanaSwing v){
this.v=v;
}
public void actionPerformed(ActionEvent e){
double media=0.0;
int contador=0;
FileInputStream entrada=null;
try{
entrada = new FileInputStream("notas.bin");
DataInputStream datos=new DataInputStream(entrada);
//recupera todos los datos del fichero mientras
//no llegue al final del mismo
while(true){
media+=datos.readInt();
contador++;
}
}
catch(FileNotFoundException ex){
System.out.println("El fichero especificado no
existe");
}
catch(EOFException ex){
media=media/contador;
v.jlmedia.setText(String.valueOf(media));
}
catch(IOException ex){
System.out.println("No se permiten más operaciones
sobre el fichero");
}
finally{
try{
entrada.close();
}
catch(IOException ex){}
}
}
}

4. ALMACENAMIENTO DE OBJETOS EN DISCO


Además de datos de tipos primitivos es posible almacenar objetos Java en un fichero
binario. Esto resulta especialmente interesante en el caso de que necesitemos almacenar en
una archivo la información contenida en un objeto de tipo JavaBean; en vez de almacenar por
separado los distintos campos que forman el objeto como tipos primitivos, es posible guardar el
objeto completo utilizando una única instrucción y recuperarlo después de la misma manera.

La forma de gestionar objetos en disco es muy similar a la seguida con los tipos
primitivos; en vez de usar las clases “Data”, se emplearán ObjectOutputStream y
ObjectInputStream, apoyándose en FileOutputStream y FileInputStream.

4.1. LA INTERFAZ SERIALIZABLE

Cuando se almacena un objeto en un archivo binario, la JVM realiza previamente un


proceso de serialización del objeto para poder guardarlo. La serialización consiste en convertir
el objeto en una serie de bits que pueda ser reconvertida de nuevo en el objeto original
(desserialización) durante el proceso de lectura (figura 3).
Objeto Objeto

serialización 1 1 0 0 1 0 0 0 1 0 1 0 1 ... desserialización


Figura. 3.

Para que un objeto pueda ser serializado, la clase a la que pertenece el objeto (o su
superclase) debe implementar la interfaz java.io.Serializable. Esta interfaz no dispone de
ningún método que tenga que implementar el programador; simplemente con indicar en la
definición de la clase que se implementa la interfaz:

public class NombreClase implements Serializable

será suficiente para que la JVM se encargue de realizar las operaciones oportunas para
serializar y desserializar el objeto.

Muchas de las clases del Java SE de uso general, como String, las clases de envoltorio
o ArrayList, implementan Serializable. En el caso de clases personalizadas será necesario
especificar la implementación de la interfaz si queremos que sus objetos puedan ser
almacenados en disco. El siguiente listado corresponde a un ejemplo de clase JavaBean que
implementa Serializable:

public class Empleado implements Serializable{


private int codigo;
private String nombre;
public int getCodigo(){
return codigo;
}
public String getNombre(){
return nombre;
}
public void setCodigo (int codigo){
this.codigo=codigo;
}
public void setNombre (String nombre){
this.nombre=nombre;
}
}
4.2. ESCRITURA DEL OBJETO

La clase ObjectOutputStream proporciona los métodos para la escritura de objetos en


un archivo binario. Para crear un objeto ObjectOutputStream es necesario indicar el
OutputStream de salida donde se realizarán las operaciones de escritura.

Este flujo de salida será, como en el caso de los datos primitivos Java, un objeto
FileOutputStream. Las siguientes instrucciones crearían un ObjectOutputStream para escribir
objetos en el fichero “contenido.dat” en modo append:

FileOutputStream fs = new FileOutputStream (


"contenido.dat", true);
ObjectOutputStream salida=new ObjectOutputStream(fs);

4.2.1. Métodos de escritura

Para escribir el objeto en el fichero recurriremos al método writeObject() de la clase


ObjectOutputStream, al que tendremos que pasarle como parámetro el objeto que queremos
escribir.

Además de este método, ObjectOutputStream dispone de un juego de métodos


writeXxx() para escribir en el fichero datos de tipos primitivos Java, donde Xxx representa el
nombre de cada uno de los tipos primitivos del lenguaje. Esto significa que podría utilizarse
también esta clase en vez de DataOutputStream para el almacenamiento en disco de datos
básicos.

El siguiente bloque de instrucciones de ejemplo almacenaría en el archivo


“contenido.dat” dos objetos de la clase Empleado presentada anteriormente como ejemplo:

Empleado e1=new Empleado(1,"ejemplo1");


Empleado e2=new Empleado(2,"ejemplo2");
salida.writeObject(e1);
salida.writeObject(e2);
salida.close(); //cierra el flujo de salida

4.3. LECTURA DE OBJETOS

Para realizar la lectura de objetos de disco almacenados con writeObject, utilizaremos


el método readObject() existente en la clase ObjectInputStream. La creación de un objeto de
esta clase requiere de la existencia de un objeto FileInputStream asociado al fichero del que se
realizará la lectura.

Cada llamada a readObject() devuelve el objeto que actualmente está siendo apuntado
por el puntero de lectura, desplazando después dicho puntero a la siguiente posición. En este
sentido, hay que tener en cuenta que después de leer el último objeto del fichero la
siguiente llamada al método readObject() provocará una excepción IOException.

El siguiente bloque de instrucciones mostraría los nombres de todos los empleados


almacenados en el fichero “contenido.dat”:
try{
FileInputStream fi = new FileInputStream("contenido.dat");
ObjectInputStream entrada=new ObjectInputStream(fi);
//realiza la lectura de objetos
//hasta que se produzca una IOException
while(true){
Empleado em=(Empleado)entrada.readObject();
System.out.println(em.getNombre());
}
}
catch(FileNotFoundException e){
System.out.println("El fichero indicado no existe");
}
catch(ClassNotFoundException e){
System.out.println("Clase no encontrada");
}
catch(IOException e){
System.out.println("Finalizada la lectura de datos");
}

La clase ObjectInputStream dispone además de un juego de métodos readXxx() para la


recuperación de tipos primitivos que hayan sido almacenados en el fichero mediante los
métodos writeXxx() de la clase ObjectOutputStream.

EJERCICIO 4

En este ejercicio realizaremos una aplicación basada en entorno gráfico que permita
grabar en disco objetos de tipo Empleado. También dispondrá de una opción que permita
mostrar el nombre de un empleado a partir de su código. El aspecto de la interfaz será el que
se indica en la figura 4.

Los empleados se mantendrán en el fichero mientras esté en ejecución la aplicación,


es decir, cada vez que se ejecute el fichero se poblará con nuevos empleados.
Figura. 4.

El código de la clase interfaz gráfica se muestra en el siguiente listado:

package graficos;
import javax.swing.*;
public class Interfaz extends JFrame{
JTextField jtcodigo,jtnombre,jtinput;
JLabel jlout;
public Interfaz(String title, int x, int y, int w, int h){
super(title);
this.getContentPane().setLayout(null);
this.setBounds(x, y, w, h);
JLabel jlcodigo = new JLabel ("Código Empleado: ");
jlcodigo.setBounds(50, 50, 140, 30);
jtcodigo = new JTextField ();
jtcodigo.setBounds(200, 50, 100, 30);
JLabel jlnombre = new JLabel ("Nombre Empleado: ");
jlnombre.setBounds(50, 100, 140, 30);
jtnombre = new JTextField ();
jtnombre.setBounds(200, 100, 100, 30);
JButton jb=new JButton("Añadir");
jb.setBounds(150, 150, 120, 30);
JLabel jlinput = new JLabel ("Introduce Codigo: ");
jlinput.setBounds(50, 250, 140, 30);
jtinput = new JTextField ();
jtinput.setBounds(200, 250, 100, 30);
JButton jbbuscar=new JButton("Buscar");
jbbuscar.setBounds(100, 300, 150, 40);
jlout=new JLabel("");
jlout.setBounds(300, 300, 120, 30);
//se añaden los controles al panel de contenido
this.getContentPane().add(jlcodigo);
this.getContentPane().add(jlnombre);
this.getContentPane().add(jlout);
this.getContentPane().add(jtcodigo);
this.getContentPane().add(jtnombre);
this.getContentPane().add(jtinput);
this.getContentPane().add(jlinput);
this.getContentPane().add(jb);
this.getContentPane().add(jbbuscar);

//asocia el botón Añadir con su manejador


jb.addActionListener(new GestionGuardar(this));
//asocia el botón Media con su manejador
jbbuscar.addActionListener(new GestionBusqueda(this));
this.setVisible(true);
}
}
La clase Empleado que encapsula los datos del empleado es la misma que la utilizada
en ejemplos anteriores:

package graficos;
import java.io.*;
public class Empleado implements Serializable{
private int codigo;
private String nombre;
public Empleado(){}
public Empleado(int codigo, String nombre){
this.codigo=codigo;
this.nombre=nombre;
}
public int getCodigo() {
return codigo;
}
public void setCodigo(int codigo) {
this.codigo = codigo;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
}
La clase GestionGuardar se encarga de gestionar el almacenamiento de los empleados
en el fichero. Como vemos en el siguiente listado, el fichero se prepara para la escritura en el
constructor de la clase, realizándose las llamadas a writeObject() con un nuevo objeto cada vez
que se pulsa el botón “Añadir”:

package graficos;
import java.awt.event.*;
import java.io.*;
public class GestionGuardar implements ActionListener{
private Interfaz v;
private FileOutputStream salida;
private ObjectOutputStream fichero;
public GestionGuardar(Interfaz v){
this.v=v;
try{
salida = new FileOutputStream("empleados.bin");
fichero=new ObjectOutputStream(salida);
}
catch(FileNotFoundException ex){
System.out.println("El fichero indicado no existe");
}
catch(IOException ex){
ex.printStackTrace();
}
}
public void actionPerformed(ActionEvent e){
try{
//crea el objeto Empleado con los datos de
//los campos de texto
Empleado em=new Empleado(Integer.parseInt(
v.jtcodigo.getText()),v.jtnombre.getText());
fichero.writeObject(em);
}
catch(IOException ex){
System.out.println("No se permiten más operaciones
sobre el fichero");
}
}
}
En cuanto a la clase que gestiona la búsqueda del Empleado, su código será como se
indica a continuación:

package graficos;

import java.awt.event.*;
import java.io.*;
public class GestionBusqueda implements ActionListener{
private Interfaz v;
boolean encontrado=false;
public GestionBusqueda(Interfaz v){
this.v=v;
}
public void actionPerformed(ActionEvent e){
encontrado=false;
FileInputStream entrada=null;
try{
entrada = new FileInputStream("empleados.bin");
ObjectInputStream datos=
new ObjectInputStream(entrada);
while(true){
Empleado emp=(Empleado)datos.readObject();
//si el código de empleado existe se
//muestra el nombre del empleado
if(emp.getCodigo()==
Integer.parseInt(v.jtinput.getText())){
v.jlout.setText(emp.getNombre());
encontrado=true;
}
}
}
catch(ClassNotFoundException ex){
ex.printStackTrace();
}
catch(FileNotFoundException ex){
System.out.println("El fichero especificado no
existe");
}
catch(IOException ex){
//si despues de mrecorrer todos los empleados
//no se encuentra el código se muestra el mensaje
if(!encontrado){
v.jlout.setText("No existe ningún empleado con
ese nombre");
}
ex.printStackTrace();
}
finally{
try{
entrada.close();
}
catch(IOException ex){}
}
}
}

5. ACCESO ALEATORIO A UN FICHERO. CLASE


RANDOMACCESSFILE
A través de la clase RandomAccessFile podemos acceder a un fichero y situar el
puntero de lectura/escritura en cualquier parte del interior del mismo. Una vez posicionado, se
puede recurrir a los distintos métodos proporcionados por la clase para la lectura/escritura tanto
de datos de tipos primitivos como cadenas de caracteres.

Para crear un objeto RandomAccessFile podemos utilizar el siguiente constructor:

RandomAccessFile (String ruta, String modo)

Donde el segundo parámetro “modo”, es una cadena de caracteres que nos indica el
modo de acceso al fichero. Entre sus posibles valores destacamos:

ƒ “r”. Acceso en modo lectura. Únicamente se podrán utilizar los métodos de


lectura de la clase; cualquier llamada a los métodos write lanzará una
excepción de tipo IOException. Si el fichero que se intenta leer no existiera, la
utilización de este modo de acceso provocaría una excepción de tipo
FileNotFoundException al intentar crear el objeto.

ƒ “rw”. Acceso en modo lectura y escritura. Cuando se crea el objeto con este
modo de acceso se podrán realizar tanto operaciones de lectura de datos
como de escritura, y si el fichero no existe se creará.

La siguiente instrucción de ejemplo crearía un RandomAccessFile para acceder al


fichero contenido.dat en modo de sólo lectura:

RandomAccessFile fich = new RandomAccessFile (


“contenido.dat”, “r”);
5.1. MÉTODOS DE LA CLASE RANDOMACCESSFILE

A continuación, vamos a ver los principales métodos que nos proporciona esta clase y
que nos permitirán realizar operaciones de lectura y escritura de datos en cualquier parte del
mismo. Destacamos los siguientes:

ƒ length(). Devuelve la longitud del fichero en bytes. Utilizando este método


podemos conocer el tamaño del mismo.

ƒ seek (int pos). Desplaza el puntero a la posición indicada como parámetro.


Dicha posición representa el número de bytes desde el principio del fichero.
De cara a desplazar el puntero para leer un determinado dato, es necesario
tener en cuenta los tipos de datos almacenados y el número de bytes que
estos ocupan. Por ejemplo, si hemos almacenado valores de tipo int (cada int
ocupa cuatro bytes) y quisiéramos recuperar el número que ocupa la tercera
posición, habría que desplazar el puntero a la posición 8 (2 * 4).

ƒ skipBytes (int total). Desplaza el puntero el número de bytes especificados a


partir de la posición actual.

ƒ getFilePointer (). Devuelve la posición actual del puntero, es decir, el número


de bytes entre el principio del fichero y la posición del puntero.

ƒ writeXxx (tipo dato). El juego de métodos writeXxx() escriben un dato del tipo
especificado en la posición actual del fichero, siendo Xxx y tipo el nombre del
tipo primitivo. Por ejemplo, writeInt (int dato) se utilizaría para escribir datos de
tipo int, mientras que writeBoolean (boolean dato) almacenaría un dato
boolean. Después de realizar la escritura del dato, el puntero se desplaza
tantos bytes como ocupe el dato escrito.

ƒ writeChars (String cadena). Almacena una cadena de caracteres en el


fichero. Cada carácter es almacenado con dos bytes que representa la
codificación Unicode del carácter.

ƒ readXxx (). Juego de métodos que permiten recuperar el dato existente en la


posición actual en el tipo especificado. Después de realizar la lectura, el
puntero se desplaza tantos bytes como ocupe el dato leído.

EJERCICIO 5

El siguiente ejercicio consistirá en un sencillo programa que nos aclare el


funcionamiento del acceso aleatorio a ficheros. Se trata de una aplicación encargada de
generar 10 números enteros aleatorios entre 0 y 100 y almacenarlos en un fichero binario.
Posteriormente, solicitará al usuario el orden del número que quiere recuperar y se lo mostrará
en pantalla.

El siguiente listado corresponde al código de la clase:

import java.io.*;
import java.util.*;
public class Test {
public static void main(String[] args) {
try{
RandomAccessFile rfile=
new RandomAccessFile("numeros.bin","rw");
for(int i=1;i<=10;i++){
int num=(int)(Math.random()*100);
System.out.println("Número "+i+" :"+num);
rfile.writeInt(num);
}
//cierra y abre de nuevo para sólo lectura
rfile.close();
rfile=new RandomAccessFile("numeros.bin","r");
//solicita la posición del número a recuperar
Scanner sc = new Scanner(System.in);
System.out.println("Introduzca la posición del
número que quiere recuperar:");
int num=Integer.parseInt(sc.nextLine());
//lee el número que ocupa la posición indicada,
//teniendo en cuenta que cada int ocupa 4 bytes
int pos=(num-1)*4;
//si existe la posición indicada
if(pos<(rfile.length()-4)){
rfile.seek(pos);
System.out.println(rfile.readInt());
}
else{
System.out.println("No se puede leer más allá de
la última posición");
}
rfile.close();
}
catch (FileNotFoundException e){
System.out.println("Fichero no existente");
}
catch(IOException e){
e.printStackTrace();
}
}
}
AUTOEVALUACIÓN

1. Indica cual de las siguientes instrucciones permitiría crear un objeto para realizar
operaciones de lectura de cadenas de caracteres sobre el fichero contenido.txt:

A. InputStream is = new InputStream (“contenido.txt”);

B. BufferedReader reader =
new BufferedReader (new FileReader( “contenido.txt”));
C. BufferedReader reader = new BufferedReader (“contenido.txt”);

D. FileReader reader = new FileReader (new InputStream (“contenido.txt”));

2. Si durante la creación de un objeto PrintWriter para escribir en un fichero,


proporcionamos la ruta de un fichero no existente en el disco:

A. Se producirá una excepción IOException.

B. Se producirá una excepción EOFException

C. Se producirá una excepción FileNotFoundException

D. Se creará el fichero

3. Para escribir cadenas de texto en un fichero, de modo que los nuevos datos se
añadan al final del mismo manteniendo el contenido anterior, la creación del objeto
FileWriter debería realizarse:

A. FileWriter writer = new FileWriter (“fichero.txt”);

B. FileWriter writer = new FileWriter (“fichero.txt”, true);

C. FileWriter writer = new FileWriter (“fichero.txt”, “append”);

D. FileWriter writer = new FileWriter (“fichero.txt”, 0);

4. El siguiente bloque de instrucciones tiene como misión recuperar todos los


números enteros almacenados en un fichero y mostrar su suma. Indica la
instrucción que falta en la línea del comentario:

int suma=0;

try{

FileInputStream fi = new FileInputStream("numeros.bin");

DataInputStream entrada=new DataInputStream(fi);

//aquí falta instrucción

suma+=entrada.readInt();
}

catch(FileNotFoundException e){

System.out.println("El fichero indicado no existe");

catch(EOFException e){

System.out.println("La suma final es "+suma);

catch(IOException e){

System.out.println("No se permiten más operaciones sobre el fichero");

A. while(true){

B. for (int i=1; i<=100;i++){

C. while(entrada.readLine()!=null){

D. while(entrada.readInt()!=0){

5. Para que un objeto pueda ser almacenado en disco

A. La clase a la que pertenece tiene que implementar la interfaz Serializable

B. La clase a la que pertenece deberá tener sobrescrito el método toString()

C. La clase a la que pertenece debe ser un JavaBean

D. Solamente deberá disponer de métodos de tipo set/get

6. Si después de recuperar todos los números enteros almacenados en un archivo


binario mediante el método readInt() de la clase DataInputStream, hacemos una
nueva llamada a dicho método:

A. El método devolverá el valor -1

B. El método devolverá el valor 0

C. Se producirá una excepción FileNotFoundException

D. Se producirá una excepción EOFException


7. Si después de recuperar todos los objetos almacenados en un archivo binario
mediante el método readObject() de la clase ObjectInputStream, hacemos una
nueva llamada a dicho método:

A. El método devolverá el valor null

B. El método devolverá el valor 0

C. Se producirá una excepción IOException

D. Se producirá una excepción EOFException

8. ¿Cuál de los siguientes métodos no podríamos utilizar para almacenar


directamente el contenido de una variable double en un archivo de disco?:

A. Método writeDouble() de la clase DataOutputStream

B. Método write() de la clase FileWriter

C. Método print() de la clase PrintWriter

D. Método writeDouble() de la clase ObjectOutputStream


Ejercicios Propuestos
1. Realizar una programa basado en consola que solicite al usuario la introducción
de nombres y los almacene en un fichero de texto, cada nombre se separará de
los adyacentes mediante un espacio (“ “). Tras solicitar cada nombre, el programa
preguntará al usuario si quiere guardar un nuevo nombre, así hasta que responda
“no”. Una vez finalizada la introducción de los nombres, se realizará la lectura de
los mismos y su visualización en pantalla.

2. Tenemos un fichero de texto de tipo agenda donde se almacenan las direcciones


de correo electrónico de una serie de Empresas. El formato del fichero es el
siguiente:

“Tecnologías asociadas:tecnologias@grupo.com|Hidráulicas Manur:hidromanur@auna.es| ”

ƒ Como se puede apreciar, cada pareja empresa:email está separada de la


siguiente por una barra vertical (“|”).

ƒ Se pide desarrollar una aplicación basada en entorno gráfico, consistente en


una ventana donde se solicite al usuario el nombre de la Empresa a través de
un TextField. Al pulsar un botón “Aceptar”, se mostrará en alguna parte de la
ventana el email su email, pero si dicha Empresa no estuviera registrada se
mostrará un mensaje de tipo “Empresa no registrada”.

3. Crear una aplicación basada en interfaz gráfica que permita a un usuario


almacenar datos numéricos enteros suministrados a través de un TextField en un
fichero binario. Paralelamente, se ejecutará un proceso en segundo plano que
cada 5 segundos actualice la información que se muestra en una parte de la
interfaz gráfica. Dicha información corresponde a los valores mínimo, máximo y
medio de los almacenados hasta ese momento en el fichero.

Você também pode gostar