08.Ficheros

Ficheros

Introducción a los Ficheros

Introdución os ficheiros

Na programación adoitan usarse variables para almacenar información que queremos tratar durante a execución: os datos de entrada, resultados e valores intermedios Con todo, a información almacenada nas variables é volátil, desaparecendo cando remata a execución do programa. Cando queremos almacenar de xeito permanente, fálase de persistencia, sendo dúas as formas máis habituais de facelo:

  • Organizado a información nun ou varios ficheiros nun sistema de almacenamento persistente

  • Facendo uso das facilidades que nos permiten as bases de datos Neste capítulo introduciremos a primeira das opcións; e para iso, comezaremos abordando a súa definición desde o punto de vista de máis baixo nivel:

  • Un ficheiro é un conxunto de bits almacenado nun dispositivo accesible a través dunha ruta (pathname) que o identifica.

Basicamente, unha secuencia de 1s e 0s que son almacenados fóra da memoria volátil do equipo (nun disco duro ou nun DVD, por exemplo). Desde un nivel máis alto, e para o que nos interesa desde o punto de vista da programación, esta é a información que debemos ter en conta.

Tipos de ficheiros

E posible facer moitas clasificacións, que iremos detallando a continuación.

Segundo a súa estrutura de almacenamento e contido

Podemos atopar coma tipos básicos de ficheiros os de texto plano e os binarios.

Os de texto plano almacenan secuencias de caracteres correspondentes a unha codificación determinada (ASCII, Unicode, EBCDIC, etc.). Son lexibles mediante un software de edición de texto coma o Bloc de Notas de Windows ou os editores Vi ou nano de Linux.

Exemplos:

  • os ficheiros de texto con extensión .txt
  • os .csv de valores separados por comas
  • os .htm y .html correspondentes a páxinas web
  • os de linguaxes de marcas .xml ou .rss.

O sistema de codificación de caracteres máis popular é o código ASCII (American Standard Code for Information Interchange, código estándar estadounidense para intercambio de información), que define 256 caracteres distintos (todas as combinacións de 8 bits,dous elevado a 8 posibilidades). Algúns deles, chamados caracteres de control, encárganse de definir accións coma o borrado, o salto de liña ou o tabulador e non representan símbolos concretos. Podes consultar na wikipedia máis información.

Os binarios conteñen información codificada en binario para o seu procesamento por parte de aplicacións. O seu contido resulta ilexible nun editor de texto. Exemplos: executables (.exe) documentos de aplicacións (.pdf, .docx, .xlsx, .pptx) ficheiros de imaxe, audio ou vídeo (.jpg, .gif, .mp3, .avi, .mkv) librerías de sistema (.dll).

Por organización interna

Cando se utilizan ficheiros de texto plano para almacenar información pódense clasificar conforme a súa ‘‘organización interna’’ en secuenciais, de acceso directo ou aleatorio, ou indexados. Nos secuenciais a información escríbese en posicións fisicamente contiguas. Para acceder a un dato hai que recorrer todos os anteriores. Exemplo de ficheiro secuencial con información sobre clientes: 00789521T#Alba#Blanco#González#613524647$11111111H#Xosé Ramon#García#Pemán#613423824$38546998X#Xurxo#Cazás#Otero#666332245$09653801B#Antón#Resines#Pardo#619335241%

