Sunteți pe pagina 1din 32

Tutoriales

I/O: Leer y Escibir


Autor: Sun
Traductor: Juan Antonio Palos (Ozito)

• I/O: Leer y Escribir


o Introducción a los Streams I/O
o Serialización de Objetos
o Trabajar con Ficheros de Acceso Aleatorio
o Y el Resto...
• Introducción a los Streams de I/O
o Streams de Caracteres
o Streams de Bytes
o Entender las Superclases de I/O
• Usar Strings de Profundidad
• Como usar Streams de Ficheros
• Como usar Piped Streams
o Usar Streams para Envolver otros Streams
• Usar Streams de Proceso
• Cómo Concatenar Ficheros
• Trabajar con Streams Filtrados
o Usar Streams Filtrados
o Cómo usar DataInputStream y DataOutputStream
o Escribir Nuestros Propios Streams Filtrados
• Cómo usar DataInputStream y DataOutputStream
• Escribir Streams Filtrados
o La clase CheckedOutputStream
o La Clase CheckedInputStream
o El Interface Checksum y la clase Adler32
o Un Programa de Prueba
o Filtrar Ficheros de Acceso Aleatorio
• Serialización de Objetos
• Serializar Objetos
o ¿Cómo Escribir en un ObjectOutputStream?
o ¿Cómo Leer desde un ObjectInputStream?
• Proporcionar Serialización de Objetos para Nuestras Clases
o Implementar el Interface Serializable
o Personalizar la Serialización
o Implementar el Interface Externalizable
o Proteger la Información Sensible
• Trabajar con Ficheros de Acceso Aleatorio
o Usar Ficheros de Acceso Aleatorio
o Escribir Filtros para Ficheros de Acceso Aleatorio
• Usar Ficheros de Acceso Aleatorio
• Escribir Filtros para Ficheros de Acceso Aleatorio
o CheckedDataOutput contra CheckedOutputStream
o CheckedDataInput contra CheckedInputStream
o Los Programas Principales
• Y el Resto...

1
I/O: Leer y Escribir

Frecuentemente los programas necesitan traer información desde una fuente externa o
enviar información a una fuente externa. La información pueder estár en cualquier parte,
en un fichero, en disco, en algún lugar de la red, en memoria o en otro programa.
También puede ser de cualquier tipo: objetos, caracteres, imágenes o sonidos.

Para traer la información, un programa abre un stream sobre una fuente de información
(un fichero, memoria, un socket) y lee la información serialmente, de esta forma:

Similarmente, un programa puede enviar información a un destino externo abriendo un


stream sobre un destino y escribiendo la información serialmente, de esta forma:

No importa de donde venga o donde vaya la información y tampoco importa el tipo de los
datos que están siendo leídos o escritos, los algoritmos para leer y escribir son casi
siempre los mismos.

Leer Escribir
abrir un stream abrir un stream
mientras haya información mientras haya información
leer información escribir información
cerrar el stream cerrar el stream

El paquete java.io contiene una colección de clases stream que soportan estos algoritmos
para leer y escribir. Estas clases están divididas en dos árboles basándose en los tipos de
datos (caracteres o bytes) sobre los que opera.

2
Sin embargo, algunas veces es más conveniente agrupar las clases basándose en su
propósito en vez en los tipos de datos que lee o escribe. Así, podemos agrupar los
streams dependiendo de si leen u escriben lados en las "profundidades" o procesan la
información que está siendo leída o escrita.

Introducción a los Streams I/O

Esta sección describe todos los tipos de streams y muestra las clases del paquete java.io
que los implementan de acuerdo a la división del árbol de clases.

Luego, como mucha gente piensa en términos de lo que quieren hacer en vez de lo que
están haciendo, proporcionamos dos secciones que nos muestran cómo usar los streams
seleccionados basándonos en su propósito.

• Introdución a los Streams de I/O


o Streams de Caracteres
o Streams de Bytes
o Entender las Superclases de I/O

Introdución a los Streams de I/O

Streams de Caracteres

Reader y Writer son las superclases abstractas para streams de caracteres en java.io.

3
Reader proporciona el API y una implementación para readers-- streams que leen
caracteres de 16-bits-- y Writer proporciona el API y una implementación para writers--
streams que escriben caracteres de 16-bits.

Las subclases de Reader y Writer que implementan streams especializados se dividen en


dos categorías: aquellos que leen o escriben datos en profundidad (sinks) [mostrados en
gris en la siguiente figura] y aquellos que realizan alguna suerte de proceso [mostrados en
blanco].

La figura muestra el árbol de clases para Reader y Writer.

La mayoría de los programas deberían usar este tipo de streams, ya que ambos pueden
manejar caracteres en el conjunto de caracteres Unicode (mientras que los streams de
bytes están limitados a bytes ISO-Latin-1 de 8-bit).

Streams de Bytes

Los programas deberían usar los streams de bytes, descendientes de InputStream y


OutputStream, para leer y escribir bytes de 8-bits. InputStream y OutputStream
proporcionan el API y alguna implementación streams de entrada y salida y leen y
escriben bytes de 8-bits. Estos streams se usan normalmente para leer y escribir datos
binarios como imágenes y sonidos.

Al igual que Reader y Writer, las subclases de InputStream y OutputStream proporcionan


I/O especializada que se divide en dos categorias: streams de profundidad y streams de
procesamiento. La siguiente figura muestra el árbol de clases para los streams de bytes.

4
Como se ha mencionado, dos de las clases de streams de bytes, ObjectInputStream y
ObjectOutputStream, se usan para la serialización de objetos.

Esta clases se cubren completamente en la página Serialización de Objetos.

Entender las Superclases de I/O

Reader y InputStream definen APIs similares pero para diferentes tipos de datos. Por
ejemplo, Reader contiene estos métodos para leer caracteres y arrays de caracteres:

int read()
int read(char cbuf[])
int read(char cbuf[], int offset, int length)

InputStream defien los mismos métodos pero para leer bytes o arrays de bytes:

int read()
int read(byte cbuf[])
int read(byte cbuf[], int offset, int length)

Ambos también proporcionan métodos para marcar localizaciones en el stream, saltar


entradas y restablecer la posición actual.

Writer y OutputStream son similarmente paralelas. Writer define tres métodos para
escribir caracteres y arrays de caracteres.

int write(int c)
int write(char cbuf[])
int write(char cbuf[], int offset, int length)

Y OutputStream define los mismos métodos pero para Bytes.

5
int write(int c)
int write(byte cbuf[])
int write(byte cbuf[], int offset, int length)

Todos estos streams --readers, writers, inputstreams, y outputstreams-- se abren


automáticamente cuando se crean. Podemos cerrar explícitamente cualquier stream
llamando a su método close.

O el recolector de basura puede cerrarlos implícitmamente, lo que ocurre cuando se deja


de referenciar el objeto.

• Usar Strings de Profundidad

Usar Strings de Profundidad

Los streams de profundidad "sink" leen o escriben datos desde sitios especializados como
strings, ficheros o tuberías (pipes). Típicamente, cada reader o inputstream está pensado
para un tipo específico de fuente de entrada, java.io contiene un writer o un outputstream
paralelo que pueden crearlo. La siguiente tabla nos muestra los streams de datos sink de
java.io:

Tipo de Sink Streams de Caracteres Streams de Bytes


