Leer y escribir ficheros de texto con java 8

De ChuWiki
Saltar a: navegación, buscar

Con Java 8, aparecen nuevas clases, sintaxis y formas de hacer las cosas que permiten, entre otras, leer y escribir ficheros de texto de forma más sencilla que la tradicional de Java.

Leer fichero de texto

Abrir el fichero

Tenemos varias formas de hacerlo. Una muy al estilo de java 8 consiste en usar la clase Files y su método lines(), de esta manera

Path path = Paths.get("fichero.txt");
Stream<String> stream = Files.lines();


Hemos creado una instancia de Path con el nombre de fichero usando el método Paths.get(). Luego con la clase Files, obtenemos un 'stream' o flujo de líneas (de String). Estas líneas no se cargan todas en memoria, sino que se irán leyendo del fichero según las vayamos necesitando.

Leer del Stream

La clase Stream tiene muchos métodos que nos permiten hacer cosas con esas líneas, como filtrarlas, crear nuevos flujos o 'stream' modificados, etc, etc. Sin embargo, nos centramos sólo en leer y para ello nos viene bien el método forEach()

stream.forEach(System.out::println);


forEach() admite como parámetro una clase que implemente la interface Consumer. Sin embargo, aprovechando la sintaxis de Lambdas de java 8, podemos "crear" sobre la marcha una clase anónima que implemente esa interface.

Una forma es la que acabamos de usar. Basta con referenciar a un método de cualquier clase o intancia que tenga un método que admita un String como parámetro. Por ejemplo, el método System.out.println(). ¿cómo lo referenciamos?. Ponemos el nombre de la clase (si el método es estático) o de una instancia de esa clase (System.out en nuestro ejemplo), dos veces dos puntos :: y el nombre del método sin paréntesis ni nada (println en nuestro ejemplo.

Otra opción es usar una expresión Lambda típica de java 8

stream.forEach((s)->System.out.println(s));

donde ponemos entre paréntesis el nombre del parámetro que vamos a recibir que será de tipo String puesto que el Stream es de tipo String, una flecha -> y lo que queremos hacer con ese String. Debemos poner llaves si tenemos más de una linea de código. En este caso, sólo tenemos una línea para sacar por pantalla la línea del fichero.

Tratamiento de excepciones. try-with-resources

Tanto la apertura del fichero como la lectura, pueden lanzar excepciones que debemos tratar. En java anterior a 8, debemos montar un try-catch de la siguiente forma

BufferedReader br = null;

try {
   // Abrir fichero y leer del fichero en un bucle
} catch (Exception e) {
   e.printStackTrace();
} finally {
   if (null != br) {
       try {
           br.close();
       } catch (Exception e2) {
           e2.printStackTrace();
       }
   }

}

Es bastante liado. Hay que poner un try-catch y para asegurarnos que el fichero se cierra tanto si salta excepción como si no, se pone el cierre en un finally. Para que la variable BufferedReader esté accesible en el finally, debemos declararla fuera del try-catch, pero el fichero debe abrirse dentro del try, por si salta excepción. Esto hace que en el finally la variable BufferedReader pueda ser null y necesitamos comprobarlo. Y para liarlo aún más, la llamada a close() también lanza una excepción, por lo que necesitamos anidar un segundo bloque try-catch.

En java 8 existe una forma más sencilla de hacer todo esto, conocida como 'try-with-resources'. Es un try que admite entre paréntesis la apertura de un recurso (fichero, socket, ...) que implemente la interfaz AutoCloseable. Detrás del try, entre paréntesis, se pone la apertura del recurso y el mismo try se encargará de cerrarlo cuando termine, tanto si salta excepción como si no, como haría el finally. Nuestro código quedará entonces, de forma completa, así

Path path = Paths.get("fichero.txt");

try (Stream<String> stream = Files.lines(path)) {
   stream.forEach(System.out::println);
} catch (IOException e) {
   e.printStackTrace();
}

Stream implementa AutoCloseable, por lo que el try se encargará de cerrarlo automáticamente cuando termine.

CharSet

El fichero de texto puede usar cualquier codificación de caracteres. Si al abrir el fichero usamos el método que hemos indicado, se supone que el fichero está en UTF-8. Sin embargo, el método Files.lines() admite un segundo parámetro con el CharSet que queramos usar. Por ejemplo

Path path = Paths.get("fichero.txt");

try (Stream<String> stream = Files.lines(path,Charset.defaultCharset())) {
...
} ...

usará la codificación de caracteres por defecto del sistema operativo.

Escribir fichero de texto

La parte más específica de java 8 ya está contada, por lo que escribir un fichero de texto requiere menos explicación. Únicamente mencionar que al escribir un fichero de texto no hay un flujo Stream de líneas, ya que el fichero no está creado. El código sencillo puede ser este

      String[] lines = new String[] { "line 1", "line 2", "line 2" };
      Path path = Paths.get("outputfile.txt");
      try (BufferedWriter br = Files.newBufferedWriter(path,
            Charset.defaultCharset(), StandardOpenOption.CREATE)) {
         for (String line : lines) {
            br.write(line);
            br.newLine();
         }
      } catch (Exception e) {
         e.printStackTrace();
      }

Declaramos un array de String para escribir en el fichero. Esto es para el ejemplo, en un código real los String pueden venir de cualquier sitio.

Abrimos el fichero como antes, en un try-with-resources, pero obteniendo un BufferedWriter. En la apertura, además del Path y del CharSet, hemos puesto la opción StandardOpenOption.CREATE. Esto crea el fichero si no existe, o lo sobreescribe si existe. Hay opciones, entre otras, para que proteste si el fichero existe StandardOpenOption.CREATE_NEW o para que añada sin sobreescribir StandardOpenOption.APPEND.

A partir de ahí, un bucle normal sobre el array de líneas para ir escribiéndolas en el fichero.

A lo java 8

No trata de la temática de escribir fichero en java 8, pero otra forma de recorrer el array, más al estilo de java 8, es usando los Stream que usamos antes. La clase Arrays nos permite obtener un Stream de cualuier array, así

String [] lines = ....
Stream<String> stream = Arrays.stream(lines);

y ahora, al igual que hicimos antes, podemos usar forEach para ir escribiendo en el fichero. Hay un problema en este caso y es que las operaciones de escritura pueden lanzar una IOException, que debemos tratar en el método que pasemos al Stream. El código completo con una expresión Lambda quedaría así

      String[] lines = new String[] { "line 1", "line 2", "line 2" };
      Path path = Paths.get("outputfile.txt");
      try (BufferedWriter br = Files.newBufferedWriter(path,
            Charset.defaultCharset(), StandardOpenOption.CREATE)) {
         Arrays.stream(lines).forEach((s) -> {
            try {
               br.write(s);
               br.newLine();
            } catch (IOException e) {
               throw new UncheckedIOException(e);
            }

         });
      } catch (Exception e) {
         e.printStackTrace();
      }

Nos fijamos sólo en el interior de la expresión Lambda. Escribimos la línea con br.write() y un salto de línea con br.newLine. Como esto puede soltar una excepción, la capturamos y, o bien la silenciamos sacando su traza por pantalla o algo, o bien como en este caso, la relanzamos como UncheckedIOException, que no es necesario capturarla, al ser Unchecked.

Github

Tienes código de ejemplo de este tutorial en Github

Enlaces