>>
Lección 16


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.


Índice

Ejercicios de la Lección 16.


José Luis Abreu y Marta Oliveró