Memory CharArrayReader, ByteArrayInputStream,
CharArrayWriter, ByteArrayOutputStream,
StringReader, StringBufferInputStream
StringWriter
Pipe PipedReader, PipedInputStream,
PipedWriter PipedOutputStream
File FileReader, FileInputStream,
FileWriter FileOutputStream

Observa que tanto el grupo de streams de caracteres como el bytes contienen parejas
paralelas que operan con el tipo de sinks de datos.

Estos se describen luego.

CharArrayReader y CharArrayWriter
ByteArrayInputStream y ByteArrayOutputStream
Estos streams se usan para leer y escribir desde memoria. Podemos crear estos
streams sobre un array existente y luego usara los métodos de lectura y escritura
para leer y escribir desde el array.
FileReader y FileWriter
FileInputStream y FileOutputStream
Colectivamente llamados streams de ficheros, estos streams se usan para leer y
escribir ficheros del sistema de ficheros nativo.

Como usar Streams de Ficheros tiene un ejemplo que usa FileReader y FileWriter
para copiar el contenido de un fichror a otro.

PipedReader y PipedWriter
PipedInputStream y PipedOutputStream
Implementan los componentes de entrada y salida de una tubería.

6
Las tuberías (Pipes) se usan para canalizar la salida de un programa (o thread)
hacia la entrada de otro programa (o thread).

Puedes ver PipedReader y PipedWriter en acción en la página Como usar Pipe


Streams.

StringReader y StringWriter
StringBufferInputStream
Se usa StringReader para leer caracteres desde un String que reside en memoria.
Se usa StringWriter para escribir en un String.

StringWriter recoge los caracteres escritos en un StringBuffer, que puede ser


convertido en un String. StringBufferInputStream es similar a StringReader,
excepto en que lee bytes desde un StringBuffer.

• Como usar Streams de Ficheros

Como usar Streams de Ficheros

Los streams de ficheros son quizás los más fáciles de entender. Simplemente ponemos,
el stream de ficheros --FileReader, FileWriter, FileInputStream, y FileOutputStream-- cada
uno de lectura o escritura sobre un fichero del sistema de ficheros nativo.

Podemos crear un stream de fichero desde un nombre de fichero en el formato de un


string, desde un objeto File, o desde un objeto FileDescriptor.

El siguiente programa Copy usa FileReader y FileWriter para copiar el contenido de un


fichero llamado farrago.txt en otro fichero llamado outagain.txt.

import java.io.*;

public class Copy {


public static void main(String[] args) throws IOException {
File inputFile = new File("farrago.txt");
File outputFile = new File("outagain.txt");

FileReader in = new FileReader(inputFile);


FileWriter out = new FileWriter(outputFile);
int c;

while ((c = in.read()) != -1)


out.write(c);

in.close();
out.close();
}
}

Este programa es muy sencillo.

7
Abre FileReader sobre farrago.txt y abre FileWriter sobre outagain.txt.

El programa lee caracteres desde el reader mientras haya más entradas en el fichero de
entrada.

Cuando la entrada se acada, el programa cierra tanto el reader como el writer.

Observa el código que usa el programa Copy para crear un FileReader.

File inputFile = new File("farrago.txt");


FileReader in = new FileReader(inputFile);

Este código crea un objeto File que representa el fichero nombrado en el sistema de
ficheros nativo. File es una clase de utilidad proporcionada por java.io. Este programa usa
este objeto sólo para construir un FileReader sobre farrago.txt.

Sin embargo, se podría usar inputFile para obtener información sobre farrago.txt, como su
path completo.

Después de haber ejecutado el programa, deberíamos encontrar una copia exacta de


farrago.txt en un fichero llamado outagain.txt en el mismo directorio. Aquí está el
contenido del fichero:

So she went into the garden to cut a cabbage-leaf, to


make an apple-pie; and at the same time a great
she-bear, coming up the street, pops its head into the
shop. 'What! no soap?' So he died, and she very
imprudently married the barber; and there were
present the Picninnies, and the Joblillies, and the
Garyalies, and the grand Panjandrum himself, with the
little round button at top, and they all fell to playing
the game of catch as catch can, till the gun powder ran
out at the heels of their boots.

Samuel Foote 1720-1777

Recuerda que FileReader y FileWriter leen y escriben caracteres de 16 bits. Sin embargo,
la mayoría del sistemas de ficheros nativos están basados en bytes de 8 bits. Estos
streams codifican los caracteres según operan de acuerdo al esquema de codificación de
caracteres por defecto. Podemos encontrar la codificación de caracteres por defecto
usando System.getProperty("file.encoding"). Para especificar otra codificación,
deberíamos construir un OutputStreamWriter sobre un FileOutputStream y especificarla.
Para más información sobre la codificación de caracteres puedes ver la sección
Internationalization.

Para curiosos, aquí tenemos otra versión de este programa, CopyBytes, que usa
FileInputStream y FileOutputStream en lugar de FileReader y FileWriter.

• Como usar Piped Streams


o Usar Streams para Envolver otros Streams

Como usar Piped Streams

8
PipedReader y PipedWriter (y sus correspondientes streams de entrada y salida
PipedInputStream y PipedOutputStream) implementan los componentes de entrada y
salida de una tubería.

Las tuberías se utilizan para canalizar la salida de un programa (o thread) a la entrada de


otro programa (o thread)

¿Por qué son útiles?

Consideremos una clase que implementa varias utilidades de manipulación de strings


como ordenación o inversión de texto. Sería bonito que la salida de uno de estos métodos
puediera ser usada como entrada del otro para que pudieramos encadenar una serie de
llamadas a métodos que realizan un función de gran importancia. Por ejemplo, podríamos
invertir todas las palabras de una lista, ordenarlas, y volver a invertirlas para crear una
lista de palabrás rítmicas.

Sin los streams de tuberías, el programa debería almacenar los resultados en algún lugar
(como en un fichero o en la memoria) entre cada paso, como se ve aquí:

Con los streams de tuberías, la salida de un método puede ser dirigida hacia la entrada
del siguiente, como se muestra en esta figura:

Luego investigaremos un programa que implementa lo que representa el diagrama de la


figura anterior.

Este programa usa PipedReader y PipedWriter para conectar la entrada y la salida de sus
métodos reverse y sort para crear una lista de palabras rítmicas. Este programa se
compone de varias clases. Esta sección muestra y explica sólo los elementos del
programa que leen y escriben en las tuberías. Sigue los siguientes enlaces al código para
ver el programa completo.

Primero, echemos un vistazo a la secuencia de llamada de los métodos reverse y sort


desde el método main en la clase RhymingWords.

FileReader words = new FileReader("words.txt");


Reader rhymingWords = reverse(sort(reverse(words)));

9
La llamada más interna a reverse toma un FileReader abierto sobre el fichero words.txt
que contiene una lista de palabras. El valor devuelto por reverse se pasa a sort, cuyo
valor de retorno es pasado a otra llamada a reverse.

Echemos un vistazo al método reverse; el método sort es similiar y lo entenderemos una


vez que comprendamos reverse.

public static Reader reverse(Reader source) {


BufferedReader in = new BufferedReader(source);

PipedWriter pipeOut = new PipedWriter();


PipedReader pipeIn = new PipedReader(pipeOut);
PrintWriter out = new PrintWriter(pipeOut);

new ReverseThread(out, in).start();

return pipeIn;
}