Por cada contacto estruturouse a información en cinco datos independentes: NIF, nome, primeiro apelido, segundo apelido e número de teléfono. Neste caso o programador decidiu utilizar o grade (#) como separador de datos, o dólar ($) como separador de contactos e o tanto por cento (%) coma marca de fin de ficheiro. Nos de acceso directo ou aleatorio cada liña de contido organízase cuns tamaños fixos de dato. Pódese acceder directamente ó principio de cada liña.

Nesta ocasión cada contacto ocupa unha liña do ficheiro (o final de cada unha o sistema operativo incluirá un ou dos caracteres de salto de liña invisibles para o usuario), e cada dato utiliza un número de caracteres fixo, aínda que non o ocupe totalmente (no exemplo reservanse 15 caracteres para o nome, aínda que no caso de Alba só se utilicen catro).

Finalmente, nos indexados, xeralmente nun ficheiro de acceso aleatorio a información almacénase na orde en que se da de alta. Aínda que se conseguira introducir a información de acordo a algún criterio de ordenación concreto, nalgunhas ocasións é útil poder ordenala por varios criterios distintos. No exemplo anterior é posible que necesitemos un listado de clientes ordenado por NIF e outro polo apelido. Para dar solución a este problema creouse a organización indexada, que consiste na existencia de un ou varios arquivos adxuntos que ordenan o dato (chamado clave) polo que se desexa ordenar o ficheiro e o relacionan coa localización da liña correspondente.

Por tipos de soporte de almacenamento

De “acordo á organización física dos datos”, diferenciamos entre dous tipos de soportes Nos secuenciais para acceder a un dato hai que recorrer todo o contido do soporte previo ó devandito dato (por exemplo, as cintas magnéticas). Nos direccionables pódese acceder directamente a un dato sen ter que recorrer todos os anteriores (por exemplo un disco duro). Nun soporte direccionable pódese implementar un acceso secuencial, directo ou indexado, mentres que nun soporte secuencial só se poderá implementar un acceso secuencial.

Por xerarquía de memorias

Os ficheiros adoitan almacenarse en diferentes memorias, tendo en conta que hai que atopar un compromiso entre a velocidade de acceso desexada, a capacidade requirida e o custo por bit. diagrama

E/S con fluxos

En Java defínese a abstracción de stream (fluxo) para tratar a comunicación de información entre o programa e o exterior. Entre unha fonte e un destino flúe unha secuencia de datos: os fluxos, que actúan coma interfaz co dispositivo ou clase asociada. Así permiten:

  • Operación independente do tipo de datos e do dispositivo
  • Maior flexibilidade (redirección, combinación, …)
  • Diversidade de dispositivos (ficheiro, pantallas, teclado, rede, …)
  • Diversidade de formas de comunicación (con modo de acceso secuencial ou aleatorio, e información intercambiada que pode ser binaria, caracteres ou liñas)

Analoxía entre UNIX e Java

Hai unha certa analoxía entre o funcionamento en Unix e en Java:

Unix Java
Entrada estándar Habitualmente teclado System.in
Saída estándar Habitualmente a consola System.out
Saída de erro Habitualmente a consola System.err

En Java accedese á E/S estándar a través dos campos estáticos da clase java.lang.System indicados.

Utilización dos fluxos

Lectura

  1. Abrir un fluxo a unha fonte de datos (creación do obxecto stream)
  • Teclado
  • Fichero
  • Socket remoto
  1. Mentres existan datos dispoñibles
  • Ler datos
  1. Cerrar o fluxo (método close)

Escritura

  1. Abrir un fluxo a unha fonte de datos (creación do obxecto stream)
  • Pantalla
  • Ficheiro
  • Socket local
  1. Mentres existan datos dispoñibles
  • Escribir datos
  1. Cerrar o fluxo (método close) A ter en conta: o sistema xa se encarga de abrir e pechar os fluxos estándar un fallo en calquera punto produce a excepción IOException Para o que estamos a tratar, serán de interese os File streams para escribir e ler datos en ficheiros

Exemplo de entrada de texto desde un ficheiro

 try {
    BufferedReader reader = new BufferedReader(new FileReader(nomeficheiro"));
    String linea = reader.readLine();
    while(linea != null) {
        // procesar o texto da liña
        linea = reader.readLine();
    }
    reader.close();
  }
  catch(FileNotFoundException e) {
  // non se atopou o ficheiro
  }
  catch(IOException e) {
  // algo foi mal ó ler ou pechar o ficheiro
  }

De xeito xeral, pódese comprender o funcionamento do código… pero para mellorar o seu entendemento, explicaremos que son as excepcións e como facer uso das mesmas.

Uso de finally:

import java.io.*;
 public class SimpleExceptionHandling {
    public static void main(String[] args) {
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream("C:\\UnFicheiroTemporal");
        } catch (FileNotFoundException e) {
            System.out.println("Aconteceu a excepción  :: " + e.getMessage());
            System.out.println("Necesitamos pechar FileStream!");
        } finally {
            System.out.println("Afortunadamente pecharase neste bloque");
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
            } catch (IOException e) {
            }
        }
    }
 }

try-with-resources

Mecanismo que automaticamente pecha recursos (pode ser máis de un) cando se acabou o seu traballo con eles. Por exemplo:

private static void printFile() throws IOException {
    try(FileInputStream input = new FileInputStream("ficheiro.txt")) {
        int data = input.read();
        while(data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    }
 }

Neste exemplo ábrese un FileInputStream dentro dun bloque try-with-resources, lee datos do FileInputStream, e pechao automaticamente cando a execución sae do bloque. Isto último e posible porque FileInputStream implementa a interface java.lang.AutoCloseable

Exercicio 1

Partindo do seguinte código, simplificalo co uso de try-with-resources.

Scanner scanner = null;
try {
    scanner = new Scanner(new File("test.txt"));
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (scanner != null) {
        scanner.close();
    }
}

Clase File

Podes consultar na web de Oracle a documentación de la biblioteca estándar de clases de java.

Constructores

  • File(String ruta)
  • File(String ruta, String nombre)
  • File(File directorio, String nombre)

Metodos

  • canRead() e canWrite() : comproba se o ficheiro se pode ler/escribir
  • delete(): borra o ficheiro
  • exists(): devolve boolean si existe el fichero
  • getPath(): devolve a ruta do ficheiro
  • mkdir(): crea un directorio coa ruta do obxecto que o recibe. Hay una versión que es mkdirs() que se usa para crear una jerarquía de directorios
  • isDirectory() comproba se o ficheiro é un directorio

Pódese usar rutas absolutas como no siguinte exemplo pero es totalmente recomendable usar rutas relativas:

        File prueba = new File("c:\\midirectorio\\prueba.txt");
        if(!prueba.exists())
            System.out.println("c:\\midirectorio\\prueba.txt NO existe");
        if(!new File("c:\\midirectorio").exists())
            System.out.println("c:\\midirectorio NO existe");
        if(!new File("m:").exists())
            System.out.println("m: NO existe");
        
        if(new File("l:\\midirectorio\\prueba.txt").exists())
            System.out.println("l:\\midirectorio\\prueba.txt existe");

Exemplo de mkdirs:

        String directorio = "l:/micarpeta";
        String varios = "carpeta1/carpeta2/carpeta3";

        // Crear un directorio
        boolean exito = (new File(directorio)).mkdir();
        if (exito)
            System.out.println("Directorio: " + directorio + " creado");
        // Crear varios directorios
        exito = (new File(directorio+"/"+varios)).mkdirs();
        if (exito)
            System.out.println("Directorios: " + varios + " creados");

Podes atopar na coñecida web de referencia w3schools exemplos explicativos con código de manexo de ficheiros con estes métodos.
Con todo, dende Java SE 7, existe un xeito mellor de xestionar os ficheiros: java.nio.
Entón, se existe unha forma máis axeitada de xestionar os ficheiros en Java… por que aprendemos a utilizar esta? Porque debido o enorme número de aplicacións que usan a antigua API (o que se chame legacy), Oracle non ten plan de eliminar ou desaconsellar o seu uso nas vindeiras versións de Java. E por iso, porque podes atopar un montón de código en funcionamento o que lle tes que dar mantemento (ou facer migración), e que non vai desaparecer de xeito inminente, e bo coñecer esta API.

java.nio para xestión de ficheiros e directorios

Agora que xa coñecemos esta nova posibilidade de traballar con ficheiros, que permite contar con toda a funcionalidade que se acadaba antes, indagaremos sobre cales son as melloras que trae e como traballar coa novaAPI.

Melloras de java.nio

Cales son esas melloras na xestión? Por unha banda, permite facer unha mellor xestión de erros. Poñamos este código de exemplo

File file = new File("probas/titorial.txt");
boolean result = file.delete()

Compila e pode funcionar mais no caso de fallar… como saberemos se é porque o ficheiro non existe, ou é porque non temos permisos para borrar?
Reescribindo o código coa nova NIO2 API:

Path path = Paths.get("probas/titorial.txt");
Files.delete(path);

Agora será preciso xestionar unha IOException, que dará os detalles de que foi o que aconteceu (por exemplo, non ter permisos de borrado).
A nova versión tamén mellora o soporte ós metadatos e trae melloras de rendemento evitando problemas de insuficiencia de memoria.

Diferencias con file e como facer uso de java.nio

A primeira diferencia que salta rápido a vista dun programador é o cambio no nome do paquete e da clase (observables en letra grosa os cambios). Antes construíamos un obxecto file vía o construtor:

java.io.File file = new java.io.File("probas/titorial.txt");

Agora obteremos un Path usando un método static (get):

java.nio.file.Path path = java.nio.file.Paths.get("probas/titorial.txt");

Para crear ficheiros usaremos os métodos createNewFile() e Files.createFile():

boolean result = file.createNewFile();
Path newPath = Files.createFile(path);
//Para crear un directorio, mkdir() ou Files.createDirectory():
boolean result = file.mkdir();
File newPath = Files.createDirectory(path);

Hai variantes para incluir subdirectorios que non existan con mkdirs() e Files.createDirectories():

boolean result = file.mkdirs();
File newPath = Files.createDirectories(path);

Para renomear ou mover un ficheiro, precisamos crear outra instancia do obxecto e usar renameTo() ou Files.move():

boolean result = file.renameTo(new File("baeldung/tutorial2.txt"));
Path newPath = Files.move(path, Paths.get("baeldung/tutorial2.txt"));

Finalmente para borrar, usaremos delete() ou Files.delete():

boolean result = file.delete();
Files.delete(Paths.get(path));

Lembra que os métodos legacy devolven un flag cun result set a falso en caso de erro; e os métodos NIO2 devolve unha instancia de Path instance (agás a operación de borrado que lanza unha IOException en caso de erro).

Exercicio 1

Cal sería o código para saber se o indicado no path é u ficheiro? E se é posible escribir no mesmo? Amosa o código de xeito tradicional e coa nova API NIO.

Exercicio 2

En args[0] indicas un directorio e o programa imprime seu contido (no recursivo). Exemplo: imaxe funcionamiento Faino con File e NIO

Exercicio 3

Como no anterior pero agora o programa imprime o contido de forma recursiva. Hai varias maneiras unha é directamente coa clase File e utilizando o método utilidad fileList().

Creando un ficheiro

Podemos crear un ficheiro baleiro cos atributos que desexemos co método: createFile(Path, FileAttribute); Se non se especifican os atributos, será creado con atributos por defecto. E de xa existir o ficheiro indicado na ruta, lanzarase unha excepción. O seguinte código crea un ficheiro con atributos por defecto:

Path file = ...;
try {
    // Create the empty file with default permissions, etc.
    Files.createFile(file);
} catch (FileAlreadyExistsException x) {
    System.err.format("O ficheiro chamado  %s" +  " xa existe%n", file);
} catch (IOException x) {
    // Outro tipo de fallo, coma os relacionados con permisos.
    System.err.format("createFile da erro: %s%n", x);
}

Creando un directorio

Pódese facer usando o método:
createDirectory(Path, FileAttribute);
Se non se especifica ningún atributo, terá os que correspondan por defecto. Un exemplo:

Path dir = ...;
Files.createDirectory(path);

Mostrando o contido dun directorio

Pódese facer usando o método: newDirectoryStream(Path);
Este método devolve un obxecto que implementa a interfaz DirectoryStream, que a súa vez implementa Iterable.
Recorda que o devolto DirectoryStream é un fluxo, e que se non o chamas desde un try-with-resources debes pechar o fluxo no bloque finally.
A continuación un exemplo que amosa os contidos dun directorio:

Path dir = ...;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
    for (Path file: stream) {
        System.out.println(file.getFileName());
    }
} catch (IOException | DirectoryIteratorException x) {
    // IOException nunca será lazanda pola iteracion neste exemplo
    // so pode ser lanzado por newDirectoryStream.
    System.err.println(x);
}

Borrando un ficheiro, enlace simbólico ou directorio

Debes ter en conta que para borrar un directorio este non debe ter contido, ou a operación de borrado fallará. Cando o que se borra é un enlace simbólico, o destino do enlace non será borrado.
A clase File facilita dous métodos para o borrado.
O método delete(Path) → borra o ficheiro ou lanza unha excepción se a operación falla (por exemplo, non existe a excepción ou non se gozan dos permisos requiridos.
Por exemplo:

try {
    Files.delete(path);
} catch (NoSuchFileException x) {
    System.err.format("%s: no such" + " file or directory%n", path);
} catch (DirectoryNotEmptyException x) {
    System.err.format("%s not empty%n", path);
} catch (IOException x) {
    // File permission problems are caught here.
    System.err.println(x);
}

O método deleteIfExists(Path) → a diferencia do anterior, non lanzará excepción se o ficheiro non existe (o cal pode ser útil cando se traballa con varios fíos).

Copiando un ficheiro ou directorio

Para copiar un ficheiro ou directorio existe o método: copy(Path, Path, CopyOption…);
A copia falla se o destino existe agás a opción REPLACE_EXISTING se atope indicada.
Os directorios poden ser copiados, pero debese ter en contacta que os ficheiros contidos na ruta orixe non o serán: o novo directorio sempre será creado baleiro.
O seguinte código exemplifica o funcionamento do método copy (recorda que será preciso importar o paquete con import static java.nio.file.StandardCopyOption.*):

Files.copy(orixe, destino, StandardCopyOption.REPLACE_EXISTING);
Existen outros métodos da clase Files que permiten facer a copia entre un ficheiro e un fluxo:

  • copy(InputStream, Path, CopyOptions…) → todos os bytes dun dun fluxo de entrada a un ficheiro
  • copy(Path, OutputStream) → todos os bytes dun ficheiro a un fluxo de saída

Movendo un ficheiro ou directorio

É posible mover un ficheiro e directorio usando move(Path, Path, CopyOption…)
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
Neste exemplo empregouse a opción REPLACE_EXISTING que sobreescribe de xa existir na ruta de destino o ficheiro ou directorio

Exercicio 4

Investiga la interfaz Path para obtener estos datos de un ruta:

  • Ruta do directorio pai
  • Número de elementos que compoñe a ruta
  • Obter o nome do arquivo (se o ten)

Partindo doutra ruta para o destino, copia un fichero de una carpeta a outra. Finalmente borra o path orixe.

Extra: relación y conversión entre Path y File

Entre a Interface Path e a classe File existen mecanismos para obter unha representación dun tipo o outro. No JDK7 non é necesario realizar operacións de conversión complexas soamemte é necesario recurrir ó método .toFile de cada un de eles:

  • path.toFile(): Retorna un obxecto File representando sua ruta.
  • file.toPath(): Retorna un obxecto de tipo Path construido dende unha ruta abstracta. O obxecto Path resultante encontrase asociado co sistema de arquivos por defecto.

Ficheros de Texto

Java con ficheiros de caracteres

Como indica no primeiro apartado introdutorio os ficheiros de caracteres ou tamén chamados de texto están compostos de caracteres alfanuméricos nun formato estándar (ASCII, UNICODE etc…).

Lectura

Para ler caracteres necesitamos a clase FileReader, que se obtén a partir dun obxecto File.
A clase FileReader proporciona diversos métodos de lectura:

  • int read(): lee un carácter e o devolve. clase que define as operacións básicas de entrada de bytes con ficheiros.
  • int read(char [] buf): lee ata buf.lenght caracteres de datos dunha matriz de caracteres (buf). Os caracteres lidos do ficheiro vanse almacenando no buf.
  • int read(char[] buf, int desprazamento, int n): lee ata n caracteres de datos da matriz buf, comezando por buf[ desprazamento ] e devolve o número lido de caracteres. Estes métodos devolven -1 se chegou o final do ficheiro. Exemplo:
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class LerCaracteres {    
    public static void main(String args[])throws IOException {     
        File doc = new File("Demo.txt");    
        FileReader freader = new FileReader(doc);
        //segunda forma do metodo
        char [] i = new char[100];
        freader.read(i);
        for(char j : i)
            System.out.print(j);
        freader.close();
    }}

Todo estes métodos de lectura son de carácter a carácter ou agregando un desprazamento (int a int), pero … é a maneira máis práctica? Non, hai outras alternativas que detallamos a continuación.

Usando a clase BufferedReader

Forma máis cómoda de facer lectura de ficheiro de liña a liña.
Podes consultar máis información na API de Java SE 11
A clase BufferedReader ten un método que é readLine(). Este método devolve unha liña completa en String, ata o final do ficheiro que devolve null.
Usando esta clase temos opcións de máis alto nivel de abstracción, que facilitan a vida ó programador.
Para crear un obxecto BufferedReader hai que crealo a partires de FileReader:

File doc = new File("C:\\Drive\\Learn.txt");
BufferedReader obj = new BufferedReader(new FileReader(doc));

Fluxo de comunicacion Exemplo:

public class LerLineas {   
    public static void main(String[] args) throws FileNotFoundException, IOException {
     
        File doc = new File("prueba.txt");
        //Uso do try with resources, non fai falta usar o close 
        try(BufferedReader bufferedReader = new BufferedReader(new FileReader(doc))){
        String line = bufferedReader.readLine();
            while(line != null) {
                System.out.println(line);
                line = bufferedReader.readLine();
            }
}}}

Usando a clase Scanner

Esta clase permítenos ler ficheiros dunha maneira moi práctica (ademais do seu uso moi común de recoller datos do teclado).
Podes consultar máis información na API de Java SE 11.
O que hai que ter en conta é que necesitamos un obxecto tipo File para o construtor de Scanner. Existen outras posibilidades como crear un obxecto Scanner con InputStream. Seguiremos coa primeira forma pero coma sempre en programación… hai múltiples opcións!

File doc = new File(path);
Scanner obj = new Scanner(doc);

A clase Scanner ten dous métodos clave:

  • boolean hasNext(): que devolve verdadeiro se non chega o final do ficheiro, falso en caso contrario.
  • String hasNextLine(): obtemos a liña actual do ficheiro en cadea alfanumérica.

Exemplo:

public class LerLineasScanner {
    public static void main(String[] args) { 
        //obxecto file para Scanner
        File f = new File("demo.txt");
        String cadena;
        Scanner entrada = null;
        try {
            entrada = new Scanner(f);         //crease un Scanner asociado o ficheiro
            while (entrada.hasNext()) {       //mentres non se alcance o final do ficheiro                       
                cadena = entrada.nextLine();  //lese unha liña do ficheiro
                System.out.println(cadena);   //mostrase por pantalla                                           
            }
        } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
        } finally{
            entrada.close();                                                                                      
        }}}

Escritura

En Java unha das opción que nos permite escribir nun ficheiro é coa clase FileWriter.
Podes consultar máis información na API de Java SE 11
Esta clase nos proporciona unha serie de métodos:

  • void write(int c): escribe un carácter clase que define as operacións básicas de entrada de bytes con ficheiros.
  • void write(char[] buf): escribe un array de caracteres.
  • void write(char[] buf, int desplazamento, int n): escribe n caracteres de datos da matriz buf e comezando por buf[ desplazamento ].
  • void write(String str): escribe unha cadea de caracteres.
  • append(char c): engade un carácter a un ficheiro. Para crear obxectos tipo FileWriter é necesario ter un obxecto File, como pasa con FileReader:
File ficheiro = new File(fich.txt);
FileWriter fic = new FileWriter(ficheiro);

Ademais de coñecer os anteriores métodos é necesario ter en conta o seguinte: o obxecto tipo FileWriter por defecto vai sempre sobrescribir o contido. Se o que queremos é engadir debemos poñer no construtor un segundo parámetro a true:

FileWriter fic = new FileWriter (ficheiro, true);

Exemplo:

     //try with resource acepta multiples fontes de datos
try(FileWriter fw=new FileWriter("ficheiro1.txt"); FileReader fr=new FileReader("ficheiro1.txt")){  
            //Escribimos no ficheiro un String, un caracter 97 (a)
            // e array de caracteres
            fw.write("Esto es una prueb");
            fw.write(97);
            char[] buf = {'1','2','1'}; 
            fw.write(buf);
            //Gardamos os cambios do ficheiro, método que forza o guardado
            //para que se poida ler antes de pechar
            fw.flush();
            //Leemos o ficheiro e o amosamos por pantalla
            int valor=fr.read();
            while(valor!=-1){
                System.out.print((char)valor);
                valor=fr.read();
            }
        }catch(IOException e){
            System.out.println("Error E/S: "+e);
}}}

