>>
Lección 17


Corrientes de datos (Streams)


Esta sección no tiene ejemplos. Sin embargo tanto en la última sección de la lección como en la siguiente lección aparecen varios ejemplos de aplicaciones de corrientes de datos o Streams. 

El paquete java.io tiene varias clases que son derivadas de dos clases básicas: InputStream y OutputStream y que representan corrientes de entrada de bytes provenientes de una fuente o de salida de bytes que tienen un destino definido, respectivamente. La fuente de una corriente de entrada o InputStream puede ser un archivo, el teclado, un array de bytes en la memoria RAM, una conexión a un URL en internet o muchas otras cosas. El destino de una corriente de salida o OutputStream puede ser un archivo, una impresora, un array de bytes, una conexión a un URL en internet o muchas otras cosas.

Éstos son las variables y los métodos de InputStream y OutputStream:

public abstract class InputStream {
  public InputStream(); // vacío
  public int available() throws IOException;

  public void close() throws IOException;
  public synchronized void mark(int límite);
  public boolean markSupported();
  public abstract int read() throws IOException;
  public int read(byte[] b) throws IOException;
  public int read(byte[] b,int pos, int n) throws IOException;
  public synchronized void reset() throws IOException;
  public long skip(long n) throws IOException;
}

public abstract class OutputStream {
  public OutputStream(); // vacío
  public void close() throws IOException;

  public void flush() throws IOException;
  public abstract void write(int b) throws IOException;
  public void write(byte[] b) throws IOException;
  public void write(byte[] b,int pos, int n) throws IOException;
}

Como puede verse estas clases proporcionan los métodos básicos de lectura y escritura y algunos otros que ahora explicamos. available() nos dice el número de bytes que se pueden leer sin bloqueo. Esto quiere decir que son los bytes que se pueden leer sin tener que esperar por más, que están ya disponibles. Este método no debe emplearse para intentar averiguar si una cooriente de datos ya se agotó pues para muchas corrientes de entrada el resultado suele ser siempre cero y eso no quiere decir que ya se llegó al final. Por supuesto close() en ambas clases sirve para cerrar la corriente y liberar los recursos que puedan estarse usando. Si markSupported() regresa true entonces se pueden utilizar los métodos mark(int límite) y reset(), el primero marca la posición de la lectura mientras no se lean más de límite bytes y reset() regresa la lectura a ese lugar, siempre y cuando no se haya excedido el límite de bytes leidos. skip(int n) hace que la lectura prosiga saltándose n bytes. flush() obliga a que se escriba físicamente lo que aún esté en el almacén temporal de escritura cuando se está utilizando un BufferedOutputStream.

Se pueden crear Input y Output Streams a partir de objetos de la clase File. El siguiente ejmplo muestra cómo se crean y se usan estas corrientes. El ejemplo es un programa de línea de comandos que copia un archivo a otro.

Estas dos clases son abstracciones que nos permiten usar las interfaces DataInput y DataOutput presentadas en relación con RandomAccessFile, para leer datos de cualquier fuente o escribirlos en cualquier destino. Para poder hacer esto se necesita poder crear un objeto de la clase InputStream o de la clase OutputStream lo cual se logra gracias a una variedad de métodos de diversas clases que permiten crear InputStreams y OutputStreams en varias situaciones. Luego se pueden usar las clases DataInputStream y DataOutputStream que proporcionan los métodos de lectura y escritura de las interfaces DataInput y DataOutput para cualquier InputStream o OutputStream respectivamente. Esto último se logra gracias a los constructores:

public DataInputStream(InputStream is);
public DataOutputStream(OutputStream os);

cada uno de su correspondiente clase.

Se puede crear un objeto de la clase InputStream con una matriz de bytes como fuente. Para ello se utiliza uno de los constructores de la clase ByteArrayInputStream:

public ByteArrayInputStream(byte[] b);
public ByteArrayInputStream(byte[] b,int pos,int n);

Para leer y escribir cadenas de caracteres (Strings) deben usarse las clases derivadas de Reader y Writer respectivamente pues convierten correctamente bytes a caracteres y viceversa. Por ejemplo, para leer cadenas escritas por el usuario en la línea de comandos, debe crearse un BufferedReader y para ello se convierte el InputStream System.in (que lee bytes) primero en un InputStreamReader que convierte los bytes a caracteres usando la convención local (o la que se le especifique)  y luego éste en un BufferedReader que hace la lectura más eficiente y tiene un método readLine() que es que suele usarse para leer cada línea escrita por el usuario cuando éste pulsa intro o return. La línea típica para realizar esta construcción es:

BufferedReader br=new BufferedReader(new InputStreamReader(System.in));

Luego pueden leerse las cadenas que el usuario escribe con líneas como ésta:

System.out.print("escriba su nombre:"); br.readLine();

El ejemplo pipeApplet de la siguiente sección usa un FileReader que realiza una función parecida pero en la lectura desde un archivo. Si lo que se desea es escribir en un archivo de texto lo que conviene usar es un FileWriter.

Si se desea tener un array de bytes como destino de un OutputStream, se puede crear usando los constructores de ByteArrayOutputStream:

public ByteArrayOutputStream();
public ByteArrayOutputStream(int n);

Estos constructores proporcionan un OutputStream al que se puede enviar toda la información que uno desee. El resultado de la construcción es una matriz de bytes (en el segundo caso comienza con un tamaño n) que va creciendo a medida que se necesite y que alberga toda la información que se le ha enviado. La matriz resultante se puede obtener usando el método:

public synchronized byte[] toByteArray();

de la misma clase ByteArrayOutputStream y que da solamente los bytes que se han escrito. El ejemplo pipeApplet de la siguiente sección usa un ByteArrayOutputStream.

También se pueden crear InputStreams y OutputStreams a partir de archivos usando los constructores de la clase FilInputStream y FileOutputStream respectivamente:

public FileInputStream(File fuente);
public FileOutputStream(File destino);

También hay constructores para estas clases partiendo de las direcciones de los archivos en forma de cadenas o partiendo de su FileDescriptor.

Finalmente mencionaremos sólo superficialmente algunas extensiones de InputStream y OutputStream que pueden tener alguna utilidad:

FilterInputStream y FilterOutputStream son subclases de InputStream y OutputStream que sirven para filtrar o transformar los datos. Las clases sólo añaden los constructores:

public FilterInputStream(InputStream is);
public FilterOutputStream(OutputStream out);

Si no se hacen subclases sobreescribiendo los constructores, el resultado es el InputStream o el OutputStream originales. Es precisamente al sobreescribir los constructores que se deben definir los filtros. Por ejemplo se puede definir un filtro que codifique datos de manera que sólo se puedan entender si se descodifican, es decir, si se pasan por el filtro inverso.

Casi todas las otras subclases se construyen como subclases de FilterInputStream y FilterOutputStream para aprovechar los posibles filtros que se deseen utilizar, aunque en la mayoría de las aplicaciones no se usa ningún filtro.

PrintStream es una subclase de FilterOutputStream que implementa una serie de métodos llamados print y println que sirven para escibir cómodamente datos mezclando cadenas con números. Estos métodos convierten todo a cadenas. El alumno ya ha utilizado muchas veces un PrintStream que es System.out, por lo que ya está familiarizado con su funcionamiento. Actualmente la clase PrintStream sólo se usa en System.out, para escribir texto en impresoras o archivos se debe usar siempre Writers: PrintWriter o FileWriter respectivamente.

BufferedInputStream y BufferedOutputStream son subclases que se obtienen usando los constructores

public BufferedInputStream(InputStream is);
public BufferedInputStream(InputStream is,int tamaño);
public BufferedOutputStream(OutputStream is);
public BufferedOutputStream(OutputStream is,int tamaño);

Lo que hacen es usar un "buffer" o almacén temporal con el objeto de mejorar la eficiencia de la lectura o la escritura. En el caso de la escritura es importante llamar al método flush() antes de cerrar la corriente. En los constructores tamaño especifica el número de bytes que se usarán en el buffer.

LineNumberInputStream y PushbackInputStream son otras dos extensiones de FilterInputStream. La primera tiene sólo dos métodos más: int getLineNumber() y void setLineNumber(int L) que permiten llevar control del número de líneas en corrientes de texto. Por supuesto, getLineNumber() devuelve el número de línea en que se encuentra el puntero. setLineNumber(int L) define una nueva numeración de las líneas, lo cual se usa por ejemplo cuando al comenzar una nueva corriente se le quiere asignar el número de líneas de la corriente anterior para numerar las líneas como si provinieran de una sola corriente. PushbackInputStream tiene un solo método público extra: unread(char ch) cuya función es agregar artificialmente a la corriente un caracter extra pecisamente en el lugar donde se encuentra. Esto es útil para convertir archivos de texto de sistemas operativos diferentes donde los pasos de línea pueden estar dados por caracteres diferentes.