Las sentencias en negrita de reverse crean los dos puntos finales de una tubería --un
PipedWriter y un PipedReader-- y los conecta construyendo el PipedReader "sobre" el
PipedWriter.

Cualquier cosa escrita en el PipedWriter puede ser leída desde el PipedReader.

Las formas de conexión de tuberías se ilustran aquí:.

reverse arranca un ReverseThread que escribe su salida en el PipedWriter y devuelve el


PipedReader al llamador.

Entonces el llamador preprara un thread de ordenación para leerla.

El método sort es exactamente lo mismo, excepto en que crea y arranca un SortThread.

Usar Streams para Envolver otros Streams

El método reverse contiene algún código interesante; en particular estas dos sentencias:

BufferedReader in = new BufferedReader(source);


...
PrintWriter out = new PrintWriter(pipeOut);

La primera línea abre un BufferedReader sobre source, el argumento a invertir (un


Reader).

Esto esencialmente "envuelve" source en un BufferedReader.

10
El programa lee desde el BufferedReader, que a su vez lee desde source.

El programa hace esto para poder usar el método de conveniencia readLine de


BufferedReader.

De forma similar, el PipedWriter es envuelto en un PrintWriter para que el programa


pueda usar el método de conveniencia println de PrintWriter.

Frecuentemente veremos estreams envueltos de esta forma para así combinar las
distintas características de varios streams.

Intenta esto:

Escribe otra versión de este programa que use inputstreams y outputstreams en vez de
readers y writers.

Aquí puedes ver las soluciones:

• RhymingWords
• ReverseThread

• SortThread

• Usar Streams de Proceso

Usar Streams de Proceso

Los streams de proceso realizan alguna suerte de operación, como almacenamiento o


codificación de caracteres, mientras leen o escriben. Al igual que de los stream de
profundidad, java.io contiene parejas de streams. Uno que realiza una operación particular
durante la lectura y otro que realiza la misma operación (o la inversa) durante la escritura.
Esta tabla nos muestra los streams de proceso de java.io:

Proceso Stream de Caracteres Streams de Bytes


Almacenamiento BufferedReader, BufferedInputStream,
BufferedWriter BufferedOutputStream
Filtrado FilterReader, FilterInputStream,
FilterWriter FilterOutputStream
Conversión entre InputStreamReader,
Bytes y Caracteres OutputStreamWriter
Concatenación SequenceInputStream
Serialización de Objetos ObjectInputStream,
ObjectOutputStream
Conversión de Datos DataInputStream,
DataOutputStream
Contaje LineNumberReader LineNumberInputStream
Exploración PushbackReader PushbackInputStream
Impresión PrintWriter PrintStream

Observa que muchas veces, java.io contiene streams de caracteres y de bytes que
realizan el mismo proceso pero para diferentes tipos de datos.

11
BufferedReader y BufferedWriter BufferedInputStream y BufferedOutputStream
Almacenan los datos en buffers mientras leen o escriben, por lo tanto reduciendo
así el número de accesos requeridos a la fuente de datos original. Los streams con
buffer normalmente son más eficientes que los que no lo utilizan.
FilterReader y FilterWriter FilterInputStream y FilterOutputStream
Clases abstractas, como sus padres. Definen el interface para filtros de streams,
que filtran los datos que están siendo leídos o escritos.

Trabajar con Streams Filtrados más adelante en esta lección, nos mostrará como
usar filtros de streams y como implementar el nuestro propio.

InputStreamReader y OutputStreamWriter
Una pareja de reader y writer que realiza un puente entre streams de bytes y
streams de caracteres. Un InputStreamReader lee bytes desde un InputStream y
los convierte a caracteres usando la decodificación de caracteres por defecto o
una decodificación de caracteres especificada por su nombre.

De forma similar, un OutputStreamWriter convierte caracteres a bytes usando una


decodificación de caracteres por defecto o una decodificación de caracteres
especificada por su nombre y luego escribe estos bytes en un OutputStream.

Podemos aprender el nombre de la codificación de caracteres por defecto


llamando a System.getProperty("file.encoding").

SequenceInputStream
Concatena varios streams de entrada en un sólo stream de entrada.

Cómo Contanerar Ficheros tiene un pequeño ejemplo de esta clase.

ObjectInputStream y ObjectOutputStream
Se usa para serializar objetos. Puedes ver Serialización de Objetos.
DataInputStream y DataOutputStream
Lee o escribir tipos de datos primitivos de Java de una forma independiente de la
máquina.

Cómo usar DataInputStream y DataOutputStream nos enseña un ejemplo de uso


de estos dos streams.

LineNumberReader y LineNumberInputStream
Sigue la pista del número de línea mientras lee.
PushbackReader y PushbackInputStream
Dos streams cada uno con un caracter (o byte) de menos en el buffer.

Algunas veces, cuando se leen datos desde un stream, encontraremos útil


explorar el siguiente ítem del stream para decidir que hacer luego. Sin embargo, si
lo exploramos, necesitaremos ponerlo de nuevo en su sitio para que pueda ser
leído y procesado normalmente.

PrintWriter y PrintStream
Contienen métodos de impresión de conveniencia. Estos son streams sencillos
para escribir, por eso frecuentemente veremos otros streams envueltos en uno de
estos.

12
• Cómo Concatenar Ficheros

Cómo Concatenar Ficheros

SequenceInputStream crea un sólo stream de entrada desde varias fuentes de entrada.


Este programa de ejemplo Concatenate, usa SequenceInputStream para implementar la
utilidad de concatenación que secuencialmente concatena ficheros en el orden que son
listados en la línea de comandos:

Esta es la clase controladora de la utilidad Concatenate:

import java.io.*;

public class Concatenate {


public static void main(String[] args) throws IOException {
ListOfFiles mylist = new ListOfFiles(args);

SequenceInputStream s = new SequenceInputStream(mylist);


int c;

while ((c = s.read()) != -1)


System.out.write(c);

s.close();
}
}

Lo primero que hace esta clase es crear un objeto ListOfFiles llamado mylist que es
inicializado con los argumentos de la línea de comandos introducidos por el usuario. Los
argumentos de la línea de comandos listan los ficheros a concatenar. Se usa mylist para
inicializar SequenceInputStream que usa mylist para obtener un nuevo InputStream para
cada ficheros de lista:

import java.util.*;
import java.io.*;

public class ListOfFiles implements Enumeration {

private String[] listOfFiles;


private int current = 0;

public ListOfFiles(String[] listOfFiles) {


this.listOfFiles = listOfFiles;
}

public boolean hasMoreElements() {


if (current < listOfFiles.length)
return true;
else
return false;
}

13
public Object nextElement() {
InputStream in = null;

if (!hasMoreElements())
throw new NoSuchElementException("No more files.");
else {
String nextElement = listOfFiles[current];
current++;
try {
in = new FileInputStream(nextElement);
} catch (FileNotFoundException e) {
System.err.println("ListOfFiles: Can't open " + nextElement);
}
}
return in;
}
}

ListOfFiles implementa el interface Enumeration. Veremos como esto entra en juego


cuando pasemos por el resto del programa.