Nota: método flush() para gardar os cambios.

Usando a clase BufferedWriter

Como pasa coa lectura, Java ofrécenos clases para mellorar a eficiencia na escritura é métodos máis sinxelos para escribir liñas. Neste caso o BufferedWriter é unha clase que deriva da clase Write, e que se compón a partir de FileWriter.
Como estades vendo é unha serie de obxectos que “alimentan” o seguinte ata chegar o que precisamos.
Podes consultar máis información na API de Java SE 11.
Na clase BufferedWriter temos os seguintes métodos coma os máis interesantes:

  • void write(String cad): permitenos escribir cadea de alfanuméricos no buffer. Tamén acepta caracteres e arrays.
  • void newLine(): escribe un salto de liña no buffer.

Exemplo:

 // A partir de Java 9 hai unha mellora do try-with-resources
        // Desta forma cerrase o buffer cando termina, reducese a try
        BufferedWriter writer = new BufferedWriter(new FileWriter("test.txt"));
            try (writer) {         
                writer.write("Esto e unha linea");
                writer.newLine();
            }     
   }}

Usando a clase PrintWriter

Por último a clase PrintWriter é moi utilizada xa que ten un método chamado println() moi cómodo de utilizar:

  • void println(String cad): escribe a cadea de caracteres no ficheiro cun salto de liña. Outra das vantaxes desta clase que deriva de Write é que o construtor só precisa a ruta do ficheiro, polo tanto aforramos obxectos e liñas de código: Exemplo:
public class EscribirPrintWriter {    
    public static void main(String[] args) throws FileNotFoundException {      
        try {
            //se non existe se crea
            PrintWriter printWriter = new PrintWriter("test.txt");
            for (int i = 1; i < 4; i++) {
                //se sobreescribe o contido, de necesitar agregar debese usar apend
                printWriter.println("Esta e a linea numero: " + i);          
            }
            printWriter.close();
        } catch (Exception e) {
            e.printStackTrace();
    }}}

Esta clase tamén ten outros métodos como printf() (saída formateada), print() (escritura sen salto de liña) ou append() (para agregar contido).

Exercicio 1

Desenvolve un programa que amañe un ficheiro de texto, quitando espazos baleiros e poñendo a primeira letra de cada línea en maiúscula. Para esta labor crea un ficheiro temporal (investiga o uso do File.createTempFile), que logo sobrescribirá o ficheiro orixinal. Para detectar espazos y letras minúsculas pódese utilizar a clase Character.

Java NIO

Tal como vimos en anteriores apartados a API java.nio.file ofrécenos xeitos máis directos de poder traballar con ficheiros.
Podes consultar máis información na API de Java SE 11.
Neste caso con ficheiros de texto e sempre a partir da clase Files e sus métodos estáticos. Os máis usados son:

  • List < string > readAllLines(Path path, Charset ch): método estático que nos devolve un listado de strings que son as liñas do ficheiro (o charset é opcional). O sinxelo deste método e que péchase e libera o recurso automaticamente ó finalizar a lectura. (existe outra posibilidade con File.lines que amosaremos na sección de lambda)
  • Path write(Path path, Iterable lines, Charset cs, OpenOption… options): esta versión do método escribe liñas ó ficheiro. É necesario un obxecto iterable (cada elemento unha liña), a codificación que se vai usar é o modo de escritura (por defecto se non existe crease, e de existir vaise sobrescribir). Charset: o frecuente é StandardCharsets.UTF_8 OpenOption: para crear o ficheiro StandardOpenOption.CREATE (hai moitas posibilidades próbalas)

Exemplo:

public class EscribirLerNIO {    
    public static void main(String[] args) {      
        EscribirLerNIO eLN = new EscribirLerNIO();
        eLN.lerLineas();
        eLN.escribirLineas();
    }
    
    public void lerLineas(){ 
        Charset charset = Charset.forName("ISO-8859-1");
        try {
            //non nos preocupamos ni de abrir ni pechar ningun recurso
            List<String> lines = Files.readAllLines(Paths.get("prueba.txt"), charset);
            for (String line : lines) {
                System.out.println(line);
      }
    } catch (IOException e) {
        System.out.println(e);
    }}
      
    public void escribirLineas(){
        Iterable<String> iterable = Arrays.asList("line1", "line2");    
        try {
            Files.write(Paths.get("prueba_escribir.txt"), iterable);
            byte[] bytes = Files.readAllBytes(Paths.get("prueba_escribir.txt"));
            System.out.println(new String(bytes));
    } catch (IOException e) {
            System.out.println(e);
    }}}
String text = "Esto es una cadena de prueba";
Files.write(Paths.get("/examples/writeText.txt"), text.getBytes(StandardCharsets.UTF_8),
        StandardOpenOption.CREATE ...);

Ficheiros de acceso secuencial

O acceso secuencial é o máis frecuente cando se traballa con ficheiros, e xa coñecemos as características e vantaxes do acceso explicadas no primeiro apartado.
Neste tipo de acceso hai te que ter en conta que debemos coñecer o formato ou estrutura do ficheiro de antemán, e ir procesándoo.
Na labor de lectura ou escritura pódese traballar a nivel de bytes ou de caracteres. O seguinte exemplo amosa unha posible aplicación liña a liña de ficheiros de texto.
O exemplo é un pequeno programa de produtos e provedores dunha empresa.

Abrir/crear ficheiro

Xa detallado no apartado de ficheiros de texto.

String fichero = "produc.txt";
File f = new File(fichero);
FileWriter fw = new FileWriter(f);

Gardar

Unha vez aberto unha posibilidade para gardar é usar a clase PrintWriter. Podes consultar máis información na API de Java SE 11
O que temos que ter en conta é que o gardar creamos unha estrutura ríxida de información/rexistros, que logo vamos a utilizar para ler ou engadir máis rexistros.
Un exemplo de gardar os produtos:

PrintWriter escritura = new PrintWriter(fw);
for (int i = 0; i < menu.produtos.size(); i++) {
      escritura.println(menu.produtos.get(i).imprimeProduto());
}
escritura.close();

A información que temos que gardar está nunha colección de obxectos, e a orden de almacenamento dos datos está no método imprimeProduto():

public String imprimeProduto() {
     String saidaProducto = codProd + ":" + tipo + ":" + nome + ":" + prezo + ":" + pais + ":";
        for (int i = 0; i < this.getCodigoProveedor().size(); i++) {
            if (i != 0) {
                saidaProduto = saidaProduto.concat(",");
            }
       saidaProduto= saidaProduto.concat(this.getCodigoProveedor().get(i).getCodProv());
        }
        return saidaProduto;
    }

Ler en ficheiros

Amosaremos a posibilidade directamente sobre este exemplo no que se lee de maneira secuencial e envorca toda esta información en obxectos e estes a súa vez introdúcense nunha colección para xestionalos axeitadamente (neste caso ArrayList):

FileReader fr = null;
            BufferedReader br = null;
            fr = new FileReader("prov.txt");
            br = new BufferedReader(fr);
            String linea;
            String[] items;
            while ((linea = br.readLine()) != null) {
                items = linea.trim().split(":");
                menu.proveedores.add(new Proveedor(items[0], items[1], items[2].toUpperCase()));
            }
            br.close();

Exercicio 2

Disponse de dous ficheiros de texto “proveedores.txt” e “productos.txt”:

  • proveedores.txt: cada liña conten a información dun provedor. Esta componse dos seguintes campos separados por “:” codProv:nome:enderezo
  • productos.txt: cada liña conten a información dun produto. Esta componse dos seguintes campos separados por “:” codProd:tipo:nome:prezo[ :pais ]:codProv,codProv,codProv

