Lección 08


Entrada y salida de datos


El acceso a disco en Java se hace utilizando las clases del paquete java.io. Hay dos grupos de clases que hay que distinguir: las correspondientes a archivos y directorios, relacionadas con la clase File y las correspondientes a corrientes de datos (streams), relacionadas con las clases abstractas InputStream y OutputStream y con la clase PrintStream. Las corrientes de datos se pueden generar de muchas formas, una de ellas es usando archivos, pero no es la única que se va a utilizar pues debido a que los archivos no pueden usarse en los applets (por razones de seguridad), la lectura de datos en un applet debe hacerse con corrientes de datos relacionadas con URL's (Unique Resource Locators) y no con archivos. El estudio de los URL's se hace en la lección 9.


Archivos y directorios (File)


Un archivo (File) en Java no es como un archivo del DOS. En Java se utiliza el concepto de archivos de Unix, es decir, tanto los archivos de datos como los directorios se representan como objetos de una misma clase, la clase File. De hecho la clase File sirve para representar los aspectos externos de los archivos, no sirve ni para leer ni para escribir en ellos. Esto puede apreciarse claramente echando un vistazo a las variables y los métodos de la clase:

public File(String dirección) throws NullPointer Exception;
public File(String directorio,String nombre) throws NullPointer Exception;
public File(File dir, String nombre);
// Constantes
public static final String pathSeparator;

public static final Char pathSeparatorChar;
public static final String separator;
public static final Char separatorChar;
// Métodos
public boolean canRead();
public boolean canWrite();
public boolean delete();
public boolean equals(Object ob);

public boolean exists();
public String getAbsolutePath();
public String getName();
public String getParent();
public String getPath();
public int hashCode();
public boolean isAbsolute();
public boolean isDirectory();
public boolean isFile();
public long lastModified();
public long length();
public String[] list();
public String[] list(FilenameFilter f);
public boolean mkdir();

public boolean mkdirs();
public boolean renameTo(File dest);
public String toString();

Hay tres constructores. El primero pide como parámetro una dirección que puede ser la de un directorio o la de un archivo. El segundo pide la dirección de un directorio y el nombre de un subdirectorio o un archivo. La tercera pide un objeto de la clase File, que debe corresponder a un directorio, y el nombre de un subdirectorio o archivo. Para llamar estos constructores no es necesario que los archivos o directorios existan. De hecho el método exists() nos informa de la existencia de un archivo y el método mkdirs() crea todos los directorios necesarios para que un objeto de la clase File corresponda a un directorio real en el disco. Los métodos isFile() e isDirectory() nos informan si el objeto de la clase File es un archivo o un directorio. getName() devuelve el nombre del archivo o directorio y getPath() devuelve la dirección relativa al directorio actual. getAbsolutePath() devuelve la dirección absoluta, es decir referida a la raíz del disco.  isAbsolute() nos informa si la dirección del archivo o directorio está dada (por construcción) en forma absoluta. El método delete()  borra el directorio o archivo del disco y el método renameTo(String File dest) cambia y/o mueve el archivo (o directorio) a la nueva dirección. canRead() y canWrite() nos informan si el archivo se puede leer y si se puede escribir en él respectivamente. Ambos métodos devuelven false si se trata de un directorio.

Las constantes pathSeparatorChar y separatorChar son los caracteres que el sistema operativo usa como separador de directorios en la variable PATH  y como separador de directorios en los nombres de archivos. En DOS y Windows pathSeparatorChar=';' y separatorChar='\'. pathSeparator y separator son los mismos separadores pero en forma de cadenas en lugar de caracteres, en DOS y en Windows: pathSeparator=";" y separatorChar="\" .

El método list() devuelve los nombres de todos los archivos y subdirectorios contenidos en nuestro objeto de la clase File.

El método list(FilenameFilter f) devuelve sólo los archivos o directorios que el filtro f acepta. Filtername es una interfaz del paquete java.io con un solo método