Después el método main crea el SequenceInputStream, lee un byte cada vez. Cuando el
SequenceInputStream necesita un InputStream de una nueva fuente (como para el primer
byte leído o cuando alcanza el final del inputstream actual), llama a nextElement sobre el
objeto Enumeration para obetener el siguiente InputStream. ListOfFiles crea objetos
FileInputStream enforma de lazo, lo que significa que siempre que SequenceInputStream
llama a nextElement, ListOfFiles abre un FileInputStream sobre el siguiente nombre de
ficheros de la lista y devuelve el stream. Cuando el ListOfFiles llega al final de los ficheros
a leer (no tiene más elementos), nextElement devuelve null, y la llamada al método read
de SequenceInputStream devuelve -1 para indicar el final de la entrada.

Concatenate simplemente hace eco de todos los datos leidos desde el


SequenceInputStream a la salida estándar.

Prueba Esto:

Intenta ejecutar Concatenate sobre los ficheros farrago.txt y words.txt que han sido usados
como entradas para otros ejemplos de esta lección.

• Trabajar con Streams Filtrados


o Usar Streams Filtrados
o Cómo usar DataInputStream y DataOutputStream
o Escribir Nuestros Propios Streams Filtrados

Trabajar con Streams Filtrados

14
Añadimos un stream filtrado a otro stream para filtrar los datos que están siendo leídos o
escritos desde el stream original.

El paquete java.io contiene estos streams filtrados que son subclases de


FilterInputStream o FilterOutputStream.

• DataInputStream y DataOutputStream
• BufferedInputStream y BufferedOutputStream
• LineNumberInputStream
• PushbackInputStream
• PrintStream (este es un estream de salida)

Esta sección muestra cómo usar streams filtrados a través de un ejemplo que usa un
DataInputStream y un DataOutputStream. Además, esta sección muestra como escribir
nuestros propios streams filtrados.

Usar Streams Filtrados

Para usar un stream de entrada o salida filtrado, adjuntamos el stream filtrado a otro
stream de entrada o salida.

Por ejemplo, podemos adjuntar un DataInputStream al stream de entrada estándar con el


siguiente código:

DataInputStream dis = new DataInputStream(System.in);


String input;

while ((input = dis.readLine()) != null) {


. . . // do something interesting here
}

Podríamos hacer esto para poder usar los métodos readXXX más convenientes, como un
readLine, implementado por DataInputStream.

Cómo usar DataInputStream y DataOutputStream

Esta página cubre y explica un ejemplo de uso de DataInputStream y DataOutputStream,


dos streams filtrados que pueden leer y escribir tipos de datos primitivos de Java.

Escribir Nuestros Propios Streams Filtrados

Muchos programadores podrían encontrar que necesitan implementar sus propios


streams que filtren o procesen datos que están siendo leídos o escritos desde un stream.
Algunas veces el proceso es independiente del formato de los datos, como el contaje de
varios ítems de un stream, y algunas veces el proceso está relacionado directamente con
los propios datos o su formato, como la lectura y escritura de datos que están contenidos
en filas y columnas. Frecuentemente, estos programadores, subclasifican
FilterOutputStream y FilterInputStream para conseguir sus objetivos. Esta sección
describe un ejemplo de cómo subclasificar FileInputStream y FilterOutputStream para
crear nuestos propios filtros.

• Cómo usar DataInputStream y DataOutputStream

15
Cómo usar DataInputStream y DataOutputStream

Este página muestra cómo usar las clases DataInputStreamy DataOutputStream de


java.io. Crea un ejemplo: DataIOTest, que lee y escribe datos tabulares (una factura de
merchandising Java).

Los datos tabulares están formateados en columnas, donde cada columna está separada
de la siguiente por un tab. Las columnas contienen los precios de venta, el número de
unidades pedidas, y una descripción del ítem, de esta forma:

19.99 12 Java T-shirt


9.99 8 Java Mug

DataOutputStream, al igual que otros streams de salida filtrados, debe adjuntarse a algún
otro OutputStream. En este caso, se adjunta a un FileOutputStream que está configurado
para escribir en un fichero llamado invoice1.txt.

DataOutputStream dos = new DataOutputStream(


new FileOutputStream("invoice1.txt"));

Luego, DataIOTest usa métodos especializados writeXXXde DataOutputStream para


escribir los datos de la factura (contenida dentro de arrays en el programa) de acuerdo a
los tipos de datos que se están escribiendo.

for (int i = 0; i < prices.length; i ++) {


dos.writeDouble(prices[i]);
dos.writeChar('\t');
dos.writeInt(units[i]);
dos.writeChar('\t');
dos.writeChars(descs[i]);
dos.writeChar('\n');
}
dos.close();

Observa que este código cierra el stream de salida cuando ha finalizado.

Luego, DataIOTest abre un DataInputStream sobre el fichero que acaba de escribir.

DataInputStream dis = new DataInputStream(


new FileInputStream("invoice1.txt"));

DataInputStream también debe adjuntar algún otro InputStream; en este caso, un


FileInputStream configurado para leer el fichero que acaba de escribir --invoice1.txt.

DataIOTest luego sólo lee los datos usando los métodos especializados readXXX de
DataInputStream:

try {
while (true) {
price = dis.readDouble();
dis.readChar(); // throws out the tab
unit = dis.readInt();

16
dis.readChar(); // throws out the tab
desc = dis.readLine();
System.out.println("You've ordered " + unit
+ " units of " + desc
+ " at $" + price);
total = total + unit * price;
}
} catch (EOFException e) {
}
System.out.println("For a TOTAL of: $" + total);
dis.close();

Cuando se han leído todos los datos, DataIOTest muestra una sentencia sumarizando el
pedido y la cantidad debida, y cierra el stream.

Observa el bucle que usa DataIOTest para leer los datos desde el DataInputStream.

Normalmente, cuando se lee usaremos un bucle como este:

while ((input = dis.readLine()) != null) {


...
}

El método readLine devuelve un valor, null, que indica que se ha alcanzado el fin del
fichero. Muchos de los métodos readXXX de DataInputStream no pueden hacer esto
porque cualquier valor devuelto para indicar fin-de-fichero podría ser un valor leído
legítimamente desde el stream. Por ejemplo, supongamos que queremos usar -1 para
indicar el fin-de-fichero. Bien, no podemos usarlo porque -1 es un valor legítimo que
puede ser leído desde el stream de entrada usando readDouble, readInt, o uno de los
otros métodos de lectura que leen números.

Por eso el método readXXXde DataInputStream lanza una EOFException en su lugar.

Cuando ocurre EOFException termina el while (true).

Cuando ejecutemos el programa DataIOTest deberíamos ver la siguiente salida:

You've ordered 12 units of Java T-shirt at $19.99


You've ordered 8 units of Java Mug at $9.99
You've ordered 13 units of Duke Juggling Dolls at $15.99
You've ordered 29 units of Java Pin at $3.99
You've ordered 50 units of Java Key Chain at $4.99
For a TOTAL of: $892.88

• Escribir Streams Filtrados


o La clase CheckedOutputStream
o La Clase CheckedInputStream
o El Interface Checksum y la clase Adler32
o Un Programa de Prueba
o Filtrar Ficheros de Acceso Aleatorio

17
Escribir Streams Filtrados

Lo siguiente es una lista de pasos a realizar cuando escribamos nuestro propios streams
filtrados tanto de entrada como de salida.

• Crear una subclase de FilterInputStream y FilterOutputStream. Normalmente los


streams de entrada y salida vienen en parejas, por eso necesitamos crear las dos
versión del stream filtrado.
• Sobreescribir los métodos read y write.
• Sobreescribir cualquier otro método que pudieramos necesitar.
• Aseguranos de que los streams de entrada y salida funcionan juntos.