SequenceInputStream es una subclase de InputStream que permite concatenar varios InputStreams. Tiene dos constructores:

public SequenceInputStream(InputStream s1,InputStream s2);
public SequenceInputStream(Enumeration e);

El primero crea un InputStream que consiste en s1 seguido de s2, El segundo tiene como parámetro una enumeración que debe ser de InputStreams y el resultado es un InputStream que recorrerá en orden todos los que vienen especificados en la enumeración.

Antes de terminar esta lección con el estudio de las clases PipedInputStream, PipedOutputStream, PipedReader y PipedWriter, mencionamos otra clase del paquete java.io: StreamTokenizer. Su función es realizar análisis lexicográficos sobre una corriente de bytes. Es parecida a la clase java.util.StringTokenizer que hemos utilizado en algunos ejemplos pero su funcionamiento es más complejo y especializado. No la estudiaremos en este curso.


Tuberías (Pipes)


PipedInputStream y PipedOutputStream son subclases de InputStream y OutputStream que juntas representan las llamadas tuberías (en inglés pipes) de bytes.  Estos son los métodos de ambas clases:

public class PipedInputStream extends InputStream
{
  public PipedInputStream(PipedOutputStream fuente) throws IOException;
  public PipedInputStream(); // vacío
  public void close() throws IOException;
  public void connect(PipedOutputStream fuente) throws IOException;
  public synchronized int read() throws IOException;
  public synchronized int read(byte[] b,int pos,int n) throws IOException;
}

public class PipedOutputStream extends OutputStream
{
  public PipedOutputStream(PipedInputStream destino) throws IOException;
  public PipedOutputStream(); // vacío
  public void close() throws IOException;
  public void connect(PipedInputStream destino) throws IOException;
  public void write() throws IOException;
  public void write(byte[] b,int pos,int n) throws IOException;
}

En realidad lo único que hacen estas clases es transformar un InputStream en OutputStream o viceversa. Si un programador desea que la salida de datos de algún proceso se convierta en la entrada de otro proceso basta que cree un par estos objetos de esta manera:

PipedOutputStream pos=new PipedOutputStream();
PipedInputStream pis=new PipedInputStream(pos);

Hecho esto debe asignar como salida del primer proceso a pos y como entrada del segundo a pis. El encadenamiento puede hacerse al revés o creando ambos sin parámetro y usando pis.connect(pos) o pos.connect(pis), da lo mismo.

Análogamente, para hacer tuberías con corrientes de caracteres, se utilizan PipedWriter y PipedReader.

El siguiente ejemplo es un applet en el que se construye una tubería (pipe) que encadena cinco procesos usando cuatro apareamientos: PipedWriter-PipedReader de la forma:

PipedWriter pw=new PipedWriter();
PipedReader pr=new PipedReader(pw);

Observe que los métodos que hacen las transformaciones de caracteres se programaron usando BufferedReader y BufferedWriter, por lo cual hay que crear objetos de estos tipos a partir de los PipedWriter y PipedReader.

 

Cada vez que se cambia el estado de un selector, el applet arranca cinco hilos que se encargan cada uno de hacer uno de los cambios al texto del primer campo de texto, o simplemente de pasar cada caracter sin modificarlo. También se arranca un hilo que espera hasta que todos los otros hilos han parado y entonces obtiene el resultado y lo pone en el segundo campo de texto. La larga espera se debe a que cada hilo tarda algún tiempo en darse cuenta que su InputStream se ha terminado.

Éste es el código del applet:

Para construir este ejemplo se creó una clase abstracta llamada transChars que se encarga de procesar todos los caracteres provenientes de un BufferedReader usando el método

public abstract char Transform(char b);

y pasar el resultado a un BufferedWriter. Éste es el código de transChars:

También se construyeron seis subclases de transChars que sólo implementan un constructor para determinar su BufferedReader y su BufferedWriter, y un método Transform para realizar su trabajo. copyChars sólo pasa cada caracter sin hacerle nada, quitaAcentos, aMayusculas, aMinusculas, sinVocales y sinConsonantes hacen más o menos lo que sus nombres indican. Aquí está el código de estas seis clases.


Índice


José Luis Abreu y Marta Oliveró