Coma se pode observar, o quinto e o sexto campo da liña está formado a súa vez por unha lista que contén o código dos provedores que subministran o devandito produto, e estes códigos están separados polo carácter “,”.
Existen dous tipos de produtos: con “n” é nacional, e con “i” internacional. Se é internacional o producto terá un campo máis que é o país de orixe.
A partires destes ficheiros crear os obxectos precisos, cunha arquitectura O.O. correcta.
Crea un programa con estas funcionalidades:

  • Engadir un produto.
  • Obter un total do facturado por parte dun provedor.
  • Engadir provedor a un determinado produto.
  • Gardar os cambios nos correspondentes ficheiros. Posible interfaz:
void crearProducto(String codProd, String tipo, String nombre, double precio, String pais) throws Exception;
    
void imprimirProveedores();
    
void imprimirProductos();
    
boolean asignarProveedor(String codProd, String codProv) throws Exception;
    
Proveedor getProveedorByCod(String codProv);
    
void facturacion(String codProv) throws Exception; // muestra por pantalla la facturación total del proveedor

Ficheros de Bytes

Nesta actividade imos ver como traballar con un tipo de ficheiros, o de bytes de maneira máis clásica e logo usando Java NIO. Para a continuación ver o uso de datos primitivos de Java con ficheiros.

Java con Ficheiros de Bytes

O fluxo de bytes é a maneira máis eficiente de traballar con entrada e saída de bytes, e tamén a máis simple e de baixo nivel. Neste caso traballarase con bytes directamente que se almacenan nos ficheiros en formato binario.

imaxe1 imaxe2

Java ofrécenos o paquete básico de Entrada/Saída java.io. Partindo deste paquete temos as dúas clases para a manipulación de ficheiros con fluxo de bytes:

  • FileInputStream: clase que define as operacións básicas de entrada de bytes con ficheiros.
  • FileOutputStream: clase que define as operacións básicas de saída de bytes con ficheiros.

Java sempre crea un obxecto para asignarlle un fluxo de bytes. As anteriores clases herdan das clases abstractas InputStream e OutputStream respectivamente.

Operacións básicas:

  • Método read(): método da clase FileInputStream, que lee do fluxo de entrada un byte (8 bits) e devolve un int (tipo primitivo de java). Hai dúas posibilidades: Lectura normal: devolve un int (24 ceros á esquerda e 8 bits resultante da lectura). Lectura fin do stream: obtense o valor “-1”. int read(): Le un byte de data do fluxo de entrada.
  • Método write(): método da clase FileOutputStream, que escribe no ficheiro un byte pero sempre a partires dun int (cast forzoso quédase cos últimos 8 bits) void write (int b): o mesmo pero escribe o número de bytes ó fluxo de saída.
  • Método flush(): método que forza o gardado no stream o no writer de bytes. Exemplo de lectura e escritura:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class FluxoBytes {
    public static void main(String[] args) {  
        //Obtéñese o directorio home do sistema onde almacenarase o ficheiro
        String directory = System.getProperty("user.home");
        String fileName = "sample.txt";
        String absolutePath = directory + File.separator + fileName;
         // escritura do ficheiro
        try(FileOutputStream fileOutputStream = new FileOutputStream(absolutePath)) {
         String fileContent = "This is a sample text.";
            //almacenamento de bytes
            fileOutputStream.write(fileContent.getBytes());
        } catch (FileNotFoundException e) {
            System.err.println("Ficheiro non encontrado");
        } catch (IOException e) {
            System.err.println("Error na E/S");
        }
        // lendo o ficheiro
        try(FileInputStream fileInputStream = new FileInputStream(absolutePath)) {
            int ch = fileInputStream.read();
            //ata a fin do stream
            while(ch != -1) {
                System.out.print((char)ch);
                ch = fileInputStream.read();
            }
        } catch (FileNotFoundException e) {
            System.err.println("Ficheiro non encontrado");
        } catch (IOException e) {
            System.err.println("Error na E/S");
        } }
}

Java NIO

A partires da versión 1.4 de Java existe un novo paquete de funcionalidades de entrada e saída que evita as deficiencias de java.io e fai máis sinxelo o uso de streams (métodos máis directos). Centrándonos en ficheiros, mellora o rendemento das operacións, aparecen habilidades asíncronas (chamadas sen bloqueo), etc… Operacións básicas:

  • Files.write: método estático de escritura de bytes. É necesario indicarlle a ruta, o contido a escribir e unha opción de escritura:clase que define as operacións básicas de entrada de bytes con ficheiros. Path Files.write(Path path,byte[] bytes, StandardOpenOption options) throws IOException Aspectos a ter en conta é que devolve a ruta do ficheiro a escribir. Para o terceiro argumento do método o máis frecuente é utilizar o enumerado StandardOpenOption, que ten múltiples opcións (enlace das opcións): hai que telas en conta para a forma de abrir o ficheiro.
  • Files.readAllBytes: método estático para ler desde un ficheiro e envorcalo a unha matriz de bytes. Necesita unha ruta, é cerra o ficheiro cando chega o final ou hai un erro de entrada ou saída. byte[] readAllBytes(Path path) throws IOException Exemplo do seu uso:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class FluxoBytesNIO {
public static void main(String[] args) {
    String directory = System.getProperty("user.home");
    String fileName = "sample.txt";
    String content = "This is a sample text.";
    Path path = Paths.get(directory, fileName);
    try {
        Files.write(path, content.getBytes(), StandardOpenOption.CREATE);
    } catch (IOException e) {
        System.err.println("Error na E/S");
    }
    try { 
        byte[] data = Files.readAllBytes(path);
        System.out.println(new String(data));
        } catch (IOException e) {
        System.err.println("Error na E/S");
        }
}}

Interface Path e clase Paths: utilidades do paquete java.NIO para manexar rutas do sistema, no noso caso rutas de ficheiros. A clase Paths ten métodos estáticos que devolve a interface Path a partires dunha URI o cadea de caracteres. A interface Path é un argumento de diferentes métodos en java.NIO, que representa a fonte de información. Un posible uso co método readAllBytes() é usalo xunto co construtor de String. Se queremos obter unha cadea de alfanuméricos de todo o contido do ficheiro podes empregalo así:

String content = new String(Files.readAllBytes(Paths.get("readMe.txt")), StandardCharsets.UTF_8);

Manexo de datos primitivos en ficheiros

En ocasións temos a necesidade de utilizar datos primitivos en ficheiros; estes datos primitivos son os herdados de linguaxes máis clásicas e non se comportan como obxectos normais no paradigma orientación de obxectos. Son os seguintes:

  • byte
  • short
  • int
  • long
  • float
  • double
  • boolean
  • char

No uso con ficheiros cada tipo de dato primitivo ten a súa función de lectura e escritura, e é frecuente utilizar ficheiros binarios.

Escritura

O primeiro paso e obter o stream do ficheiro binario, do mesmo xeito que no apartado de “Java con ficheiros de bytes”: FileOutputStream fileOutputStream = new FileOutputStream(absolutePath);

Unha vez obtido o stream para a escritura (sexa por ruta de string ou obxecto da clase Path) é preciso crear un obxecto da clase DataOutputStream que recibe como parámetro un obxecto tipo FileOutputStream:

DataOutputStream salida = new DataOutputStream(fileOutputStream); Co obxecto DataOutputStream xa temos métodos de escritura por cada tipo de dato primitivo desta forma → writexXx() onde xXx é o nome do dato primitivo. Exemplo de escribir datos primitivos int (enteiro):

import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;
public class DataOutputStreamInt {   
 public static void main(String[] args) {
        escribirInt();
    }
    //Uso de recursos e excepcións clásica sen try with resources
    private static void escribirInt() {
            
        Scanner sc = new Scanner(System.in);                                                                      
        FileOutputStream fos = null;
        DataOutputStream salida = null;
        int n;
        try {
            fos = new FileOutputStream("datos.dat");
            salida = new DataOutputStream(fos);
            System.out.print("Introduce número enteiro. -1 para rematar: ");
            n = sc.nextInt();
            while (n != -1) {
                salida.writeInt(n); //se escribe o número enteiro no ficheiro                                   
                System.out.print("Introduce número enteiro. -1 para rematar: ");
                n = sc.nextInt();
            }
        } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
        } catch (IOException e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
                if (salida != null) {
                    salida.close();
                }
            } catch (IOException e) {
                System.out.println(e.getMessage());                                                               
            }
       }}
}