public boolean accept(File directorio,String name);

de manera que un FilternameFilter es cualquier clase que implemente esta interfaz.

El siguiente ejemplo muestra una implementación de un filtro que acepta sólo los archivos con una cierta extensión. La aplicación usa args[0] para el directorio y args[1] como la extensión para el filtro.

Para probar el ejemplo hay que usar una ventana del DOS y pasar al directorio ejem08. Todos los ejemplos de esta lección serán aplicaciones y no applets porque los applets no tienen acceso a los archivos.

Una observación importante que hay que hacer es la ausencia de métodos de lectura o escritura en la clase File. Para leer el contenido de un archivo o escribir en él es necesario utilizar otras clases. Hay dos opciones. La primera es utilizar la clase RandomAccessFile que proporciona todos los métodos necesarios para leer y escribir en archivos. La otra opción es utilizar corrientes de datos o Streams. La clase RandomAccessFile proporciona una alternativa de entrada y salida cómoda y completamente independiente de los Streams. Pero es importante señalar que la alternativa de los Streams es más general y muy útil y la única posible para internet, por lo que se le dedica una sección aparte.


Lectura y escritura de archivos

(RandomAccessFile)


La clase RandomAccessFile proporciona todos los métodos necesarios para leer y escribir en archivos. Contra lo que se podría suponer, la clase RandomAccessFile  no es una extensión de File. Son clases complementarias. Como se señaló anteriormente, File representa directorios y archivos, pero no proporciona métodos para acceder al interior de los archivos. En cambio RandomAccessFile sólo representa archivos y proporciona precisamente los métodos para leer su contenido y escribir en ellos. Esta clase es suficiente para todas las aplicaciones locales, sin embargo para acceder a datos en internet será necesario utilizar los Streams que se estudian en la siguiente sección. Éstos son los constructores de RandomAccessFile:

public RandomAccessFile(String nombre,String modo) throws IOException
public RandomAccessFile(File f,String modo) throws IOException

El segundo parámetro en ambos casos debe ser "r" o "rw" que significan que el archivo es de sólo lectura o de lectura y escritura respectivamente. El primer parámetro debe ser la dirección completa de un archivo o bien un objeto de tipo File que deberá ser un archivo y no un directorio. Llamar a uno de estos constructores abre el archivo si ya existe o lo crea si aún no existe, siempre y cuando la dirección sí exista. Todos los métodos de RandomAccessFile arrojan IOException, por lo cual todo lo que se programe usando esta clase debe escribirse dentro de una construcción:

try { ... } catch (IOException ioe) {}

RandomAccessFile tiene unos métodos para saber el tamaño del archivo en bytes y poder cerrarlo cuando se ha terminado de trabajar con él:

public long length() throws IOException
public void close() throws IOException

y otros que permiten saber la localización del puntero del archivo y colocarlo en una posición arbitraria:

public long getFilePointer() throws IOException
public void seek(long pos) throws IOException

Para leer y escribir RandomAccessFile tiene todos los métodos de las interfaces DataInput y DataOutput que se utilizan también con Streams, lo cual resulta cómodo pues sólo hay que aprender a usar un conjunto de métodos.

public interface DataInput {
  public boolean readBoolean() throws IOException;
  public byte readByte() throws IOException;
  public char readChar() throws IOException;
  public double readDouble() throws IOException;
  public float readFloat() throws IOException;
  public void readFully(byte[] b) throws IOException;
  public void readFully(byte[] b,int pos,int n) throws IOException;
  public int readInt() throws IOException;
  public String readLine() throws IOException; 
  public long readLong() throws IOException;
  public short readShort() throws IOException;
  public String readUTF() throws IOException;
  public int readUnsignedByte() throws IOException;
  public int readUnsignedShort() throws IOException;
  public int skipBytes(int n) throws IOException;
}

public interface DataOutput
{

