Sesión 8

En esta sesión practicaremos algunos conceptos básicos de Entrada/Salida, como la lectura y escritura básica con ficheros, y el uso de ficheros de propiedades, y entrada/salida estándar. Antes de comenzar, lee la introducción del punto 2.3 (Entrada/Salida), y los apartados 2.3.1 (Flujos de Entrada/Salida), 2.3.2 (Entrada, salida y salida de error estándar) y 2.3.3 (Acceso a ficheros) del tema 2 de teoría.

1. En este primer ejercicio practicaremos la lectura y escritura básica con ficheros, utilizando las dos posibles alternativas: Streams y Readers/Writers:

  1. Echa un vistazo a la clase Ej4.java 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 Ej4 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.
  2. 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).
    1. Primero obtendremos el flujo de entrada para leer del fichero. Utilizaremos un objeto de tipo FileInputStream, que es el stream preparado para leer de ficheros:
      public void leeEscribeStream()
      {
      	FileInputStream in = new FileInputStream("entrada.dat");
      }
    2. Después obtendremos el flujo de salida, para escribir en el fichero destino. Emplearemos un objeto de tipo FileOutputStream, que es el stream preparado para volcar datos a ficheros:
      public void leeEscribeStream()
      {
      	FileInputStream in = new FileInputStream("entrada.dat");
      	FileOutputStream out = new FileOutputStream("salidaStream.dat");
      }
    3. El siguiente paso es leer el contenido de la entrada, e irlo volcando en la salida. Para leer datos de la entrada emplearemos el método read() de FileInputStream, que irá leyendo caracteres (transformados en enteros). Para escribir, utilizaremos el método write() de FileOutputStream, que vuelca esos mismos enteros que leemos:
      public void leeEscribeStream()
      {
      	FileInputStream in = new FileInputStream("entrada.dat");
      	FileOutputStream out = new FileOutputStream("salidaStream.dat");
      	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?

    4. Finalmente, lo que nos queda es cerrar tanto el flujo de entrada como el de salida:
      public void leeEscribeStream()
      {
      	...
      	in.close();
      	out.close();
      }
    5. Compila el programa. Te dará errores porque se deben capturar ciertas excepciones cuando se trabaja con métodos de entrada salida en fichero (FileNotFoundException e IOException, concretamente). 
      public void leeEscribeStream()
      {
      	try
      	{
      		...
      	} catch (FileNotFoundException e) {
      		Sytem.err.println ("Fichero no encontrado");
      	} catch (IOException e2) {
      		System.err.println ("Error al acceder a los ficheros");
      	}
      }

      Captúralas y prueba el resultado.

    6. Al ejercicio le falta algo, porque si recuerdas, aparte de leer y volcar el contenido del fichero, debemos añadir a la salida como cabecera el contenido del campo cabecera.

      Observa en la API que la clase FileOutputStream no tiene métodos para escribir directamente una cadena a fichero. Lo que vamos a hacer es convertir la cadena a un array de bytes, y luego utilizar el método write(byte[ ] b) para volcarla. Todo esto lo haremos justo antes de empezar a leer el fichero de entrada, y volcar su contenido:
      public void leeEscribeStream()
      {
      	try
      	{
      		FileInputStream in = 
      			new FileInputStream("entrada.dat");
      		FileOutputStream out = 
      			new FileOutputStream("salidaStream.dat");
      
      		byte[] b = cabecera.getBytes();
      		out.write(b);
      
      		int c;
      		while ((c = in.read()) != -1)
      		
      		...
      }

      Prueba el método ya completo, y comprueba que el fichero de salida (salidaStream.dat) deja algo como:

      # 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
  3. 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).
    1. Igual que en el método anterior, primero obtendremos las variables para leer de la entrada y escribir en la salida. Para leer podríamos utilizar la clase FileReader, pero en su lugar vamos a utilizar la clase BufferedReader que nos va a permitir leer líneas enteras del fichero, en lugar de leer carácter a carácter. Para escribir, vamos a utilizar la clase PrintWriter, que también nos permitirá escribir líneas enteras en la salida.
      public void leeEscribeWriter()
      {
      	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.

    2. El siguiente paso es leer el contenido de la entrada, e irlo volcando en la salida. Para leer datos de la entrada emplearemos el método readLine() de BufferedReader, que irá leyendo líneas enteras del fichero. Para escribir, utilizaremos el método println() de PrintWriter, que vuelca esas mismas líneas que leemos:
      public void leeEscribeWriter()
      {
      	BufferedReader br = new ...;
      	PrintWriter pw = new ...;
      
      	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?

    3. Finalmente, lo que nos queda es cerrar tanto el flujo de entrada como el de salida:
      public void leeEscribeWriter()
      {
      	...
      	br.close();
      	pw.close();
      }
    4. Compila el programa. Te dará errores porque se deben capturar las mismas excepciones que antes (FileNotFoundException e IOException). 
      public void leeEscribeWriter()
      {
      	try
      	{
      		...
      	} catch (FileNotFoundException e) {
      		Sytem.err.println ("Fichero no encontrado");
      	} catch (IOException e2) {
      		System.err.println ("Error al acceder a los ficheros");
      	}
      }

      Captúralas y prueba el resultado.

    5. Para completar el ejercicio, nos falta añadir la cabecera antes de volcar el fichero. Observa que con PrintWriter no hace falta que convirtamos la cadena a bytes y luego la escribamos, podemos escribir directamente la cadena, antes de empezar a leer el fichero:
      public void leeEscribeWriter()
      {
      	try
      	{
      		BufferedReader br = ...;
      		PrintWriter pw = ...;
      		pw.print(cabecera);
      
      		String linea = "";
      		while ((linea = br.readLine()) != null)
      
      		...
      }

      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.

    6. 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.

2. En este segundo ejercicio practicaremos el uso de ficheros de propiedades, y el uso de la entrada y salida estándares.

  1. Echa un vistazo a la clase Ej5.java 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.
  2. 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.
    1. 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:
      public Ej5()
      {
      	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.
    2. 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:
      public Ej5()
      {
      	Properties p = new Properties();
      	p.load(new FileInputStream("prop.txt"));
      
      	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).

    3. 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:

      public Ej5()
      {
      	Properties p = new Properties();
      	p.load(new FileInputStream("prop.txt"));
      
      	Enumeration en = p.propertyNames();
      	BufferedReader in = 
      	  new BufferedReader(new InputStreamReader(System.in));		
      	
      	while (en.hasMoreElements())
      	{
      		...
      	}	
      }

      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:

      public Ej5()
      {
      	...
      	
      	while (en.hasMoreElements())
      	{
      		String prop = (String)(en.nextElement());
      		System.out.println("Introduzca valor para propiedad " + prop);
      		String valor = in.readLine();
      		p.setProperty(prop, valor);
      	}	
      }
    4. Finalmente, cerramos el buffer de entrada, y guardamos las propiedades en el fichero.

      public Ej5()
      {
      	...
      	in.close();
      	p.store(new FileOutputStream("prop.txt"), "Cabecera del fichero");
      }
    5. Compilad y ejecutad el programa. Para que os compile deberéis capturar las excepciones que se os indique en los errores de compilación:

      public Ej5()
      {
      	try
      	{
      		...
      	} catch(...) {}
      }

PARA ENTREGAR