Lectura

Como pasa coa escritura, necesitamos dalgunha forma obter o Stream do ficheiro que imos ler: FileInputStream fileInputStream = new FileInputStream(String ruta);

Recordade que FileInputStream pódese obter tamén do obxecto File coma unha das formas máis usadas. A partires do obxecto anterior necesitamos crear un tipo DataInputStream, que é onde java nos ofrece os métodos de lectura. Un método de lectura por cada tipo de dato primitivo: readxXx() onde xXx é o nome do dato primitivo. Exemplo de ler datos primitivos int (enteiro):

import java.io.DataInputStream;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class DataInputStreamInt {
      public static void main(String[] args) {
          lerInt();
    }
    //Uso de recursos e excepcións clásica sen try with resources
    public static void lerInt(){
        FileInputStream fis = null;
        DataInputStream entrada = null;
        int n;
        try {
            fis = new FileInputStream("datos.dat");
            entrada = new DataInputStream(fis);
            while (true) {   
                n = entrada.readInt();  //se lee  un enteiro do ficheiro                                           
                System.out.println(n);  //se mostra na pantalla
            }
        } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
        } catch (EOFException e) {
            System.out.println("Fin de fichero");
        } catch (IOException e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
                if (entrada != null) {
                    entrada.close();
                }
            } catch (IOException e) {
                System.out.println(e.getMessage());                                                               
            }
        }
    }}

As vantaxes de utilizar ficheiros binarios con datos primitivos son a inmediatez e a non necesidade de conversión da datos (casting). Por outra banda non nos da a liberdade e flexibilidade dos ficheiros de texto. Temos que ter claro qué tipo de dato imos ler.

Exercicio 1

Desenvolve un programa en Java que realice unha copia byte a byte dun arquivo pasado por parámetros. O nome do novo ficheiro sería: proba.txt -> proba_Copia.txt

Exercicio 2

Desenvolve un programa en Java que crea unha matriz de elementos de tipo double e lee por teclado o valor dos seus elementos. A continuación escribe o contido da matriz nun ficheiro. O principio do ficheiro escríbense dous enteiros cos valores do número de filas e columnas da matriz.

Serialización

Nesta páxina imos ver como recuperar obxectos dun ficheiro e tamén como almacenalos nel.

¿Que é a serialización de obxectos?

A serialización é un proceso de transformación que nos permite codificar obxectos da nosa arquitectura orientada a obxectos. Esta codificación sírvenos para transmitir ou almacenar obxectos. No noso caso vamos a traballar nunha forma de almacenamento e recuperación de obxectos en ficheiros, pero a serialización tamén é usada en sockets, en modelos cliente servidor etc… e decir fluxos (stream).

Serializacion

A necesidade de serialización xurde no propio paradigma de programación orientada a obxectos. E moito máis cómodo e natural andar a manexar obxectos, que non datos primitivos ou cadeas de alfanuméricos obtidos de ficheiros. Entón Java permítenos gardar e recuperar obxectos en ficheiros binarios. Para a serialización de obxectos (que non é máis que escribir ou ler un obxecto completo) fai falla un obxecto serializable: unha secuencia de bytes que contén datos del propio obxecto e metadatos do mesmo. Estes metadatos son o tipo de obxecto que é e a sua arquitectura interna (datos primitivos ou referencias a outros obxectos). Para indicar a Java que un obxecto é serializable a sua clase ten que implementar a interfaz Serializable, desta forma:

Public class Agenda implements Serializable {
private String nombre;
private String p_Apellido;
private transient String s_Apellido;


public Agenda(String nombre, String p_Apellido, String s_Apellido){
this.nombre = nombre;
this.p_Apellido = p_Apellido;
this.s_Apellido = s_Apellido;
}}

Aspectos útiles a ter en conta na serialización:

  • Coa herdanza as clases fillo son tamén serializable se o son no pai.
  • Todo os atributos de clase tipo primitivo son serializables.
  • Se non se desexa serializar algún atributo úsase o modificador transient.
  • Unha interfaz non se pode serializar.
  • As colecciones de obxectos pódense serializar.
  • SerialVersionUID: é un identificador de clases cando serializamos os seus obxectos. Vai ser un atributo da clase de tipo long, estático e inmutable (final): private static final long serialVersionUID = 4L;

Para todo o proceso de serialización e deserialización temos dúas clases ObjectInputStream e ObjectOutputStream, que se expoñen a continuación.

Escritura e serialización de obxectos

Na escritura de un obxecto nun ficheiro necesitamos crear un fluxo de saída primeiro. O máis frecuente é coa clase FileOutputStream, a partires do fluxo xa podemos crear un obxecto ObjectOutputStream, que ten o método de escritura: void writeObject(Object o): método de escritura dun obxecto no fluxo que se lle pasa como parámetro. Exemplo:

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.NoSuchElementException;
import java.util.Scanner;
//Clase para serializar e gardar obxectos da clase Account
public class CreateSequentialFile
{
   private static ObjectOutputStream output; // obxecto con fluxo de saída   
   public static void main(String[] args)
   {
      openFile();
      addRecords();
      closeFile();
   }
   //método de abrir ficheiro
   public static void openFile()
   {
      try {
         output = new ObjectOutputStream(
            Files.newOutputStream(Paths.get("clients.ser")));
      }
      catch (IOException ioException)
      {
         System.err.println("Error na apertura de ficheiro. Pechando.");
         System.exit(1); 
      } 
   } 
   // método de agregar obxectos
   public static void addRecords()
   {
      
      boolean fin= false; 
      Scanner input = new Scanner(System.in);   
      System.out.println("Introduza o número de conta, primer nome, apelido e balance.");
      while (!fin) 
      {
         try 
         {                        
            // crease un novo obxecto; asúmese datos correctos
            Account record = new Account(input.nextInt(),
               input.next(), input.next(), input.nextDouble());
            // serializa e garda o obxecto no ficheiro
            output.writeObject(record); 
            System.out.println("Desexa continuar? S|N");
            String sair= input.next();
            if (sair.equalsIgnoreCase("N")) fin = true;   
         } 
         catch (NoSuchElementException elementException)
         {
            System.err.println("Datos incorrectos.");
            input.nextLine(); // descartamos datos de entrada 
         } 
         catch (IOException ioException)
         {
            System.err.println("Error na escritura. Pechando.");
            break;
         } 
      }
   } 
   //método de pechar ficheiro
   public static void closeFile() 
   {
      try 
      {
         if (output != null)
            output.close();
      } 
      catch (IOException ioException)
      {
         System.err.println("Erro pechando ficheiro. Pechando.");
      } } } 

Pojo do exemplo:

import java.io.Serializable;
// clase na que os seus obxectos gardaranse nun ficheiro
public class Account implements Serializable
{
   private int account;
   private String firstName;
   private String lastName;
   private double balance;   
   // inicializacion do obxecto cos valores por defecto
   public Account() 
   {
      this(0, "", "", 0.0);
   }   
   public Account(int account, String firstName, 
      String lastName, double balance)
   {
      this.account = account;
      this.firstName = firstName;
      this.lastName = lastName;
      this.balance = balance;
   }
   public void setAccount(int acct)
   {
      this.account = account;
   } 
   public int getAccount() 
   { 
      return account; 
   }    
   public void setFirstName(String firstName)
   {
      this.firstName = firstName;
   } 
   public String getFirstName() 
   { 
      return firstName; 
   }    
   public void setLastName(String lastName)
   {
      this.lastName = lastName;
   } 
   public String getLastName() 
   {
      return lastName; 
   }    
   public void setBalance(double balance)
   {
      this.balance = balance;
   } 
   public double getBalance() 
   { 
      return balance; 
   } 
} 

Lectura e deserialización de obxectos

A lectura é moi semellante a escritura; é necesario crear un obxecto de lectura de fluxo co FileInputStream. Despois xeramos ObjectInputStream que ten o método de lectura: Object readObject(): obtense un obxecto do ficheiro e son deserializados, de maneira secuencial, un detrás doutro. Exemplo:

import java.io.EOFException;     
import java.io.IOException;      
import java.io.ObjectInputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
//Clase para deserializar e ler obxectos clase Account do ficheiro
public class ReadSequentialFile
{
   private static ObjectInputStream input;
   public static void main(String[] args)
   {
      openFile();
      readRecords();
      closeFile();
   } 

   // abrir ficheiro
   public static void openFile()
   {
      try 
      {
         input = new ObjectInputStream(          
            Files.newInputStream(Paths.get("clients.ser")));
      } 
      catch (IOException ioException)
      {
         System.err.println("Erro abrindo ficheiro.");
         System.exit(1);
      } 
   }
   // método de ler obxectos
   public static void readRecords()
   {
      System.out.printf("%-10s%-12s%-12s%10s%n", "Conta",
         "Primer nome", "Apelidos", "Balance");
      try 
      {
         while (true) // bucle ata EOFException
         {
            // lectura e cast
            Account record = (Account) input.readObject();
            // mostramos os datos
            System.out.printf("%-10d%-12s%-12s%10.2f%n",  
               record.getAccount(), record.getFirstName(), 
               record.getLastName(), record.getBalance());
         } 
      }
      catch (EOFException endOfFileException)
      {
         System.out.printf("%nNon hai mais datos.%n");
      } 
      // Excepcion que xorde cando nos chega un obxecto que non esperamos
      catch (ClassNotFoundException classNotFoundException)
      {
         System.err.println("Tipo de obxecto inválido.");
      } 
      catch (IOException ioException)
      {
         System.err.println("Erro lendo o ficheiro.");
      } 
   } 
   // método de peche
   public static void closeFile()
   {
      try
      {
         if (input != null)
            input.close();
      } 
      catch (IOException ioException)
      {
         System.err.println("Error closing file. Terminating.");
         System.exit(1);
      } 
   } } 

Problema na sobrescritura de ficheiros na serialización

Se intentas abrir e agregar novos obxectos nun ficheiro xa utilizado, é decir agregar obxectos, o máis probable é que cando queras ler che apareza o seguinte erro: Error → IOException: invalid type code: AC É provocado porque se crea unha cabeceira extra sempre que se abre o ficheiro en modo escritura. Esa cabeceira ten a información dos metadatos dos obxectos almacenados e non pode estar duplicada. Para arranxar este problema hai que programar un propio ObjectOutputStream e sobrescribir o método writeStreamHeader:

import java.io.*;
//clase de proba
class Persona implements Serializable{
    String nombre;
    int edad;
 
    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }
}
// Creamos o noso ObjectOutputStream
class MiObjectOutputStream extends ObjectOutputStream{
    /** Constructor que recibe OutputStream */
    public MiObjectOutputStream(FileOutputStream out) throws IOException{
        super(out);
    }
    //Re definición do método de escribir a cabeceira para que non faga nada. */
    @Override
    public void writeStreamHeader() throws IOException{
    }
   }
class SobreEscrituraSer{ 
    public static void main(String[] args) throws IOException, ClassNotFoundException{      
        //Primeira escritura normal
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("persoas.dat"));
        Persona p;
        p = new Persona("Juan Lopez", 30);
        oos.writeObject(p);
        oos.close();     
        //Aa segunda e de adelante usamos noso MiobjectOutputStream
        //Parametro true para a agregación de objetos
         MiObjectOutputStream moos = new MiObjectOutputStream(new FileOutputStream("persoas.dat",true));
        p = new Persona("Jose Fernandez", 28);
        moos.writeObject(p);
        moos.close();    
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("persoas.dat"));
        p=(Persona)ois.readObject();
        System.out.println(p.nombre+", "+p.edad);
        p=(Persona)ois.readObject();
        System.out.println(p.nombre+", "+p.edad);
        ois.close();
 }
}

Aspectos importantes a ter en conta na serialización

  • Os datos primitivos sempre son serializables, non fai falta implementar a interfaz.
  • Si hai composición de clases, todas as clases implicadas deben ser serializables:
import java.io.*;
class Punto{
    int x;
    int y;

    public Punto(int x, int y) {
        this.x = x;
        this.y = y;
    }
}
class Rectangulo implements Serializable{
    Punto origen;
    int base;
    int altura;
    Rectangulo(int x, int y, int base, int altura){
        origen=new Punto(x,y);
        this.base=base;
        this.altura=altura;
    }
    public String toString(){
        return origen.x+","+origen.y+","+base+","+altura;
    }

}
class Unidad6{
    public static void main(String[] args) throws IOException, ClassNotFoundException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("rectangulos.dat"));
        Rectangulo r;
        r = new Rectangulo(0,0,2,3);
        oos.writeObject(r);
        r = new Rectangulo(2,3,10,10);
        oos.writeObject(r);
        oos.close();
        
    }
}

Xenera este error: Exception in thread “main” java.io.NotSerializableException: Punto

  • En herencia é suficiente que a clase pai ou base sexa serializable para que os fillos o sexan.
  • Menos as interfaces todas as coleccións típicas son serializables.
  • Si un atributo da clase non desexas que se serialice (motivos de seguridade por exemplo), palabra reservada transient.
  • Podemos xenerar un id para a serialización das clases. Chámase SerialVersionUID é o mellor que sexa tipo long, static e final. Proba a xeneralo dende o IDE ou podes asignalo a man así:
private static final long serialVersionUID = 1L;

Exercicio 1

Crea un programa que almacene nunha lista obxectos Employee e se almacene nun ficheiro. Logo debes recuperar esa lista de obxectos do ficheiro. Employee.java:

import java.io.Serializable;
// Sempre aínda que se introduza nunha colección
public class Employee implements Serializable {
    String id;
    String firstName;
    String lastName; 
    public Employee(String id, String firstName, String lastName) {
        super();
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }
    @Override
    public String toString() {
        return "Employee [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + "]";
    }
}

Exercicio 2

Partindo de este exercicio de mapas enlace Serializa o mapa cos datos e gárdaos nun ficheiro. A continuación deserealiza dende o ficheiro e lee o mapa.

Exercicio 3

Temos que gardar obxectos dunha clase persoa co serialVersionUID, password e nome. Pero nos dicen que por motivos de seguridad o campo password no se pode gardar. Fai a proba de gardar e despois ler do ficheiro.

Exercicio extra (podeis usar coleccións)

Quérese gardar información sobre animais para realizar un xogo infantil. Os animais a mostrar no xogo son cans ou gatos.

A información que se quere gardar é o nome e do animal e a sua idade.

No caso dos cans, quérese saber se teñen o rabo longo ou non, e no caso dos gatos se teñen bigotes longos ou non.

Tamén se desexa saber a forma de expresarse de cada un. Os gatos farán “MIAU” e os cans “GUAU”.

Vamos a necesitar un método (getTipo) que nos indique que clase de animal é (can ou gato) xa que haberá momentos no que traballaremos con animais.

Unha vez definidas as clases para gardar a información anterior, crea una clase de nome OperacionesAnimales: Dicha clase poderá gardar información sobre cinco Cans e cinco gatos. Debes facer uso de arrays estáticos de tamaño 5. Disporá dun método addPerro(Perro perro) que permitirá agregar un perro (dos cinco posibles). Necesitarás un contador que che indique en qué posición do array debes de añadir o perro novo. Dicho método devolverá un boolean indicando se o can foi engadido correctamente. Se intentamos engadir un novo can e o array está ocupado, devolverá false.

O mesmo para o caso dos gatos método (addGato(Gato gato)):

  • Disporá dun método gardarAnimaisADisco(String fichero) no que se gardarán todos os obxectos que se encontren no array (perros / gatos). Escribiremos no ficheiro, antes dos obxectos, o número de obxectos total que imos a escribir.
  • Disporá dun método lerAnimaisDisco(String fichero) que lerá de disco os obxectos gardados do paso anterior. Primeiro leremos o número de animais gardados no disco para dar memoria o array que debe devolver o método. Despois leremos todos os animais que iranse gardando no array que temos que devolver. Se actualizarán os datos da clase (o array que gardan os gatos e os cans).