Esta sección nos muestra cómo implementar nuestros propios streams filtrados a través
de un ejemplo que implementa una pareja de streams filtrados de entrada y salida.

Tanto el stream de entrada como el de salida usan una clase checksum para calcular el
checksum de los datos escritos o leídos desde el stream.

El checksum se usa para determinar si los datos leídos por el stream de entrada
corresponden con los datos escritos por el stream de salida.

Cuatro clases y un interface componen este programa de ejemplo:

• Las subclases de los streams de entrada y salida filtrados--CheckedOutputStream


y CheckedInputStream.
• El interface Checksum y la clase Adler32 calculan un checksum para los streams.
• La clase CheckedIOTest define el método main para el programa.

La clase CheckedOutputStream

La clase CheckedOutputStream es una subclase de FilterOutputStream que calcula un


checksum sobre los datos que están siendo escritos en el stream. Cuando se crea un
CheckedOutputStream, debemos usar su único constructor.

public CheckedOutputStream(OutputStream out, Checksum cksum) {


super(out);
this.cksum = cksum;
}

Este constructor toma un argumento OutputStream y otro Checksum.

El argumento OutputStream es el stream de salida que este CheckedOutputStream debe


filtrar. El argumento Checksum es un objeto que calcula un checksum.
CheckedOutputStream se autoinicializa llamando al constructor de su superclase e
inicializando una variable privada , cksum, con el objeto Checksum. El
CheckedOutputStream usa cksum para actualizar el checksum cada vez que se escribe
un dato en el stream.

18
CheckedOutputStream necesita sobreescribir los métodos write de FilterOutputStream
para que cada vez que se llame al método write, se actualice el checksum.
FilterOutputStream define tres versiones del método write.

1. write(int i)
2. write(byte[] b)
3. write(byte[] b, int offset, int length)

CheckedOutputStream sobreescribe estos tres métodos.

public void write(int b) throws IOException {


out.write(b);
cksum.update(b);
}

public void write(byte[] b) throws IOException {


out.write(b, 0, b.length);
cksum.update(b, 0, b.length);
}

public void write(byte[] b, int off, int len) throws IOException {


out.write(b, off, len);
cksum.update(b, off, len);
}

Las implementaciones de estos tres métodos write son correctas: escriben los datos en el
stream de salida al que este stream está adjuntado, luego actualiza el checksum.

La Clase CheckedInputStream

La clase CheckedInputStream.

CheckedInputStream es una subclase de FilterInputStream que calcula un checksum de


los datos que están siendo leídos desde el stream. Cuando se crea un
CheckedInputStream, debemos usar su único constructor:

public CheckedInputStream(InputStream in, Checksum cksum) {


super(in);
this.cksum = cksum;
}

Este constructor es similar al de la clase CheckedOutputStream.

Sólo que CheckedOutputStream necesita sobreescribir los métodos write de


FilterOutputStream, CheckedInputStream debe sobreescribir los métodos read de
FilterInputStream para que cada que vez que se llame al método read, se actualice el
checksum. Como con FilterOutputStream, FilterInputStream define tres versiones del
método read y CheckedInputStream las sobreescribe todas.

public int read() throws IOException {


int b = in.read();
if (b != -1) {

19
cksum.update(b);
}
return b;
}

public int read(byte[] b) throws IOException {


int len;
len = in.read(b, 0, b.length);
if (len != -1) {
cksum.update(b, 0, len);
}
return len;
}

public int read(byte[] b, int off, int len) throws IOException {


len = in.read(b, off, len);
if (len != -1) {
cksum.update(b, off, len);
}
return len;
}

Las implementaciones de estos tres métodos read son correctas: leen los datos desde el
stream de entrada al que está adjunto este stream filtrado; entonces si se leyó realmente
algún dato, se actualiza el checksum.

El Interface Checksum y la clase Adler32

El interface Checksum define cuatro métodos para que lo implementen los objetos
checksum; estos métodos resetean, actualizan y devuelven el valor del checksum.
Podríamos escribir una clase Checksum que calcule un tipo específico de checksum
como el CRC-32.

Observa que la herencia en el checksum es la noción de estado. El objeto checksum no


sólo calcula un checksum y ya está. En vez de eso, el checksum es actualizado cada vez
que se lee o se escribe información en el stream para el que este objeto calcula el
checksum. Si queremos reutilizar un objeto checksum, debemos resetearlo.

Para este ejemplo, hemos implementado el checksum Adler32, que es casi una versión
del checksum CRC-32 pero puede ser calculado más rápidamente.

Un Programa de Prueba

La última clase del ejemplo, CheckedIOTest, contiene el método main para el programa.

import java.io.*;

public class CheckedIOTest {


public static void main(String[] args) throws IOException {

Adler32 inChecker = new Adler32();


Adler32 outChecker = new Adler32();
CheckedInputStream in = null;

20
CheckedOutputStream out = null;

try {
in = new CheckedInputStream(
new FileInputStream("farrago.txt"),
inChecker);
out = new CheckedOutputStream(
new FileOutputStream("outagain.txt"),
outChecker);
} catch (FileNotFoundException e) {
System.err.println("CheckedIOTest: " + e);
System.exit(-1);
} catch (IOException e) {
System.err.println("CheckedIOTest: " + e);
System.exit(-1);
}

int c;

while ((c = in.read()) != -1)


out.write(c);

System.out.println("Input stream check sum: " +


inChecker.getValue());
System.out.println("Output stream check sum: " +
outChecker.getValue());

in.close();
out.close();
}
}

El método main crea dos objetos Adler32, uno para un CheckedOutputStream y otro para
un CheckedInputStream. El ejemplo requiere dos objetos checksum porque los objetos
checksum se actualizan durante las llamadas a lo métodos read y write que ocurren
concurrentemente.

Luego, main abre un CheckedInputStream sobre un pequeño fichero de texto, farrago.txt,


y un CheckedOutputStream sobre un fichero de salida llamado outagain.txt, que no existe
hasta que ejecutemos el programa por primera vez.

El método main lee el texto desde el CheckedInputStream y simplemente lo copia en el


CheckedOutputStream. Los métodos read y write usan los objetos Adler32 para calcular
un checksum durante la lectura y escritura. Después de haber leído completamente el
fichero de entrada (y consecuentemente se haya completado de escribir el fichero de
salida), el programa imprime los checksums de los dos streams (que deben ser iguales) y
cierra los dos streams.

Cuando ejecutemos CheckedIOTest, deberíamos ver esta salida:

Input stream check sum: 736868089


Output stream check sum: 736868089

21
Filtrar Ficheros de Acceso Aleatorio

Todos los streams filtrados de java.io descienden de InputStream o OutputStream, que


implementan ficheros de acceso secuencial. Por eso si subclasificamos FilterInputStream
o FilterOutputStream nuestros streams filtrados también serán ficheros de acceso
secuencial.

Escribir Filtros para Ficheros de Acceso Aleatorio, más adelante en esta lección nos
muestra cómo re-escribir este ejemplo para que funcione sobre un RandomAccessFile
también como sobre un DataInputStream o un DataOutputStream.

• Serialización de Objetos

Serialización de Objetos

El paquete java.io tiene otros dos streams de bytes-- ObjectInputStream y