  public void write(int v) throws IOException;
  public void write(byte[] b) throws IOException;
  public void write(byte[] b,int pos,int n) throws IOException;
  public void writeBoolean(boolean b) throws IOException;
  public void writeByte(byte b) throws IOException;
  public void writeBytes(String s) throws IOException;
  public void writeChar(int v) throws IOException;
  public void writeChars(String s) throws IOException;
  public void writeDouble(double v) throws IOException;
  public void writeFloat(float v) throws IOException;
  public void writeInt(int v) throws IOException;
  public void writeLong(long v) throws IOException;
  public void writeShort(short v) throws IOException;
  public void writeUTF(String s) throws IOException;
}

Los métodos de lectura leen, a partir de la posición del puntero, el número de bytes que ocupa el dato que se está leyendo y avanzan el puntero esos mismos bytes. Los de escritura igual, escriben a partir de donde está el puntero los bytes necesarios y avanzan el puntero. Algunos de estos métodos requieren de una explicación. readUnsignedByte y readUnsignedShort están diseñados para leer datos que fueron escritos por programas hechos en C. El resultado de la lectura es un entero pero el avance sólo es de uno y dos bytes respectivamente. Los métodos readFully leen todo lo que pueden hasta que se ha llenado la matriz de bytes (o la zona de n bytes que comienza en pos) o hasta que se termina el archivo, lo primero que ocurra. skipBytes(int n) sólo avanza el puntero saltándose n bytes. Los métodos de escritura pueden hacer crecer el archivo.  readLine() lee todos los bytes que encuentra antes de un paso de línea y los devuelve en una cadena. Es un método útil para leer archivos ascii. Los métodos writeBytes(String s) y writeChars(String s) ambos escriben una cadena pero el primero escribe un byte por caracter y el segundo dos bytes por caracter respetando el standard UNICODE. readUTF y writeUTF leen y escriben cadenas UNICODE en un formato llamado UTF-8 que se usa frecuentemente para transmitir datos y es compatible con ascii.

Como segundo ejemplo de esta lección presentamos la aplicación Windows llamada disco.class que utiliza File y RandomAccessFile. disco es una aplicación que sirve para explorar el disco en busca de archivos con ciertas extensiones y desplegar su contenido. Si un archivo tiene extensión txt, javahtml o htm, con un doble click se despliega su contenido en un marco de la clase miraTexto. Si los archivos son imágenes en formato gif, jpg o jpeg,   un doble click hace que se despliegue la imagen en un marco de la clase miraImagen.  En la clase miraTexto se hace uso de RandomAccessFile y de uno de sus métodos readFully. También se utiliza java.awt.Toolkit para acomodar el marco en el centro de la pantalla.  En la clase miraImagen se utiliza java.awt.Toolkit además para leer imágenes. Las clases miraTexto y miraImagen se utilizan también en los ejemplos de la lección 9 donde se desarrolla un sencillo explorador de archivos y directorios para internet que también funciona en el disco local. En particular los constructores que tienen un URL como parámetro sólo se utilizan en dichos ejemplos y el alumno puede posponer su estudio.

Estos son los contenidos de miraTexto.java , miraImagen.java y disco.java:

El alumno observará que la clase disco implementa FilenameFilter y ordenable. Lo segundo es para utilizar la clase insercionBinariaBis que se desarrolló en la lección 2 y aquí se usa para ordenar alfabéticamente el contenido de los directorios. En este ejemplo queda claramente separada la función de la clase File que nos permite navegar por el disco y su aplicación se concentra en disco.class, mientras que la aplicación de RandoAccessFile está en la lectura de los archivos de texto dentro de la clase miraTexto.

Ambas clases miraTexto y miraImagen utilizan un hilo cuando leen los datos que van a desplegar, por eso implementan Runnable. Esto se hace para que los programas que las utilicen puedan realizar otras operaciones mientras se realiza la lectura.


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

Ejercicios de la Lección 08.


José Luis Abreu y Marta Oliveró