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 pagullo@ctv.es. 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.