ObjectOutputStream-- que funcionan como los otros streams de entrada y salida.

Sin embargo, son especiales porque pueden leer y escribir objetos.

La clave para escribir objetos es representar su estado de una forma serializada suficiente
para reconstruir el objeto cuando es leído.

Por eso, leer y escribir objetos es un proceso llamado serialización de objetos.

La serialización de objetos es esencial para construir todo excepto las aplicaciones más
temporales. Podemos usar la serialización de objetos de las siguientes formas:

• Invocación Remota de Métodos (RMI)--comunicación de objetos mediante sockets


• Persistencia de Peso Ligero-- el archivo de un objeto para una invocación posterior
en el mismo programa.

Como programador Java, necesitamos conocer la serialización de objetos desde dos


puntos de vista. Primero, necesitamos saber cómo serializar objetos escribiendolos a un
ObjectOutputStream y luego leerlos de nuevo usando un ObjectInputStream.

La siguiente página, Serializar Objetos, nos muestra cómo hacerlo.

Segundo, querremos conocer como escribir una clase para que sus ejemplares puedan
ser serializados. Podemos ver como se hace esto en la página: Proporcionar Serialización
de Objetos para Nuestras Clases.

• Serializar Objetos
o ¿Cómo Escribir en un ObjectOutputStream?
o ¿Cómo Leer desde un ObjectInputStream?

Serializar Objetos

Reconstruir un objeto desde un stream requier primero que el objeto se haya escrito en un
stream. Por eso empezaremos por aquí:

22
¿Cómo Escribir en un ObjectOutputStream?

Escribir objetos a un stream es un proceso sencillo. Por ejemplo, aquí obtenemos la hora
actual en milisegundos construyendo un objeto Date y luego serializamos ese objeto.

FileOutputStream out = new FileOutputStream("theTime");


ObjectOutputStream s = new ObjectOutputStream(out);
s.writeObject("Today");
s.writeObject(new Date());
s.flush();

ObjectOutputStream es un stream de proceso, por eso debe construirse sobre otro


stream. Este código construye un ObjectOutputStream sobre un FileOutputStream, para
serializar el objeto a un fichero llamado theTime.

Luego, el string Today y un objeto Date se escriben en el stream con el método


writeObject de ObjectOutputStream.

Si un objeto se refiere a otro objeto, entonces todos los objetos que son alcanzables
desde el primero deben ser escritos al mismo tiempo para poder mantener la relación
entre ellos. Así, el método writeObject serializa el objeto especificado, sigue sus
referencias a otros objetos recursivamente, y también los escribe todos.

El stream ObjectOutputStream implementa el interface DataOutput que define muchos


métodos para escribir tipos de datos primitivos, como writeInt, writeFloat, o writeUTF.
Podemos usar estos métodos para escribir tipos de datos primitivos a un
ObjectOutputStream.

El método writeObject lanza una NotSerializableException si el objeto dado no es


serializable. Un objeto es serializable sólo si clase implementa el interface Serializable.

¿Cómo Leer desde un ObjectInputStream?

Una vez que hemos escrito objetos y tipos de datos primitivos en un stream, querremos
leerlos de nuevo y reconstruir los objetos. Esto también es sencillo. Aquí está el código
que lee el String y el objeto Date que se escribieron en el fichero llamado theTime del
último ejemplo.

FileInputStream in = new FileInputStream("theTime");


ObjectInputStream s = new ObjectInputStream(in);
String today = (String)s.readObject();
Date date = (Date)s.readObject();

Cómo ObjectOutputStream, ObjectInputStream debe construirse sobre otro stream. En


este ejemplo, los objetos fueros archivados en un fichero, por eso el código construye un
ObjectInputStream sobre un FileInputStream. Luego, el código usa el método readObject
de ObjectInputStream para leer el String y el objeto Date desde el fichero. Los objetos
deben ser leídos desde el stream en el mismo orden en que se esribieron. Observa que el
valor de retorno de readObject es un objeto que es forzado y asignado a un tipo
específico.

23
El método readObject des-serializa el siguiente objeto en el stream y revisa sus
referencias a otros objetos recursivamente para des-serializar todos los objetos que son
alcanzables desde él. De esta forma, mantiene la relación entre los objetos.

El stream ObjectInputStream implementa el interface DataInput que define métodos para


leer tipos de datos primitivos. Los métodos de DataInput son paralelos a los definidos en
DataOutput para escribir tipos de datos primitivos. Entre ellos se incluyen readInt,
readFloat, y readUTF. Se usan estos métodos para leer tipos de datos primitivos desde un
ObjectInputStream.

• Proporcionar Serialización de Objetos para Nuestras Clases


o Implementar el Interface Serializable
o Personalizar la Serialización
o Implementar el Interface Externalizable
o Proteger la Información Sensible

Proporcionar Serialización de Objetos para Nuestras Clases

Un objeto es serializable sólo si su clase implementa el interface Serializable. Así, si


queremos serializar un ejemplar de una de nuestras clases, la clase debe implementar
este interface.

Las buenas noticias es que Serializable es un interface vacío. Es decir, no contiene


ninguna declaración de método; su propósito es simplemente identificar las clases cuyos
objetos son serializables.

Implementar el Interface Serializable

Aquí tenemos la definición completa del interface Serializable:

package java.io;
public interface Serializable {
// there's nothing in here!
};

Crear ejemplares de una clase serializable es fácil. Sólo hay que añadir la claúsula
implements Serializable a la declaración de nuestra clase:

public class MySerializableClass implements Serializable {


...
}

No tenemos que escribir ningún método. La serialización de un ejemplar de esta clase la


maneja el método defaultWriteObject de ObjectOutputStream.

Este método escribe cualquier cosa necesaria para reconstruir un ejemplar de la clase,
incluyendo lo siguiente:

• La clase del Objeto


• La firma de la clase
• Los valores para todos los miembros no-transient y no-static, incluyendo los
miembros que se refieren a otros objetos.

24
Para muchas clases, este comportamiento por defecto es suficiente. Sin embargo, la
serialización por defecto puede ser lenta, y las clases podrían querer un control más
explicito sobre la serialización.

Personalizar la Serialización

Podemos personalizar la serialización de nuestras clases proporcionando dos métodos


para ella: writeObject y readObject.

El método writeObject controla la información que se graba. Normalmente se usa para


añadir información adicional al stream. El método readObject lee la información escrita por
el correspondiente método writeObject o puede usarse para actualizar el estado del objeto
después de haber sido restaurado.

El método writeObject debe declarse exactamente como se muestra en el siguiente


ejemplo. Lo primero que debe hacer es llamar al método defaultWriteObject para realizar
la serialización por defecto. Cualquier ajuste puede realizarse después.

private void writeObject(ObjectOutputStream s)


throws IOException {
s.defaultWriteObject();
// customized serialization code
}

El método readObject debe leer todo lo escrito por writeObject en el mismo orden en que
se escribió. El método readObject también puede realizar cálculos o actualizar el estado
del objeto de alguna forma. Aquí está el método readObject que corresponde al método
writeObject anterior:

private void readObject(ObjectInputStream s)


throws IOException {
s.defaultReadObject();
// customized deserialization code
...
// followed by code to update the object, if necessary
}

El método readObject debe declarse exactamente como se ha mostrado.