Lambda con Ficheros

En cada nova versión de Java amplíase o uso de lambda e expresións de programación funcional: actualízanse así as posibilidades de Java para ser unha linguaxe máis directa e menos “verbosa”: resumindo, menos liñas de programación para acadar o mesmo.

icono lambda

Coa API java.NIO temos a clase Files, que se pode utilizar con expresións lambda. A clase Files xunto coa clase Stream crean un binomio moi útil para operacións con ficheiros.

Ler un ficheiro de maneira secuencial

Path file = Paths.get("readFile.txt");
try(Stream<String>lines = Files.lines(file)
    .onClose(() -> System.out.println("Fin de lectura"))) {
        lines.forEach(System.out::println); }

Unha vez que se obtén a ruta do ficheiro utilizamos a método estático lines() de Files. Este método devólvenos un fluxo de traballo (Stream), que ten unha directiva cando se pecha o fluxo (onClose) e un método para percorrer todas as liñas (forEach). A clase Stream pódese “alimentar” de moitas formas: se pretendemos unha maior eficiencia un obxecto BufferedReader debe ser o elixido. Files ten un método no que se obtén un buffer de lectura (newBufferedReader):

try(BufferedReader br = Files.newBufferedReader(file);
    Stream<String> lines = br.lines()
        .onClose(() -> System.out.println("Fin de lectura de ficheiro."));){
            lines.forEach(System.out::println);
}

Ler con procesamento paralelo

Outra opción que nos ofrece Stream e onde non nos ten que importar o tratamento secuencial do ficheiro é o predicado parallel(). O que fai esta opción é crear fíos de execución sobre o contido do Stream (neste caso o ficheiro), mellorando o rendemento:

try(Stream<String>lines = Files.lines(file)
        .parallel()
        .onClose(() -> System.out.println("Fin de lectura de ficheiro."))) {
        //modificamos esta parte para poder observar os fíos que se xeran
        lines.forEach(s ->{
        System.out.println(s +" " + Thread.currentThread().getName());});

Exemplo:

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream; 
public class LerFicheiroLambda { 
    public static void main(String[] args) throws IOException {    
        Path file = Paths.get("probas.txt");  
        // Uso do try-with-resources con lambda
        try(Stream<String>lines = Files.lines(file)
                        .onClose(() -> System.out.println("Fin de lectura de ficheiro."))) {
            lines.forEach(System.out::println);
        }  
        //versión más eficiente utilizando BufferedReader
        System.out.println("Version mais eficiente....");   
        try(BufferedReader br = Files.newBufferedReader(file);
            Stream<String> lines = br.lines()
                    .onClose(() -> System.out.println("Fin de lectura de ficheiro."));){  
            lines.forEach(System.out::println);
        }       
        //versión de procesamento pararelo
        System.out.println("Version con procesamento paralelo....");
         try(Stream<String>lines = Files.lines(file)
                        .parallel()
                        .onClose(() -> System.out.println("Fin de lectura de ficheiro."))) {    
            //modificamos esta parte para poder observar os fíos que se xeran
             lines.forEach(s ->{
                    System.out.println(s +" " + Thread.currentThread().getName());});
        }      
    }
}

Lectura condicional

Stream ten un predicado que é filter(): permítenos filtrar e quedar coas liñas que nos interesan do ficheiro. Ten múltiples opcións; a continuación amosamos un exemplo sinxelo:

try (Stream<String> lines = Files.lines(Path.of("probas.txt"))) {
    long i = lines.filter(line -> line.startsWith("T"))
    .count();

Filter necesita un predicado lóxico booleano, ou utilizar métodos de Stream coma os seguintes:

  • contentEquals(String st)
  • endsWith(String sufix)
  • startsWith(String prefix)
  • matches(String red): expresións regulares.
  • … Exemplo:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public class LerFilter {        
    public static void main(String[] args) throws IOException {      
        //Exemplos do uso do filter     
        try (Stream<String> lines = Files.lines(Path.of("probas.txt"))) {
            long i = lines.filter(line ->line.startsWith("T"))
            .count();
        System.out.println("Numero de lineas que empezar por 'T' e " + i);
    }                
        try (Stream<String> lines = Files.lines(Path.of("probas.txt"))) {
            long i = lines.filter(line ->line.isEmpty())
            .count();
        System.out.println("Número de lineas vacias: " + i);
    }            
    try (Stream<String> lines = Files.lines(Path.of("probas.txt"))) {  
            lines.filter(line ->line.endsWith("11"))
            .forEach(System.out::println);
    }
}}

Uso de lambda para operacións de directorios e arquivos de sistema

Para listar os directorios dunha ruta, temos a directiva isDirectory de Files:

try (Stream<Path> paths = Files.list(Path.of(folderPath))) {
    paths.filter(Files::isDirectory)
    .forEach(System.out::println);
}

Fíxate que devolve un Stream de path é dicir rutas de ficheiros. Para listar os ficheiros dunha ruta:

Para percorrer todo a árbore de directorios

Coma se fora de xeito recursivo Files ten un método estático chamado walk() que devolve un stream con todos os elementos da carpeta, das subcartafoles etc. ata chegar a final da árbore. O stream estará formado por todos os camiños que forma esa árbore.

Stream<Path> miStream = Files.walk(Paths.get("./src"));
miStream.forEach(System.out::println);

Búsqueda de ficheiros

A clase Files ten un método de busca de ficheiros. O uso máis frecuente é con tres argumentos de entrada:

  • Path p: obxecto que representa a ruta onde se vai buscar.
  • int maxDepth: enteiro que representa que nivel de busca se precisa na árbore de directorios. Se indicamos Integer.Maxvalue sería ata o final da árbore de ficheiros, se indicamos “1” sería o primeiro nivel.
  • BiPredicate <Path, BasicFileAttributes> matcher: é unha interfaz funcional que serve para indicar as condicións de busca.
try (Stream<Path> pathStream = Files.find(path, Integer.MAX_VALUE,
    (p, basicFileAttributes) ->
    p.getFileName().toString().equalsIgnoreCase(fileName))

Exemplo:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class BusquedaFicheiroLambda { 
    public static void main(String[] args) throws IOException {    
        Path path = Paths.get("/Users/jmor/NetBeansProjects/AccesoDatos_licencia");
        List<Path> result = busquedaPorNombre(path, "pom.xml");
        result.forEach(x -> System.out.println(x));    
        List<Path> result2 = busquedaPorExtension(path, ".txt");
        result2.forEach(x -> System.out.println(x));     
    }   
    //Metodo de busca usando .find con Stream
    public static List<Path> busquedaPorNombre(Path path, String fileName)
            throws IOException {
        //listado que xenera a busqueda
        List<Path> result;    
        //find ten tres atributos de entrada
        //a interfaz funcional fai ignorar as maiúsculas y minúsculas
        try (Stream<Path> pathStream = Files.find(path,
                Integer.MAX_VALUE,
                (p, basicFileAttributes) ->
                        p.getFileName().toString().equalsIgnoreCase(fileName))
        ) {
            result = pathStream.collect(Collectors.toList());
        }
        return result;
    }   
    //método de busca por extensión 
    public static List<Path> busquedaPorExtension(Path path, String ext)
            throws IOException {
        //listado que xera a busca
        List<Path> result;      
        //a interfaz funcional filtra por ficheiros (no directorios) y que
        // tena a extension
        try (Stream<Path> pathStream = Files.find(path,
                Integer.MAX_VALUE,
                (p, attr) -> 
                   attr.isRegularFile() && p.toString().endsWith(ext))
        )
        {
            result = pathStream.collect(Collectors.toList());
        }
        return result;
    }    
}

Exercicio 1

  • Crea un método de buscade de arquivos con Files.find a partires dunha ruta cun determinado tamaño (Files.size) usando expresións lambda. Mellora o teu programa detectando se un ficheiro é un directorio (Files.isDirectory), e devolvendo falso.
  • Crea un método que busque un ficheiro nun directorio e en todas os seus subcartafois (ata o final da árbore de directorios) usando lambda. Para ser máis eficiencia usa un filtro con Files::isRegularFile.