Download El sistema de archivos en Java

Document related concepts
no text concepts found
Transcript
Soporte de operaciones de entrada y salida: java.io
Java dispone de una serie de librerías (paquetes) estándar que ofrecen clases para las
necesidades más comunes, como se puede ver en la Tabla A. En este artículo abordaremos el
soporte de Java para operaciones de entrada y salida, así como para la gestión del sistema de
archivos, que se encuentra en el paquete java.io. Cuando pensamos en las operaciones de entrada
y salida, se suele pensar en dos tipos de operaciones: operaciones de lectura/escritura sobre
archivos, y operaciones de introducción de datos mediante el teclado. Si bien esto resulta muy
común, a la hora de la verdad, una aplicación puede obtener datos de muchas otras formas: a
través de una conexión vía Internet con un servidor remoto, a través de una conexión DDE con otro
programa en la misma máquina, o incluso a través del portapapeles de Windows. Se puede ver
que, a pesar de la distinta procedencia de la información en todos estos casos, su manejo será
bastante similar: solicitamos al sistema que nos conecte a la fuente o destino de la información
(abrimos archivo, nos conectamos al servidor de red, etc.), obtenemos la información, que será una
serie de datos secuenciales, y nos desconectamos de la fuente de datos, para liberar recursos del
sistema. Java utiliza el concepto de flujo (stream) para trabajar con información manejada
secuencialmente.
java.applet
Librería para creación y manejo de applets
java.awt
Librería de interface gráfico
java.io
Librería para operaciones de Entrada/Salida
java.lang
Librería con clases básicas
java.net
Librería de soporte para programación en red
java.util
Clases de utilidad, como pilas, etc.
Tabla A: Las librerías estándar de Java.
No siempre se puede o es conveniente trabajar secuencialmente: hay ocasiones en que deseamos
tener acceso aleatorio, en lugar de leerla en serie hasta que llegamos a la posición donde está la
información que deseamos. Java también proporciona soporte para acceso aleatorio a archivos, a
través de la clase RandomAccessFile. El acceso al sistema de archivos del sistema también es
fundamental: es necesario poder renombrar archivos, obtener la lista de archivos de un directorio,
saber si un archivo es de escritura o lectura, etc. Java proporciona la clase File para manejar el
sistema de archivos, independientemente de las clases basadas en flujos y de acceso aleatorio
que utiliza para leer/escribir en ellos. La Tabla B muestra las clases de entrada y salida que se
pueden encontrar en java.io.
InputStream
ByteArrayInputStream
FileInputStream
PipedInputStream
SequenceInputStream
StringBufferInputStream
FilterInputStream
BufferedInputStream
DataInputStream
LineNumberInputStream
PushbackInputStream
OutputStream
ByteArrayOutputStream
FileOutputStream
PipedOutputStream
FilterOutputStream
BufferedOutputStream
DataOutputStream
PrintStream
File
FileDescriptor
RandomAccessFile
StreamTokenizer
Tabla B: Clases en el paquete java.io
Por último, Java proporciona una clase de excepción para cada tipo de error común en las
operaciones de entrada y salida: cada una de estas clases deriva de la clase base IOException.
Abordaremos cada una de estas partes del paquete java.io por separado.
Flujos de datos
El concepto de flujo es muy potente, dado que proporciona un modo de tratar las operaciones de
entrada/salida de forma similar para distintas fuentes de datos y canales de comunicación.
Podemos definir un flujo como una secuencia de bytes que viajan desde una fuente a un destino a
través de un camino, de modo secuencial. Java proporciona un conjunto de clases para leer
información desde un flujo, y otro para escribir en él. Las dos clases fundamentales son
InputStream y OutputStream, para lectura y escritura respectivamente, que proporcionan métodos
para realizar las operaciones básicas: en función de las distintas fuentes o modos de manejar la
información se tendrán distintas clases derivadas de éstas, siempre respetando el protocolo básico
dictado por ellas. El esquema básico de trabajo con los flujos es siempre el mismo: se abren, lo
que se consigue con las operaciones new InputStreamFile(...), etc., se realizan las operaciones
deseadas de escritura y lectura con read, write, etc., y luego se cierran, con close(). El Listado A
muestra un programa que lee un archivo y lo copia en otro, utilizando flujos. Nótese que utilizamos
las clases FileInputStream y FileOutputStream, derivadas de InputStream y OutputStream y con los
mismo métodos.
import java.io.*;
public class entrada_salida1 {
static void main( String args[] )
throws IOException
{
int caracter;
int fin_archivo = -1;
// Creamos y abrimos los flujos
InputStream entrada =
new FileInputStream( "c:\\autoexec.bat" );
OutputStream salida =
new FileOutputStream( "c:\\copia.aux" );
// Realizamos operaciones de entrada y salida
caracter = entrada.read();
while( caracter != fin_archivo ) {
salida.write( caracter );
caracter = entrada.read();
}
// Cerramos los flujos
entrada.close();
salida.close();
}
};
Listado A: Programa que copia un archivo en otro (entrada_salida1.java)
Vale la pena destacar un par de puntos en el Listado A: en primer lugar, en cualquier punto del
programa se puede producir un error de entrada/salida, del tipo IOException, motivo por el que lo
hemos indicado en la primera línea del método main, mediante el código throws IOException: no
hay peligro de olvidar esto, porque Java se negará a compilar, como ya vimos en el artículo de
Marzo sobre excepciones. Otro punto importante es que hemos creado objetos de las clases
FileInputStream y FileOutputStream, pero los hemos asignado a variables de las clases
InputStream y OutputStream: esto funciona debido al polimorfismo. Dado que las clases
FileXXXStream son derivadas de las clases XXXStream, se llamará al método adecuado de
FileXXXStream, clases a las que realmente pertenecen los objeto asignados a la variable. Además
de funcionar, este modo de codificar es conveniente: así, si deseamos copiar el archivo a pantalla,
bastará con asignar a salida un flujo que sea capaz de escribir en pantalla, en lugar de en un
archivo, como puede ser System.out, que ya hemos utilizado en otros artículos, y que es un objeto
de la clase PrintStream, derivada de OutputStream. Bastará con escribir OutputStream salida =
System.out; en lugar de
OutputStream salida = new FileOutputStream...; sin modificar ningún
otro fragmento de código (se puede encontrar el código fuente en el archivo entrada_salida2.java
incluido en el disco). Alternativamente, en lugar de enviar la información de salida a la pantalla
podríamos escribir en memoria compartida entre varios procesos, a una conexión remota, o a casi
cualquier cosa capaz de almacenar información, siempre que tengamos una clase XXXStream
adecuada. En el Listado B se pueden ver los métodos de OutputStream y en la Listado C los de
InputStream. Estudiaremos estas dos clases básicas en detalle, y luego las clases derivadas,
explicando en qué se diferencian y qué añaden. Todo lo que sepamos de las clases base se
cumplirá también para las derivadas: al fin y al cabo, en Java cuando derivamos de una clase
estamos comprometiéndonos a que la clase derivada se comporte como esta, posiblemente
añadiendo nuevas capacidades.
public abstract class
java.io.OutputStream
extends java.lang.Object
{
// Constructores
public OutputStream();
// Métodos
public void write(byte b[]);
public void write(byte b[], int comienzo, int l);
public abstract void write(int b);
public void close();
public void flush();
}
Listado B: La clase base para flujos de salida/escritura, OutputStream
La clase OutputStream proporciona varios métodos para escritura, todos llamados write (en el
número de Febrero comentamos que Java soportaba la sobrecarga, es decir, tener varios métodos
con el mismo nombre). La primera versión del listado proporciona la posibilidad de escribir varios
bytes a la vez (un array), así como el segundo, que permite indicar qué parte del array es la que
deseamos tratar: comienzo es el lugar desde el que deseamos comenzar a copiar, y l el número de
bytes a partir de dicha posición. Por fin, la última versión de write, que es la que hemos utilizado en
nuestro programa, simplemente escribe un entero. Otro método importante es close, que cierra el
flujo, y que es importante no olvidar si deseamos que no se pierda información inadvertidamente.
En cuanto a flush, es un método destinado a asegurarse de que realmente se guarda la
información: muy a menudo ésta no va a parar directamente al lugar de destino, sino que se
guarda en memoria intermedia, de modo que se escriba todo un bloque de información de una vez
para evitar continuos accesos al lugar de destino, haciendo así más rápido el proceso. Esto, sin
embargo, puede hacer que perdamos información si se cae el sistema, motivo por el que existe
flush, que fuerza la escritura rea. Como se puede ver, OutputStream es una clase abstracta,
destinada a ofrecer un protocolo estándar a todos los flujos de escritura, y nada más: cada clase
derivada se encarga de implementar cada método de la manera más adecuada al destino de la
información y al canal utilizado para transmitirla.
public abstract class
java.io.InputStream
extends java.lang.Object
{
// Constructores
public InputStream();
// Métodos
public abstract int read();
public int read(byte b[]);
public int read(byte b[], int comienzo, int l);
public void close();
public long skip(long n);
public int available();
public void mark(int limiteLectura);
public boolean markSupported();
public void reset();
}
Listado C: La clase base de entrada/lectura, InputStream
La clase InputStream es la contraparte de OutputStream utilizada para lectura. Para leer del flujo
contamos con tres versiones del método read: la primera versión en el Listado C lee un entero, y
la segunda y la tercera un array de bytes, devolviendo el número de bytes leídos. Todos estos
métodos devuelven -1 para indicar que se ha encontrado el final del flujo, y no hay más datos a
recuperar. Al igual que con los flujos de salida, es necesario cerrar un InputStream una vez que
hemos terminado de utilizarlo, mediante close. Aparte de estos métodos, tenemos skip, que
avanza n bytes en el flujo de entrada, saltándoselos, y available, que determina el número de
bytes que se pueden leer. Nótese que es posible que available devuelva 0 siempre para cierto tipo
de flujos en algunos sistemas, por lo que se ha de tener precaución a la hora de utilizarlo. Otra
posibilidad interesante en un flujo es la capacidad de recordar la posición donde hemos estado en
un momento dado, para luego volver a ella: esto se implementa mediante mark, que memoriza la
posición actual, método al que se le pasa como parámetro el número de bytes que se pueden leer
sin que el marcador quede invalidado. El método reset nos devuelve a la última posición marcada.
Es posible que determinados tipos de flujos no soporten la posibilidad de marcar cierta posición y
volver a ella: por ejemplo, se podría plantear si tiene sentido volver a una posición anterior en un
flujo de entrada asociado a la entrada por teclado. Para averiguar si cierto flujo soporta o no el uso
de marcadores se puede utilizar markSupported. Nótese que el hecho de que no se pueda
garantizar la validez del uso de marcadores no es un problema de la implementación en Java, sino
un reflejo de la diversidad de los dispositivos y de los que se puede obtener información de entrada
en el mundo real, cada uno con sus distintas capacidades.
Escritura/lectura secuencial de archivos
Como hemos visto en el Listado A, Java soporta la escritura/lectura de archivos mediante las
clases FileOutputStream y FileInputStream, derivadas de OutputStream e InputStream,
respectivamente. Absolutamente todos los métodos que hemos visto para las clases base de
manejo de flujos funcionan tal y como se vio, por lo que solo expondremos las novedades que
presentan estas clases, o las pequeñas variaciones que puedan tener algunos métodos. El
Listado D muestra los nuevos métodos y constructores de FileOutputStream. Evidentemente, para
construir un flujo que funcione sobre un archivo, habrá que especificar de algún modo el archivo: el
mejor lugar para ello es el constructor del flujo. La clase proporciona tres constructores para
especificar el archivo: el primero en el listado permite indicarlo simplemente especificando el
nombre. El segundo constructor recibe como parámetro un objeto de la clase File, utilizado por
Java para representar los archivos y manejarlos (renombrarlos, eliminarlos, etc.), y cuyo estudio
abordaremos más adelante. El tercer constructor toma como parámetro un FileDescriptor, que es
otra clase utilizada para representar un archivo: la estudiaremos junto con File. Por último, tenemos
un único método nuevo, getFD, que simplemente devuelve el FileDescriptor asociado al archivo
sobre el que trabaja el flujo.
public class java.io.FileOutputStream
extends java.io.OutputStream
{
// Constructores
public FileOutputStream(String nombreArchivo);
public FileOutputStream(File archivo);
public FileOutputStream(FileDescriptor fd);
// Métodos
public final FileDescriptor getFD();
// ...
}
Listado D: Métodos que FileOutputStream añade con respecto a OutputStream.
En cuanto a FileInputStream, añade exactamente los mismos constructores y métodos con
respecto a InputStream que FileOutputStream con respecto a OutputStream.
Flujos en memoria
Es posible que deseemos manejar a veces un buffer en memoria (array) o una cadena de texto
como un flujo. Aunque en principio puede parecer muy extraño querer manejar una cadena de este
modo, esto nos permite escribir código para tratar del mismo modo cadenas, bloques de memoria o
archivos. Si, por ejemplo, nos construimos un pequeño intérprete que analice código escrito en un
archivo, ¿por qué no escribirlo basándose en las clases de flujo, de modo que se pueda también
interpretar información escrita directamente por el usuario, y que el programa obtiene de él como
una cadena?. De este modo, ahorraríamos el trabajo de escribir la cadena introducida por el
usuario en un archivo, y obtendríamos una mejora de velocidad, al no tener que pasar el código a
disco. Las clases que proporciona Java para esto son ByteArrayInputStream,
ByteArrayOutputStream y StringBufferInputStream. ByteArrayInputStream, en el Listado E, no
añade ningún nuevo método a InputStream, clase de la que, como FileInputStream, deriva. Eso sí,
el método available está garantizado que devuelve el número de bytes en memoria, y además
existe la particularidad de que reset nos lleva al comienzo del buffer, en lugar de a un marcador
guardado con mark. Como un ByteArrayInputStream se construye sobre un array de bytes,
necesitaremos constructores que tengan en cuenta este hecho: el primero del listado permite
especificar el array del que el flujo obtiene los datos, mientras que el segundo especifica el array,
pero solo una parte del mismo, indicando esto a través de los parámetros c, la posición de
comienzo, y l, el número de bytes a tener en cuenta a partir de la posición de comienzo. La clase
StringBufferStream es idéntica a ésta, salvo que en lugar de obtener la información de un array de
bytes, la obtenemos de una cadena ( un StringBuffer).
public class
java.io.ByteArrayInputStream
extends java.io.InputStream
{
// Constructores
public ByteArrayInputStream(byte buf[]);
public ByteArrayInputStream(byte buf[],
int c, int l );
// ...
}
Listado E: Métodos que ByteArrayInputStream añade a su clase base, InputStream
En cuanto a ByteArrayOutputStream, cuyos nuevos métodos se pueden encontrar en el Listado F,
es un buffer dinámico, que crece conforme le vamos añadiendo datos. Se le puede especificar un
tamaño base, como se puede ver en el primer constructor del listado, o bien dejar que tenga un
tamaño inicial por defecto, utilizando el segundo constructor. La clase ByteArrayOutputStream
ofrece varios métodos nuevos con respecto a OutputStream. Es posible saber el número de bytes
que se han escrito mediante size. Además, es posible obtener un array con los datos del flujo,
mediante toByteArray, o cadenas de texto, mediante las dos versiones de toString en el Listado
F. Esto último puede ser útil, dado que resulta muy común que lo que manejemos en memoria no
sea más que una cadena de texto. Como una comodidad adicional, existe la posibilidad de pasar
toda la información almacenada en la memoria por el flujo a otro flujo de salida, como un archivo,
etc., mediante el método writeTo( flujoSalida).
public class
java.io.ByteArrayOutputStream
extends java.io.OutputStream
{
// Constructores
public ByteArrayOutputStream(int tamanyo);
public ByteArrayOutputStream();
// Métodos
public int size();
public byte[] toByteArray();
public String toString();
public String toString(int hibyte);
public void writeTo(OutputStream os);
}
Listado F: Métodos que ByteArrayOutputStream añade a OutputStream.
Comunicación entre procesos/threads mediante
flujos
Además de las clases vistas hasta ahora, Java proporciona unos flujos especiales para
comunicación entre threads: la ventaja de utilizar este modo de comunicación es que Java se
encargará de todas las tareas de sincronización en el acceso a los datos, de modo que los
procesos lectores y escritores no choquen. Las clases utilizadas para llevar a cabo esta tarea son
PipedOutputStream y PipedInputStream. La idea básica aquí es que tenemos un objeto de cada
clase, y los threads lectores usan el de la clase PipedInputStream, y los procesos escritores el de
la clase PipedOutputStream, a través de los cuales se accede a una misma información: para
ponerlos de acuerdo en que esto es así, hay que conectar el flujo de entrada con el de salida, lo
que se hace mediante código como el que sigue: pipeEntrada.connect( pipeSalida ); o bien
pipeSalida.connect( pipeEntrada ); con lo cuál ambos trabajarán sobre la misma información. Como
se puede ver, el método connect existe para ambas clases, y es el único método que añaden a
sus clases base, que como de costumbre son InputStream y OuputStream. Además, la operación
de poner de acuerdo a ambos flujos también se puede llevar a cabo mediante un constructor que
proporcionan y que permite pasar como parámetro el pipe complementario, lo que hace
innecesario llamar a connect.
Concatenar flujos de entrada con
SequenceInputStream
Java proporciona una clase de utilidad que nos permite manipular varios flujos de lectura como si
fuesen uno solo, concatenándolos uno tras otro. La clase es SequenceInputStream, derivada de
InputStream, y el Listado G es un pequeño programa que concatena a efectos de lectura los
archivos AUTOEXEC.BAT y CONFIG.SYS. El programa es muy similar al del Listado A, solo que
en lugar de tomar la entrada de un archivo, toma la entrada de un SequenceInputStream que
concatena a efectos de lectura dos archivos. Como con las distintas clases vistas hasta ahora, esta
clase define constructores apropiados: en este caso, el constructor utilizado admite como
argumentos dos flujos de entrada (InputStream) cualesquiera. Hay otro constructor que permite
pasar una lista, en lugar de dos, mediante un argumento del tipo Enumerated.
import java.io.*;
public class entrada_salida3 {
static void main( String args[] )
throws IOException
{
int caracter;
int fin_archivo = -1;
// Creamos y abrimos los flujos
FileInputStream autoexec =
new FileInputStream( "c:\\autoexec.bat" );
FileInputStream config =
new FileInputStream( "c:\\config.sys" );
InputStream entrada =
new SequenceInputStream( autoexec, config );
OutputStream salida =
new FileOutputStream( "c:\\copia.aux" );
// Realizamos operaciones de entrada y salida
caracter = entrada.read();
while( caracter != fin_archivo ) {
salida.write( caracter );
caracter = entrada.read();
}
// Cerramos los flujos
entrada.close();
salida.close();
}
};
Listado G: Uso de SequenceInputStream (entrada_salida3.java).
Resumen
Como hemos visto, el concepto de flujo resulta muy potente: a través de él podemos obtener
información o leerla de casi cualquier fuente, incluyendo archivos, memoria, cadenas, o incluso otro
proceso/thread. Si fuera necesario, podríamos definirnos flujos para comunicación vía DDE, o casi
cualquier cosa. Además, hay flujos que actúan sobre otros, por ejemplo para concatenar dos o más
fuentes de información, como SequenceInputStream, y otros que veremos más adelante. En el
próximo artículo estudiaremos los flujos que quedan, todos ellos filtros, así como el manejo
portable del sistema de archivos y el acceso aleatorio a archivos. Además, se expondrán todas las
clases que java.io proporciona para el manejo de errores en operaciones de entrada/salida.
Flujos de filtro
En el artículo anterior se trató ampliamente el uso de flujos para operaciones de lectura y escritura,
basado en las clases InputStream y OutputStream, respectivamente. Java, además de las clases
vistas en el artículo anterior, proporciona unos flujos un tanto especiales, llamados flujos de filtro,
que actúan transformando la información de otro flujo: la jerarquía de clases de filtro se puede ver
en la Tabla A. Las clases de filtro terminan llamando a las funciones del flujo que se les ha
asignado, muchas veces transformando antes la información: por ejemplo, podríamos implementar
un filtro que pase a mayúsculas todo el texto, y que funcionaría sobre flujos asociados a archivos, a
memoria, o a cualquier otra fuente de información. También podríamos tener un filtro que haga una
especie de caché, almacenando la información en memoria intermedia, en lugar de escribirla en el
dispositivo de salida conforme llega: solo cuando hubiera una cantidad apreciable de información la
escribiría mediante una única operación de escritura sobre el flujo "real". Esta posibilidad de
aumentar o modificar la funcionalidad de muchas clases a través de una única clase filtro es lo que
hace que el concepto de flujo de filtro tenga sentido, por su potencia
InputStream
FilterInputStream
BufferedInputStream
DataInputStream
LineNumberInputStream
PushbackInputStream
OutputStream
FilterOutputStream
BufferedOutputStream
DataOutputStream
PrintStream
Tabla A: Jerarquía de clases correspondiente a flujos de filtro
Por lo que respecta a las clases que proporcionan esta capacidad, tenemos dos clases base,
FilterInputStream y FilterOutputStream, derivadas, cómo no, de InputStream y OutputStream. En
realidad estas clases no proporcionan ninguna funcionalidad aparte de aceptar como fuente o
destino de la información un flujo, mientras que las clases vistas hasta ahora trabajaban con
información en archivos, en memoria, en cadenas, etc: por tanto, los métodos que proporcionan
estas clases son exactamente los mismos que los vistos para InputStream y OutputStream en el
artículo anterior.
Filtros de "buffering"
Una posibilidad interesante con ciertos dispositivos de entrada y salida es realizar el menor número
de lecturas y escrituras de grandes bloques de información, en lugar de llevar a cabo muchas
lecturas/escrituras de bloques pequeños: esto es especialmente útil cuando se trabaja con
dispositivos lentos. Java proporciona dos clases de filtro específicas para llevar a cabo esta tarea,
BufferedInputStream y BufferedOutputStream. El Listado A muestras los constructores y métodos
que BufferedInputStream añade/modifica con respecto a InputStream: todos los demás métodos
heredados de InputStream funcionan tal y como se expuso cuando se trató dicha clase.
public BufferedInputStream( InputStream is);
public BufferedInputStream( InputStream is,
int tamanyoBuffer );
Listado A: Nuevos constructores añadidos por BufferedInputStream con respecto a InputStream.
El funcionamiento básico de BufferedInputStream es como sigue: cuando le pedimos un único byte
de información al flujo de filtro, llamando a su método read, éste le pide al flujo "real" un gran
bloque de bytes, guardándolos en un buffer en memoria. La siguiente vez que le pidamos un byte,
en lugar de pedírselo al flujo real, lo lee del buffer, donde previamente guardó la información. Esto
se repite, hasta que llegamos al final del buffer, en cuyo momento vuelve a solicitar un gran bloque
de información al flujo real. Por tanto, lo que para nosotros son múltiples operaciones de lectura, se
reduce a una o muy pocas operaciones de lectura sobre el dispositivos real, a la hora de la verdad.
Con este modo de funcionamiento, lo lógico es que a la hora de construir el filtro especifiquemos el
flujo real sobre el que trabajamos, y el tamaño del buffer de memoria: para este cometido existe el
segundo constructor del Listado A. Si no tenemos claro cuál es el tamaño recomendable del
buffer, podemos simplemente delegar la decisión en la clase, y especificar solo el flujo sobre el que
queremos trabajar a la hora de crear el BufferedInputStream, para lo que utilizaremos el primer
constructor del Listado A. El Listado B muestra el uso de un BufferedInputStream que trabaja
sobre un flujo de archivo: como se puede ver, el código es exactamente igual al que utilizaríamos si
leyéramos del FileInputStream directamente, excepto por la línea en que creamos el flujo que hace
de buffer, y por el hecho de que las operaciones de lectura, etc. las hacemos sobre el buffer, no
sobre el flujo de archivo. Se puede encontrar el código fuente en el archivo entrada_salida4.java.
import java.io.*;
public class entrada_salida4
{
public static void main( String args[] )
throws IOException
{
InputStream archivo =
new FileInputStream( "c:\\autoexec.bat" );
// Creamos un buffer para leer del
// archivo en bloques de 4096 bytes
InputStream entrada =
new BufferedInputStream( archivo, 4096 );
int fin_archivo = -1;
int caracter;
caracter = entrada.read();
while( caracter != fin_archivo ) {
System.out.print( (char)caracter );
caracter = entrada.read();
}
entrada.close();
archivo.close();
}
}
Listado B: Utilización de un buffer para trabajar con un archivo en operaciones de lectura.
Por cierto, System.in, el objeto utilizado para obtener entrada de datos por teclado, pertenece a
esta clase. El Listado C es un pequeño programa que lee información de teclado hasta que se
pulse la tecla INTRO (carácter especial representado por '\n' ).
import java.io.*;
public class entrada_salida5
{
public static void main( String args[] )
throws IOException
{
int fin_linea = '\n';
int numCaracteres = 0;
int caracter;
System.out.println( "Por favor, introduzca un texto," +
" y pulse INTRO: " );
caracter = System.in.read();
while( caracter != fin_linea ) {
numCaracteres++;
caracter = System.in.read();
}
System.out.println();
System.out.println( "Se leyeron " +
numCaracteres +
" caracteres" );
};
}
Listado C: Utilización de System.in, el objeto de la clase BufferedInputStream utilizado para
obtener información a través del teclado.
Por lo que respecta a BufferedOutputStream, proporciona dos nuevos constructores con respecto a
OutputStream, con el mismo cometido que los proporcionados por BufferedInputStream. La
diferencia, como es lógico, es que aquí lo que se hace es escribir sobre el flujo real en bloques, en
lugar de leer.
Otros filtros
Guardar información de modo que sea independiente de la máquina parece una tarea trivial, pero
no lo es tanto: para algunas máquinas un número se representa en memoria de modo distinto al
modo en que se representa en otras, de modo que si escribimos un número tal y como está en
memoria en una, al leer la información en la otra obtendremos un número distinto. Java
proporciona clases que nos permiten leer y escribir los tipos de datos primitivos (enteros,
caracteres, etc.) de modo portable: dado que uno puede desear guardar la información en muchos
lugares distintos, como un archivo, o enviarla vía módem a otra máquina, etc., la solución ideal es
utilizar un flujo que sepa transformar la información para llevar esto a cabo, y que luego la envíe a
cualquiera de estos destinos (para cada uno de los cuáles, evidentemente, habrá una clase de flujo
que sepa escribir en ellos...). La solución, por tanto, será utilizar dos clases de filtro: estas son
DataInputStream, y DataOutputStream, que actuarán sobre un flujo de archivo, de módem, etc.
Estas clases, como se puede ver en la Tabla A, derivan de FilterInputStream y FilterOutputStream,
respectivamente. El Listado D muestra los nuevos métodos de DataInputStream: nótese que para
cada tipo primitivo, byte, char, boolean, hay un método de lectura correspondiente, readByte,
readChar, readBoolean, etc. En cuanto a DataOutputStream, la clase para lectura, es similar, solo
que ahora los métodos son de escritura: writeByte, writeChar, etc.
// Constructores
public DataInputStream(InputStream in);
// Métodos
public final boolean readBoolean();
public final byte readByte();
public final char readChar();
public final double readDouble();
public final float readFloat();
public final void readFully(byte b[]);
public final void readFully(byte b[],
int off, int in);
public final int readInt();
public final String readLine();
public final long readLong();
public final short readShort();
public final int readUnsignedByte();
public final int readUnsignedShort();
public final String readUTF();
public final static String readUTF(DataInput in);
public final int skipBytes(int n);
Listado D: Nuevos métodos de DataInputStream, con respecto a InputStream.
Aparte de estas clases, también existe una clase especial, PrintStream, derivada de OutputStream,
que proporciona varias versiones de un nuevo método, print, cada una de las cuáles permite
imprimir enteros, caracteres, etc. de forma legible. También proporciona varias versiones de
println, que imprime la información que le pasemos, cambiando luego de línea. Ya hemos utilizado
esta clase en varios de nuestros programas: de hecho, el objeto System.out, que ya hemos
utilizado para mostrar información en pantalla en el Listado B y otros pertenece a esta clase. Hay
dos filtros de entrada adicionales, LineNumberInputStream y PushbackInputStream, derivados de
FilterInputStream. El primero es simplemente un filtro que lleva la cuenta del número de línea en
que estamos, y que en consecuencia añade un método para averiguar cuál es, getLineNumber, y
otro para que podamos cambiarlo nosotros, setLineNumber: esta clase puede ser útil para
imprimir listados de programas, por ejemplo. En cuanto a PushbackInputStream, permite dar
marcha atrás en la lectura, con lo que podemos "releer" el último byte leído, posiblemente
modificándolo: esto es útil para aplicaciones que trabajen con información delimitada por algún
carácter especial, y algunas más. La marcha atrás se implementa por medio de unread( caracter
), de modo que la siguiente llamada al método read nos devolverá caracter, que puede ser
exactamente el carácter que leímos, o el que nosotros queramos.
El sistema de archivos en Java
Los creadores de java.io decidieron separar las operaciones de lectura y escritura de archivos de la
manipulación del sistema de archivos: renombrarlos, eliminarlos, etc., motivo por el que hay clases
separadas para cada propósito. Las clases de lectura y escritura ya las hemos visto,
FileInputStream y FileOutputStream. Para los demás menesteres se utilizan las clases File y
FileDescriptor. La clase FileDescriptor proporciona acceso a información mantenida por el sistema
operativo, solo que no tenemos acceso a ella. ¿Para qué sirve, pues?: para bien poco, de hecho
una aplicación no debería crear objetos de esta clase, sino obtenerlos a través de métodos como
getFD de la clase FileInputStream, y utilizarlos como argumento en el contructor de File. La única
operación que se puede hacer sobre ellos es averiguar si representan un archivo válido, con valid.
En realidad, se trata de una clase que raramente se utilizará.
public class java.io.File
extends java.lang.Object
{
// Constructores
public File(File dir, String nombre);
public File(String path);
public File(String path, String nombre);
// Métodos
public boolean isDirectory();
public boolean isFile();
public boolean exists();
public boolean delete();
public boolean renameTo(File dest);
public boolean canRead();
public boolean canWrite();
public String getAbsolutePath();
public String getName();
public String getParent();
public String getPath();
public boolean isAbsolute();
public long lastModified();
public long length();
public String toString();
public int hashCode();
public boolean equals(Object obj);
// Métodos para directorios
public boolean mkdir();
public boolean mkdirs();
public String[] list();
public String[] list(FilenameFilter filtro);
// Campos
public final static String pathSeparator;
public final static char pathSeparatorChar;
public final static String separator;
public final static char separatorChar;
}
Listado E: Métodos y constructores de la clase File.
La clase File sí es más útil: el Listado E muestra sus métodos. Un detalle importante a tener en
cuenta es que esta clase no solo representa archivos, sino también directorios: para averiguar con
cuál de las dos cosas estamos trabajando tenemos los métodos isFile e isDirectory,
respectivamente. Para asegurarse de que realmente exista el archivo o directorio disponemos de
exists. Podemos averiguar si tenemos derechos de lectura o escritura con canRead y canWrite,
respectivamente, averiguar el nombre del archivo o directorio con getName, la fecha de última
modificación con lastModified, etc. Para trabajar con nombres de archivo/directorio disponemos de
getName, que devuelve solo el nombre del archivo (o directorio), getParent, que devuelve el
directorio en que se encuentra, y getAbsolutePath, que devuelve la combinación de nombre más
directorio. Los métodos mkDir, que crean un directorio con el nombre que se le indicó al objeto File
al crearlo, mkDirs (como mkDir, pero crea también los directorios padre si no existen) y list, que
devuelve todos los archivos en el directorio, o solo los especificados por una cadena de filtro
(digamos "*.exe"), según la versión del método que utilicemos, son solo para utilizarlos cuando el
objeto File con que estemos trabajando represente a un directorio. Un último detalle: al final de
Listado E se pueden encontrar varios campos declarados como final, es decir, constantes. Estos
son utilizados para conseguir que los nombres de archivos/directorios sean portables entre
sistemas: en efecto, lo que bajo Windows 95 se escribe como "C:\DOS\KEYB.COM;C:\DOS" puede
ser "C:/DOS/KEYB.COM&C:/DOS" en otro sistema operativo. Las constantes separator y
separatorChar corresponden al carácter utilizado para separar directorio de subdirectorio ("\" bajo
Windows 95) mientras que pathSeparator y pathSeparatorChar corresponden al carácter utilizado
para separar dos paths (";" bajo Windows95). En resumen, el código listaArchivos =
"C:\\DOS\\KEYB.COM;C:\\DOS"; no es portable, mientras que listaArchivos = "C:" +
File.separator + "DOS" +
File.separator + "KEYB.COM" +
File.pathSeparator + "C:" +
File.pathSeparator + "DOS"; sí es portable. En cuanto a la diferencia entre separator y
separatorChar, o pathSeparator y pathSeparatorChar uno devuelve una cadena, y el otro un
carácter, cosas muy distintas para Java. Para un pequeño programa de ejemplo sobre el uso de la
clase Fiile, podéis consultar el archivo entrada_salida6.java, incluido en el disco.
Archivos de acceso aleatorio
Acceder directamente a la posición donde se encuentra la información (acceso aleatorio) permite
mucha mayor rapidez en la recuperación de la información que acceder teniendo que leer toda la
información hasta llegar al lugar donde se encuentra lo que necesitamos (acceso secuencial), por
lo que no puede haber una librería de entrada/salida que no proporcione esta posibilidad: java.io
incluye la clase RandomAccessFile, cuyos métodos y constructores se muestran en el Listado F.
public class java.io.RandomAccessFile
extends java.lang.Object
implements java.io.DataOutput,
java.io.DataInput
{
// Constructorrs
public RandomAccessFile(File archivo, String modo);
public RandomAccessFile(String nombre, String modo);
// Métodos
public void seek(long pos);
public long length();
public long getFilePointer();
public int skipBytes(int n);
public void close();
public final FileDescriptor getFD();
public int read();
public int read(byte b[]);
public int read(byte b[], int off, int len);
public void write(byte b[]);
public void write(byte b[], int off, int len);
public void write(int b);
public final boolean readBoolean();
public final byte readByte();
public final char readChar();
public final double readDouble();
public final float readFloat();
public final void readFully(byte b[]);
public final void readFully(byte b[],
int off, int len);
public final int readInt();
public final String readLine();
public final long readLong();
public final short readShort();
public final int readUnsignedByte();
public final int readUnsignedShort();
public final String readUTF();
public final void writeBoolean(boolean v);
public final void writeByte(int v);
public final void writeBytes(String s);
public final void writeChar(int v);
public final void writeChars(String s);
public final void writeDouble(double v);
public final void writeFloat(float v);
public final void writeInt(int v);
public final void writeLong(long v);
public final void writeShort(int v);
public final void writeUTF(String str);
}
Listado F: Métodos y constructores de la clase RandomAccessFile.
Como se puede ver, se puede indicar el archivo sobre el que vamos a trabajar basándonos en un
objeto File ya existente (primera versión del constructor en el Listado F), o especificando
directamente el nombre del archivo (segunda versión del constructor). Además, es posible indicar
si deseamos abrir el archivo para lectura, o bien para lectura y escritura, lo que se hace pasando
como segundo argumento de ambos constructores la cadena "r" o "rw", respectivamente. Lo que
realmente distingue el acceso aleatorio es la posibilidad de movernos a cualquier posición dentro
del archivo: para ello utilizamos el método seek, que nos lleva a la posición que especifiquemos,
length, que nos devuelve la longitud del archivo, lo que nos permite saber hasta dónde podemos
llegar moviéndonos por él, y getFilePointer, que nos devuelve la posición donde nos hallamos en
un momento dado. Como conveniencia contamos también con el método skipBytes, que nos
permite saltarnos un número determinado de bytes desde la posición donde nos hallemos. En
cuanto al resto de los métodos, son los que se podrían esperar: las diversas versiones de read y
write permiten leer y escribir uno o más bytes, y funcionan del mismo modo que lo hacían para las
clases InputStream y OutputStream. Además, también hay un método close para cerrar el archivo
cuando terminemos de trabajar con él. También hay un gran número de métodos con nombres
como readDouble, readInt, writeDouble, writeInt, etc., que sirven para leer y escribir de forma
portable los diversos tipos de datos predefinidos. Recuérdese que las clases InputDataStream y
OutputDataStream tenían métodos con el mismo nombre y el mismo propósito. Este es un ejemplo
de uso acertado de las clases de interface: en este caso existen dos clases, InputData y
OutputData, que son simplemente clases de que no proporcionan una implementación, sino
simplemente un protocolo común, en este caso para leer/escribir tipos primitivos portablemente.
Tanto las clases de InputDataStream y OutputDataStream como la clase RandomAccessFile
implementan dichos interfaces, de modo que tenemos un protocolo con los mismos métodos para
las mismas tareas: para una discusión sobre las clases de interface se puede consultar el número
de Febrero. Esta filosofía está muy presente a lo largo de toda la librería estándar de Java. El
archivo entrada_salida7.java contiene un pequeño programa que utiliza los métodos más
importantes de la clase, y que no reproducimos aquí por falta de espacio: conviene fijarse
especialmente en el uso de seek.
Tratamiento de errores: la clase IOException
El paquete java.io proporciona una clase base para todas las excepciones debidas a errores en las
operaciones de entrada/salida, IOException. Para permitir obtener mayor información sobre el error
concreto que se produzca existen varias clases derivadas de ésta: FileNotFoundException indica
que no se encontró el archivo especificado, EOFException indica que hemos llegado al final de un
archivo inesperadamente, seguramente porque intentamos seguir leyendo a pesar de que llegamos
al final. La clase InterruptedIOException indica que se ha interrumpido una operación de
entrada/salida, y por último UTFDataFormatException indica que se ha leído en una
DataInputStream una cadena en formato UTF en mal estado (en el Listado D se puede ver que
existe un método readUTF que lee cadenas almacenadas en este formato).
En el tintero
Queda una clase más o menos importante por tratar dentro de java.io (al menos, por lo que
respecta a la versión 1.0.2 del kit de desarrollo de Java), StreamTokenizer, cuya funcionalidad solo
expondremos por encima. Básicamente, esta clase es capaz de dividir el contenido de cualquier
Stream de lectura (InputStream y clases derivadas) en palabras, saltándose lo que nosotros
indiquemos que son comentarios, e indicándonos para cada tipo de palabra si se trata de un
número, una palabra corriente, si estamos al final de una línea o del archivo: si no se diera ninguna
de estas circunstancias, se trata de un carácter especial suelto. Esta clase puede servir de
pequeña base para un pequeño analizador de fórmulas, etc. Esta pequeña introducción puede
bastar para hacerse una idea de la función de esta clase. Además de las clases vistas hasta ahora,
las nuevas versiones del kit de desarrollo de Java (posteriores a la 1.0.2) irán añadiendo más
clases, que lógicamente se ceñirán a la estructura ya existente de flujos y difícilmente alterarán la
funcionalidad de las clases estudiadas. Como siempre, para cualquier comentario sobre este o
cualquier otro de los artículos de la serie, o sobre Java en general, podéis enviar correo electrónico
al autor a [email protected].
Lectura por teclado:
/* 1) */
/* 2) */
/* 3) */
/* 4) */
/* 5) */
import java.io.*;
public class LectTeclado
{
public static void main(String Arg[ ]) throws IOException
{
/* 6) */
BufferedReader in = new BufferedReader(new
InputStreamReader(System.in));
/* 7) */
int num;
/* 8) */
System.out.print("Ingrese numero : ");
/* 9) */
num = Integer.parseInt(in.readLine( ));
/* 10) */
System.out.println("Ud ingreso el numero : " + num );
/* 11) */
}
/* 12) */ }
1) Se invoca a la librería de entrada y salida io (Input,Output), para usar en nuestro
programa todas sus clases disponibles.
import : Indica que se tienen importar (incluir) cierta librería.
java.io : Acceso a la librería io.
java.io.* : Selecciona todas las clases disponibles.
4) Declaración del programa principal con opción de control de errores.
throws IOException : Indica que cualquier error de entrada o salida de datos, será
manejado en forma interna (automática) por el programa.
6) Se crean las instancias necesarias para crear un objeto que permita manejar la
lectura de datos por teclado.
BufferedReader : Clase perteneciente a la librería io que crea un buffer de entrada
donde se almacenarán los carácteres ingresados por teclado.
in : Variable de tipo BufferedReader.
7) Se declara la variable num de tipo entero (int).
8) Se llama al método print para escribir un mensaje en pantalla dejando el cursor
inmediatamente a continuación del mensaje.
9) Se lee el número, asignando el valor a la variable num.
in.readline : Método que retorna el "string" leído por teclado.
Integer.parseInt : Método que convierte un string (cadena de caracteres) en un dato
numérico de tipo int.
Integer : Clase estándar que no necesita ser instanciada (está disponible por defecto).
10) Se llama al método println para escribir un mensaje en pantalla que consta de
una parte estática y otra variable.
El método println y print soportan varios objetos concatenados mediante el operador
+ , logrando imprimir cadenas de carácteres y variables numéricas.