Los métodos writeObject y readObject son responsalbes de serializar sólo las clases
inmediatas. Cualquier serialización requerida por la superclase se maneja
automáticamente. Sin embargo, una clase que necesita coordinarse explícitamente con su
superclase para serializarse puede hacerlo implementando el interface Externalizable.

Implementar el Interface Externalizable

Para un completo control explícito del proceso de serialización, una clase debe
implementar el interface Externalizable. Para los objetos Externalizable sólo la identidad
de la clase del objeto es grabada automáticamente en el stream. La clase es responsable
de escribir y leer sus contenidos, y debe estar coordinada con su superclase para hacerlo.

25
Aquí tenemos una definición completa del interface Externalizable que desciende del
interface Serializable:

package java.io;
public interface Externalizable extends Serializable
{
public void writeExternal(ObjectOutput out)
throws IOException;
public void readExternal(ObjectInput in)
throws IOException,
java.lang.ClassNotFoundException;
}

Lo siguiente sirve para una clase Externalizable:

• Debe implementar el interface java.io.Externalizable.


• Debe implementar un método writeExternal para salvar el estado del objeto.
También, debe coordinarse explícitamente con sus superclase para salvar su
estado.
• Debe implementar el método readExternal para leer los datos escritos por el
método writeExternal desde el stream y restaurar el estado del objeto. Debe
coordinarse explíctamente con su superclase para restaurar sus estado.
• Si se están escribiendo formatos definidos externamente, los métodos
writeExternal y readExternal son los únicos responsables de esos formatos.

Los métodos writeExternal y readExternal son públicos y corren el riesgo de que un


cliente pueda escribir o leer información en el objeto distinto usando sus métodos y
campos. Estos métodos deben se usarse solamente cuando la información contenida en
el objeto no sea importante o cuando exponer dicha información no represente un riesgo
de seguridad.

Proteger la Información Sensible

Cuando desarrollamos una clase que proporcione acceso controlado a recursos, debemos
tener cidado de proteger la información y las funciones sensibles. Durante la des-
serialización, se restaura el estado privado del objeto. Por ejemplo, un descriptor de
fichero contiene un manejador que propociona acceso a un recurso del sistema operativo.
Siendo posible olvidar que un descriptor de fichero puede permitir ciertas formas de
accesos ilegales, ya que la restauración del estado se hace desde un stream. Por lo tanto
en el momento de la serialización se debe tener cuidado y no creer que el stream contiene
sólo representaciones válidas de objetos. Para evitar comprometer una clase, debemos
evitar que el estado sensible de un objeto sea restaurado desde un stream o que sea
reverificado por la clase.

Hay disponibles varias técnicas para proteger los datos sensibles. La más sencilla es
marcar los campos que contienen los datos sensibles como private transient.

Los campos transient y static no son serializados. Marcando el campo evitaremos que el
estado aparezca en el stream y sea restaurado durante la des-serialización. Como la
lectura y escritura (de campos privados) no puede hacerde desde fuera de la clase, los
campos transient de la clase son seguros.

26
Las clases particularmente sensibles no debe ser serializadas. Para conseguir esto, el
objeto no debe implementar ninguno de los interfaces Serializable ni Externalizable.

Algunas clases podrían encontrar beneficioso permitir la escritura y lectura pero


específicamente manejadas y revalidar el estado cuando es des-serializado. La clase
debería implementar los métodos writeObject y readObject para salvar y recuperar sólo el
estado apropiado. Si el acceso debe ser denegado, lanzar una NotSerializableException
evitará accesos posteriores.

• Trabajar con Ficheros de Acceso Aleatorio


o Usar Ficheros de Acceso Aleatorio
o Escribir Filtros para Ficheros de Acceso Aleatorio

Trabajar con Ficheros de Acceso Aleatorio

Hasta ahora los streams de entrada y salida de esta lección han sido streams de acceso
secuencial, streams cuyo contenido debe ser leído o escrito secuencialmente. A pesar de
su increible utilidad, los ficheros de acceso secuencial son una consecuencia de un medio
secuencial como una cinta magnética. Los ficheros de acceso aleatorio, por otro lado,
permiten acceso no secuencial, o aleatorio, a los contenidos de un fichero.

Pero por qué necesitamos ficheros de acceso aleatorio. Consideremos el formato de


archivo conocido como "zip". Los archivos Zip contienen ficheros que normalmente están
comprimidos para ahorrar espacio. Los archivos Zip también contienen al final un
directorio de entradas que indica donde empiezan los distintos ficheros contenidos enel
archivo Zip.

Supongamos que queremos extraer un fichero específico de un archivo Zip. Si usamos un


stream de acceso secuencial, tenemos que hacer lo siguiente:

• Abrir el fichero Zip.


• Buscar a través del fichero Zip hasta localizar el fichero que queremos extraer.
• Extraer el fichero.
• Cerrar el archivo Zip.

Como media, usando este algoritmo, tendríamos que leer la mitad del archivo Zip antes
de encontrar el fichero que queremos extraer. Podemos extraer el mismo fichero del
archivo Zip de forma más eficiente usando la característica "seek" de un fichero de acceso
aleatorio:

• Abrimos el fichero Zip.


• Saltamos al directorio de entradas y localizamos la entrada del fichero que
queremos extraer del archivo Zip.
• Saltamos (hacia atrás) dentro del archivo Zip a la posición del fichero a extraer.
• Extraemos el fichero.

27
• Cerramos el archivo Zip.

Este algoritmo es más eficiente porque sólo tenemos que leer el directorio de entradas y
el fichero que queremos extraer.

La clase RandomAccessFile del paquete java.io implementa ficheros de acceso aleatorio.

• Usar Ficheros de Acceso Aleatorio

Usar Ficheros de Acceso Aleatorio

La clase RandomAccessFile implementa los interfaces DataInput y DataOutput y por lo


tanto puede usarse para leer y escribir. RandomAccessFile es similar a FileInputStream y
FileOutputStream en que especificamos un fichero del sistema de ficheros nativo para
abrirlo cuando lo creamos. Podemos hacer esto con un nombre de fichero o un objeto
File. Cuando creamos un RandomAccessFile, debemos indicar si sólo queremos leer o
también queremos escribir en el fichero. (tenemos que poder leer un fichero para poder
escribirlo). La siguiente línea de código cea un RandomAccessFile que lee el fichero
llamado farrago.txt:

new RandomAccessFile("farrago.txt", "r");

Y esta abre el mismo fichero tanto para lectura como para escritura:

new RandomAccessFile("farrago.txt", "rw");

Después de haber abierto el fichero, podemos usar los métodos comunes readXXX o
writeXXX para realizar I/O en el fichero.

RandomAccessFile soporta la noción de puntero de fichero. Este puntero indica la


posición actual en el fichero, cuando el fichero se crea por primera ver, el puntero de
fichero es 0, indicando el principio del fichero. Las llamadas a los métodos readXXX y
writeXXX ajustan la posición del puntero de fichero según el número de bytes leídos o
escritos.

Además de los métodos de I/O normales que implícitamente mueven el puntero de fichero
cuando ocurre la operación, RandomAccessFile contiene tres métodos que manipulan
explícitamente el puntero de fichero:

skipBytes
Mueve el puntero hacia adelante el número de bytes especificado.
seek
Posiciona el puntero de fichero en la posición anterior al byte especificado.
getFilePointer
Devuelve la posición actual (byte) del puntero de fichero.

