4.1.Lectura y escritura básicas
En este primer ejercicio practicaremos la lectura y escritura básica con ficheros, utilizando las dos posibles alternativas: Streams y Readers/Writers.
Echa un vistazo a la clase sesion04.Ej1 que se proporciona en la plantilla de la sesión. Verás que hay un constructor vacío, y un campo llamado cabecera, que contiene una cadena de texto. También hay dos métodos vacíos, leeEscribeStream y leeEscribeWriter, y un método main que crea un objeto de tipo Ej1 y llama a estos dos métodos. Lo que vamos a hacer es rellenar esos dos métodos de la forma que se nos indica a continuación.
El primero de los métodos leeEscribeStream va a leer un fichero de entrada (el fichero entrada.dat que se os proporciona en la plantilla), y lo va a volcar a un fichero de salida (fichero salidaStream.dat), pero añadiéndole la cadena cabecera como encabezado del fichero. Para hacer todo eso empleará flujos de tipo stream (InputStream para leer, OutputStream para escribir, o cualquier subclase derivada de éstas).
FileInputStream in = new FileInputStream("entrada.dat");NOTA IMPORTANTE: vigilad dónde ponéis el fichero entrada.dat, porque puede que no lo encuentre. Una buena idea para que esta línea de código os funcione es ponerlo en la carpeta raíz del proyecto, y no dentro del paquete sesion08. De todas formas, podéis ponerlo en cualquier parte, siempre que después sepáis cómo encontrarlo desde Java.
FileOutputStream out = new FileOutputStream("salidaStream.dat");
Aquí tendremos que tener las mismas consideraciones que con el fichero de entrada, en cuanto a cómo localizarlo. En este caso no es tan importante, porque el fichero lo creará de todas formas, en un lugar u otro, pero debemos saber dónde lo va a crear. Con una línea como ésta, lo creará en la carpeta raíz del proyecto también.
int c; while ((c = in.read()) != -1) { out.write(c); }
Echa un vistazo a la documentación sobre el método read. ¿Por qué se compara el dato que se lee con -1?
in.close(); out.close();
byte[] b = cabecera.getBytes(); out.write(b);
# Esto es la cabecera del fichero que hay que introducir Hola, este es el texto del fichero de entrada que debería copiarse en el fichero de salida
El segundo método, leeEscribeWriter, leerá el mismo fichero de entrada (entrada.dat), y lo volcará a otro fichero de salida diferente (salidaWriter.dat), empleando flujos de tipo Reader y Writer (como FileReader o FileWriter, o cualquier otro subtipo).
BufferedReader br = new BufferedReader(new FileReader("entrada.dat")); PrintWriter pw = new PrintWriter(new FileWriter("salidaWriter.dat"));
Observad que para construir tanto el BufferedReader como el PritWriter nos valemos de un objeto FileReader o FileWriter, respectivamente. Lo que hacemos es simplemente crear un buffer de entrada (BufferedReader) o de salida (PrintWriter) sobre el FileReader o el FileWriter para poder acumular cadenas de texto enteras antes de leerlas o escribirlas. Deberemos tener las mismas consideraciones que con el método anterior sobre dónde poner los ficheros para que el programa los encuentre.
String linea = ""; while ((linea = br.readLine()) != null) { pw.println(linea); }
El uso de PrintWriter permite formatear la salida de la misma
forma que si la estuviésemos sacando por pantalla, puesto que tiene los
mismos métodos que el campo System.out (métodos println, print,
etc).
Echa un vistazo a la documentación sobre el método readLinea. ¿Por
qué se compara el dato que se lee con null?
br.close(); pw.close();
pw.print(cabecera);
Prueba el método ya completo, y comprueba que el fichero de salida (salidaWriter.dat) deja el mismo resultado que con el método anterior.
NOTA: observa la API de la clase PrintWriter, y verás que tiene constructores que permiten crear este tipo de objetos a partir de Writers (como hemos hecho aquí) como a partir de OuputStreams (como habríamos hecho en el paso 2), con lo que podemos utilizar esta clase para dar formato a la salida de un fichero en cualquiera de los casos.
4.2. Trabajar con propiedades
En este segundo ejercicio practicaremos el uso de ficheros de propiedades, y el uso de la entrada y salida estándares. Echa un vistazo a la clase sesion04.Ej2 que se proporciona en la plantilla de la sesión. Sólo tiene un constructor vacío, y un método main que le llama. Vamos a completar el constructor de la forma que veremos a continuación.
Lo que vamos a hacer en el constructor es leer un fichero de propiedades (el fichero prop.txt que se proporciona en la plantilla), y luego pedirle al usuario que, por teclado, indique qué valores quiere que tengan las propiedades. Una vez establecidos los valores, volveremos a guardar el fichero de propiedades.
Lo primero que vamos a hacer es leer el fichero de propiedades. Para ello utilizaremos un objeto java.util.Properties, lo crearemos y llamaremos a su método load() para cargar las propiedades del fichero prop.txt:
Properties p = new Properties(); p.load(new FileInputStream("prop.txt"));
Observa que para cargar las propiedades, al método load le debemos pasar un InputStream desde el que leerlas. En este caso le pasamos un FileInputStream con el fichero prop.txt.
Ahora ya tenemos en el objeto p todas las propiedades del fichero. Vamos a irlas recorriendo una a una, e indicando al usuario que teclee su valor. Para recorrer las propiedades obtendremos un Enumeration con sus nombres, y luego lo iremos recorriendo, y sacándolo por pantalla:
Enumeration en = p.propertyNames(); while (en.hasMoreElements()) { String prop = (String)(en.nextElement()); System.out.println("Introduzca valor para propiedad " + prop); }
Observa el orden en que van mostrándose las propiedades. ¿Es el mismo que el que hay en el fichero? ¿A qué crees que puede deberse? (AYUDA: cuando nosotros enumeramos una serie de características, no tenemos que seguir un orden necesariamente. Del mismo modo, cuando introducimos valores en una tabla hash, el orden en que se guardan no es el mismo que el orden en que los introducimos).
Lo que hacemos con este bucle es sólo recorrer los nombres de las propiedades y sacarlos por pantalla. Nos falta hacer que el usuario teclee los valores correspondientes. Para ello utilizaremos un objeto de tipo BufferedReader, que en este caso leerá líneas de texto que el usuario entre desde teclado:
... Enumeration en = p.propertyNames(); BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); ...
observad que construimos el BufferedReader para leer de un InputStream
(no de un Reader). Esto lo podemos hacer si nos ayudamos de la
"clase puente" InputStreamReader, que transforma un tipo de
lector en otro.
Lo que nos queda por hacer es pedirle al usuario que, para cada nombre de
propiedad, introduzca su valor, y luego asignarlo a la propiedad
correspondiente:
... while (en.hasMoreElements()) { String prop = (String)(en.nextElement()); System.out.println("Introduzca valor para propiedad " + prop); String valor = in.readLine(); p.setProperty(prop, valor); }
Finalmente, cerramos el buffer de entrada, y guardamos las propiedades en el fichero.
in.close(); p.store(new FileOutputStream("prop.txt"), "Cabecera del fichero");
Compilad y ejecutad el programa. Para que os compile deberéis capturar las excepciones que se os indique en los errores de compilación.
Añadid el código necesario al ejercicio para que, además de poder modificar los valores de las propiedades, podamos añadir por teclado nuevas propiedades al fichero, y guardarlas con las existentes.
4.3. Leyendo matrices
Vamos a practicar la lectura de tokens de un fichero, y su almacenamiento para realizar alguna operación. Echa un vistazo a la clase sesion04.Ej3 que se proporciona en la plantilla de la sesión. Verás que hay un constructor vacío, y un método main que le llama. Rellenaremos el constructor como se indica en los siguientes pasos.
Lo que vamos a hacer es que el constructor acceda a un fichero (fichero matriz.txt de la plantilla) que tiene una matriz m x n. Dicho fichero tiene la siguiente estructura:
; Comentario de cabecera m n A11 A12 A13... A21 A22 A23... ...
donde m son las filas, n las columnas, y después aparece la
matriz puesta por filas, con un espacio en blanco entre cada elemento.
El ejercicio leerá la matriz (utilizando un StreamTokenizer sobre el
fichero), construirá una matriz (array) con los datos leídos, después elevará
al cuadrado cada componente, y volcará el resultado en un fichero de salida.
Primero obtendremos el flujo de entrada para leer del fichero, y el StreamTokenizer:
StreamTokenizer st = new StreamTokenizer(new FileReader("matriz.txt"));
Después establecemos qué caracteres van a identificar las líneas de comentarios. En este caso, los comentarios se identifican por punto y coma:
st.commentChar(';');
Después del comentario irán el número de filas y de columnas. Utilizamos el método nextToken del tokenizer para leerlos, y luego accedemos al campo nval para obtener qué valor numérico se ha leído en cada caso:
int filas, columnas; st.nextToken(); filas = (int)(st.nval); // Filas st.nextToken(); columnas = (int)(st.nval); // Columnas
NOTA: asumimos que el fichero va a tener un formato correcto, y no tenemos
que controlar que haya elementos no deseados por enmedio.
¿Qué se habría leído en primer lugar si no hubiésemos identificado la
primera línea como comentario? ¿Dónde podríamos haber consultado ese valor
leído?
Lo siguiente es ir leyendo los elementos de la matriz. Construimos un array de enteros de filas x columnas, y luego lo vamos rellenando con los valores que nos dé el StreamTokenizer:
int[][] matriz = new int[filas][columnas]; int t; for (int i = 0; i < filas; i++) for (int j = 0; j < columnas; j++) { t = st.nextToken(); if (t != StreamTokenizer.TT_EOF) { matriz[i][j] = (int)(st.nval); } }
Por último, calculamos el cuadrado de cada elemento de la matriz (utilizamos el método pow de la clase Math), y guardamos la matriz resultado en otro fichero de salida (matrizSal.txt), con el mismo formato que el de entrada. Utiliza un objeto PrintWriter para facilitar el volcado de la matriz al fichero.
Compila y ejecuta el programa (captura las excepciones adecuadas para que te compile bien). Comprueba que el fichero de salida genera el resultado adecuado:
; Matriz resultado 3 3 1 4 9 16 25 36 49 64 81
Prueba también a pasarle este mismo fichero como entrada al programa, y que genere otro fichero de salida diferente.
4.4. Guardar datos personales
En este ejercicio practicaremos cómo utilizar los ficheros para almacenar y
leer objetos complejos. Hasta ahora sólo hemos trabajado con enteros o cadenas,
y para leerlos basta con leer un stream de bytes, o utilizar un tokenizer
y procesar el fichero de la forma que nos convenga.
Imaginemos que trabajamos con un objeto complejo que encapsula diferentes tipos
de datos (enteros, cadenas, vectores, etc). A la hora de guardar este elemento
en fichero, se nos plantea el problema de cómo representar su información para
volcarla. De la misma forma, a la hora de leerlo, también debemos saber cómo
extraer y recomponer la información del objeto. Veremos que hay clases Java que
hacen todo este trabajo mucho más sencillo.
En la plantilla tenemos la clase datos.Persona que permite almacenar datos personales para una serie de personas. Por otro lado, tenemos en la plantilla la clase io.LeeGuardaPersona. Tiene dos métodos leePersonas y guardaPersonas que deberemos implementar:
ArrayList al = new ArrayList(); try { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fichero)); while (true) { Persona p = (Persona)(ois.readObject()); al.add(p); } } catch (Exception e) { } return al;Te parecerá raro utilizar un bucle infinito para leer el fichero. El motivo es sencillo. Cuando trabajamos con ObjectInputStreams no hay forma de saber cuándo acaba el fichero, porque leemos objetos complejos enteros, hasta que ya no hay más. En ese momento, se provoca una excepción, y el bucle terminará cuando dicha excepción salte, lo que indicará el fin de fichero. Después de eso, devolvemos la lista con los objetos que haya guardados y listo.
¿Qué pasaría si Persona no implementase la interfaz Serializable? ¿Qué excepción saltaría al ejecutar?