Académique Documents
Professionnel Documents
Culture Documents
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:
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.
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.
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.
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
4
Como se ha mencionado, dos de las clases de streams de bytes, ObjectInputStream y
ObjectOutputStream, se usan para la serialización de objetos.
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)
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)
5
int write(int c)
int write(byte cbuf[])
int write(byte cbuf[], int offset, int length)
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:
Observa que tanto el grupo de streams de caracteres como el bytes contienen parejas
paralelas que operan con el tipo de sinks de datos.
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).
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.
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.
import java.io.*;
in.close();
out.close();
}
}
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.
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.
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.
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.
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:
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.
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.
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.
El método reverse contiene algún código interesante; en particular estas dos sentencias:
10
El programa lee desde el BufferedReader, que a su vez lee desde source.
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.
• RhymingWords
• ReverseThread
• SortThread
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.
SequenceInputStream
Concatena varios streams de entrada en un sólo stream de entrada.
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.
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.
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
import java.io.*;
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.*;
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;
}
}
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.
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.
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.
• 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.
Para usar un stream de entrada o salida filtrado, adjuntamos el stream filtrado a otro
stream de entrada o salida.
Podríamos hacer esto para poder usar los métodos readXXX más convenientes, como un
readLine, implementado por DataInputStream.
15
Cómo usar DataInputStream y DataOutputStream
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:
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.
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.
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.
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.
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.
La clase CheckedOutputStream
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)
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.
19
cksum.update(b);
}
return b;
}
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 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.
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.*;
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;
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.
21
Filtrar Ficheros de Acceso Aleatorio
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
La clave para escribir objetos es representar su estado de una forma serializada suficiente
para reconstruir el objeto cuando es leído.
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:
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.
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.
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.
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.
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:
Este método escribe cualquier cosa necesaria para reconstruir un ejemplar de la clase,
incluyendo lo siguiente:
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
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:
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.
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;
}
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.
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.
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:
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.
Y esta abre el mismo fichero tanto para lectura como para escritura:
Después de haber abierto el fichero, podemos usar los métodos comunes readXXX o
writeXXX para realizar I/O en el fichero.
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
29
this.out = out;
}
Aquí están las únicas modificaciones echas a CheckedOutputStream para crear un filtro
que funcione sobre objetos DataOutput.
Nota:
30
}
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:
CheckedRAFTest crea dos RandomAccessFiles, uno para leer y uno para escribir, y usa
el filtro checksum sobre ellos.
• 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.
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*.
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