28
• Escribir Filtros para Ficheros de Acceso Aleatorio
o CheckedDataOutput contra CheckedOutputStream
o CheckedDataInput contra CheckedInputStream
o Los Programas Principales

Escribir Filtros para Ficheros de Acceso Aleatorio

Reescribamos el ejemplo de Escribir Nuestros Propios Streams Filtrados para que


funcione sobre RandomAccessFiles. Como RandomAccessFile implementa los interfaces
DataInput y DataOutput, un beneficio lateral es que los streams filtrados también
funcionan con otros streams DataInput y DataOutput incluidos algunos streams de acceso
secuencial como DataInputStream y DataOutputStream.

El ejemplo CheckedIOTest de Escribir Nuestros Propios Streams Filtrados implementa


dos streams filtrados, CheckedInputStream y CheckedOutputStream, que calculan un
checksum de los datos que son leídos o escritos en el stream.

El nuevo ejemplo, CheckedDataOutput es una re-escritura de CheckedOutputStream--


calcula un checksum para los datos escritos en el stream-- pero opera sobre objetos
DataOutput en lugar de sobre objetos OutputStream. De forma similar CheckedDataInput
modifica CheckedInputStream para que ahora funciona sobre objetos DataInput en lugar
de objetos InputStream.

CheckedDataOutput contra CheckedOutputStream

Echemos un vistazo a las diferencias entre CheckedDataOutput y CheckedOutputStream.

La primera diferencia es que CheckedDataOutput no desciende de FilterOutputStream.


En su lugar, implementa el interface DataOutput.

public class CheckedDataOutput implements DataOutput


Nota:

Para mantener el ejemplo sencillo, la clase CheckedDataOutput realmente proporcionada


en esta sección no está declarada para implementar DataOutput, porque el interface
DataOutput especifica demasiados métodos. Sin embargo, la clase CheckedDataOutput
proporcionada en el ejemplo implementa varios métodos de DataOutput para ilustrar como
deberían funcionar.

Luego, CheckedDataOutput declara una variable privada para contener un objeto


DataOutput.

private DataOutput out;

Este es el objeto en el se escribirán los datos.

El constructor para CheckedDataOutput es diferente del constructor de


CheckedOutputStream: CheckedDataOutput se crea sobre un objeto DataOutput en vez
sobre un objeto OutputStream.

public CheckedDataOutput(DataOutput out, Checksum cksum) {


this.cksum = cksum;

29
this.out = out;
}

Este constructor no llama a super(out) como hacía el constructor de


CheckedOutputStream. Esto es porque CheckedDataOutput desciende de Object en vez
de la clase stream.

Aquí están las únicas modificaciones echas a CheckedOutputStream para crear un filtro
que funcione sobre objetos DataOutput.

CheckedDataInput contra CheckedInputStream

CheckedDataInput requiere los mismos cambios que CheckedDataOuput.

• CheckedDataInput no desciende de FilterInputStream pero implementa el interface


DataInput.

Nota:

Para mantener el ejemplo sencillo, la clase CheckedDataInput realmente


proporcionada en esta sección no está declarada para implementar DataInput,
porque el interface DataInput especifica demasiados métodos. Sin embargo, la
clase CheckedDataInput proporcionada en el ejemplo implementa varios métodos
de DataInput para ilustrar como deberían funcionar.

• CheckedDataInput declara una variable privada para contener un objeto DataInput


que lo envuelve.
• El constructor de CheckedDataInput requiere un objeto DataInput en vez de un
objeto InputStream.

Además de estos cambios, el método read también se ha modificado. El


CheckedInputStream del ejemplo original implementa dos métodos read, uno para leer un
sólo byte y otro para leer un array de bytes. El interface DataInput teine métodos que
implementan la misma funcionalidad, pero tienen diferentes nombres y firmas de métodos.
Así el método read de la clase CheckedDataInput tiene nuevos nombres y firmas de
métodos.

public byte readByte() throws IOException {


byte b = in.readByte();
cksum.update(b);
return b;
}

public void readFully(byte[] b) throws IOException {


in.readFully(b, 0, b.length);
cksum.update(b, 0, b.length);
}

public void readFully(byte[] b, int off, int len)


throws IOException
{
in.readFully(b, off, len);
cksum.update(b, off, len);

30
}

Los Programas Principales

Finalmente, este ejemplo tiene dos programas principales para probar los nuevos filtros.
CheckedDITest, que ejecuta el filtro sobre ficheros de acceso secuencial (objetos
DataInputStream y DataOutputStream), y CheckedRAFTest, que ejecuta los filtros sobre
ficheros de acceso aleatorio (objetos RandomAccessFiles).

Estos dos programas se diferencian sólo en el tipo del objeto que abren para el filtro.
CheckedDITest crea un DataInputStream y un DataOutputStream y usa el filtro checksum
sobre ellos, como en el siguiente código:

cis = new CheckedDataInput(


new DataInputStream(new FileInputStream("farrago.txt")),
inChecker);
cos = new CheckedDataOutput(
new DataOutputStream(new FileOutputStream("outagain.txt")),
outChecker);

CheckedRAFTest crea dos RandomAccessFiles, uno para leer y uno para escribir, y usa
el filtro checksum sobre ellos.

cis = new CheckedDataInput(


new RandomAccessFile("farrago.txt", "r"),
inChecker);
cos = new CheckedDataOutput(
new RandomAccessFile("outagain.txt", "rw"),
outChecker);

Cuando ejecutemos cualquiera de estos programas deberíamos ver la siguiente salida:

Input stream check sum: 736868089


Output stream check sum: 736868089

• Y el Resto...

Y el Resto...

Además de las clases e interfaces explicadas en esta lección, java.io contiene las
siguientes clases e interfaces

File
Representa un fichero del sistema de ficheros nativo.

31
Podemos crear un objeto File para un fichero del sistema de ficheros nativo y
luego consultar en el objeto información sobre ese fichero (como su path
completo).

FileDescriptor
Representa un manejador de fichero (o descriptor) para abrir un fichero o un
socket.

Normalmente no usaremos esta clase.

StreamTokenizer
Parte el contenido de un stream en tokens.

Los Tokens son la unidad más pequeña reconocida por un algoritmo de análisis de
texto (como palabras, símbolos, etc). Se puede usar un StreamTokenizer para
analizar un fichero de texto. Por ejemplo, podríamos usarlo para dividir un fichero
fuente Java en nombres de variables, operadores, etc, o dividir un fichero HTML
en etiquetas HTML.

FilenameFilter
Usado por el método list de la clase File para determinar qué ficheros se deben
mostrar de un directorio. El FilenameFilter accepta o rechaza ficheros basándose
en su nombre. Podríamos usar FilenameFilter para implementar unos sencillos
patrones de búsqueda de ficheros como foo*.

También podemos encontrar otros streams de entrada y salida en el paquete java.util.zip,


incluyendo estos:

CheckedInputStream y CheckedOutputStream
Una pareja de streams de entrada y salida que mantiene un checksum de los
datos que están siendo leídos o escritos.
DeflaterOutputStreamy InflaterInputStream
Comprime o descomprime los datos que están siendo leídos o escritos.
GZIPInputStream y GZIPOutputStream
Lee y escribe datos comprimidos en el formato GZIP.
ZipInputStream y ZipOutputStream
Lee y escribe datos comprimidos en el formato ZIP.

32

S-ar putea să vă placă și