Por que eu devo ler este artigo:Utilizar os arquivos como estruturas de dados para armazenamento de longo prazo de grandes volumes de dados, sendo importante na persistência de dados e na realização do processamento de arquivos em aplicações Java. Isto permite a um programa ler e gravar dados em arquivos baseados em bytes e caracteres e em arquivos de acesso aleatório. Na implementação de programas Java que criam, recuperam, atualizam e processam arquivos de dados. O processamento de arquivo representa no Java um subconjunto das capacidades de processamento que permitem a um programa armazenar e processar volumes maciços de dados persistentes.

Um arquivo é uma abstração utilizada para uniformizar a interação entre o ambiente de execução (um programa) e os dispositivos externos (o disco rígido, por exemplo). Arquivos são utilizados para persistir dados, já o termo fluxo se refere a dados ordenados que são lidos e gravados em um arquivo.

O dispositivo de memória principal de um computador é volátil, não permitindo que as informações fiquem armazenadas permanentemente. Por este motivo os dados armazenados em variáveis são perdidos sempre que um programa terminar sua execução normalmente ou devido a algum problema. A solução para este tipo de situação é utilizar uma estrutura de dados que permita a gravação de informações em dispositivos de memória secundária como disquete, disco rígido, entre outros. Nas linguagens de programação, a estrutura de dados que guarda informações em dispositivos de armazenamento secundário é chamada de arquivo.

Um arquivo é uma abstração utilizada para uniformizar a interação entre o ambiente de execução (um programa) e os dispositivos externos (o disco rígido, por exemplo). Os computadores utilizam os arquivos como estruturas de dados para armazenamento de longo prazo de grandes volumes de dados, mesmo depois de os programas que criaram os dados terminarem sua execução. Dados mantidos em arquivos são chamados de dados persistentes porque eles existem além da duração da execução do programa em dispositivos de armazenamento secundário.

Neste artigo, será abordado como os programas Java criam, recuperam, atualizam e processam arquivos de dados. O processamento de arquivos representa no Java um subconjunto das capacidades de processamento que permitem a um programa armazenar e processar volumes maciços de dados persistentes.

Fluxos de dados (streams)

O Java enxerga um arquivo como um fluxo sequencial de caracteres ou bytes finalizados por uma marca de final de arquivo ou pelo número total de bytes registrados.

Os fluxos de entrada e saída baseados em bytes, denominados de arquivos binários, armazenam e recuperam dados no formato binário. Nos arquivos binários os dados são organizados como sendo uma sequência de bytes. Um byte é composto de oito bits agrupados. Um bit, abreviação de “binary digit”, é o menor item de dados manipulado em um computador e pode assumir o valor 0 ou o valor 1. Por exemplo, se o valor 9 fosse armazenado utilizando um fluxo baseado em bytes, ele seria armazenado no formato binário do seu valor numérico 9, ou 1001.

Os fluxos de entrada e saída baseados em caracteres, denominados de arquivos de texto, armazenam e recuperam dados como uma sequência de caracteres dividida em linhas terminadas por um caractere de fim de linha (\n).

O sistema de entrada e saída em Java fornece uma “interface” consistente ao programador, independente do dispositivo real (teclado, disco, monitor, rede de comunicação, entre outros) que é acessado, estabelecendo um nível de abstração entre o programa e o dispositivo utilizado. Essa abstração, representada na Figura 1, é chamada de stream e o dispositivo real é chamado de arquivo.

Java: Arquivos e fluxos de dados
Figura 1. Representação dos fluxos de entrada e saída de dados (streams)

Um programa Java abre um arquivo instanciando, criando e associando um objeto ao fluxo de dados. As classes utilizadas para criar esses objetos serão discutidas mais adiante. De concreto, o Java cria três objetos de fluxo que são associados a dispositivos de entrada ou saída sempre que um programa inicia a execução:

  1. System.in, objeto de fluxo de entrada padrão, normalmente utilizado pelo programa para obter dados a partir do teclado;
  2. System.out, objeto de fluxo de saída padrão, normalmente utilizado pelo programa para enviar resultados para a tela do computador; e
  3. System.err, objeto de fluxo de erro padrão, normalmente utilizado pelo programa para gerar saída de mensagens de erro na tela.

Programas Java implementam o processamento de arquivos utilizando as classes do pacote java.io. A hierarquia de classes oferecida por este pacote, apresentada de forma parcial na Figura 2, é relativamente grande e complexa, oferecendo mais de 50 classes distintas para o processamento de entrada e saída em arquivos baseados em bytes e caracteres e arquivos de acesso aleatório. Os arquivos são abertos criando-se objetos através de uma das classes de fluxo, citando:

  • FileInputStream: para entrada baseada em bytes de um arquivo;
  • FileOutputStream: para saída baseada em bytes para um arquivo;
  • RandomAccessFile: para entrada e saída baseada em bytes de e para um arquivo;
  • FileReader: para entrada baseada em caracteres de um arquivo;
  • FileWriter: para saída baseada em caracteres para um arquivo.
Hierarquia parcial de classes do pacote java.io
Figura 2. Hierarquia parcial de classes do pacote java.io

Classe File

Inicialmente será apresentada a classe File, utilizada para recuperar informações sobre arquivos ou diretórios em disco. Os objetos da classe File não abrem arquivos de dados e também não fornecem capacidades de processamento de arquivos, apenas são utilizados para especificar arquivos ou diretórios. Para instanciar um objeto da classe File deve-se escolher entre uma das quatro formas de construtores:

  • public File (String name) - especifica o name de um arquivo ou diretório para associar com o objeto instanciado;
  • public File (String pathtoname, String name) - utiliza o argumento pathtoname para localizar o arquivo ou diretório especificado por name;
  • public File (File directory, String name) - utiliza o objeto directory existente para localizar o arquivo ou diretório especificado por name;
  • public File (URI uri) - utiliza o objeto uri para localizar o arquivo. Um Uniform Resource Identifier (URI) é uma cadeia de caracteres usada para identificar um arquivo como um endereço da internet.

A classe File possui vários métodos que permitem recuperar informações sobre o objeto instanciado, como o tipo (arquivo ou diretório) do argumento informado, tamanho em bytes, data da última modificação, se existe permissão para leitura e gravação, citando:

  • boolean canRead() - retorna true se o aplicativo pode ler o arquivo especificado; false, caso contrário;
  • boolean canWrite() - retorna true se o aplicativo pode modificar o arquivo especificado; false, caso contrário;
  • boolean delete() - exclui o arquivo ou diretório especificado pelo aplicativo retornando true se a exclusão foi realizada com sucesso; false, caso contrário;
  • boolean exits() - retorna true se o nome usado como argumento no construtor File indica um arquivo ou diretório existente; false, caso contrário;
  • String getAbsolutePath() - retorna uma String com o caminho absoluto do arquivo ou diretório. Um caminho absoluto contém o caminho completo com todos os diretórios desde o diretório-raiz até o arquivo ou o diretório especificado;
  • String getName() - retorna uma String com o nome do arquivo ou diretório;
  • String getParent() - retorna uma String com o diretório-pai do arquivo ou diretório;
  • String getPath() - retorna uma String com o caminho do arquivo ou diretório;
  • boolean isFile() - retorna true se o nome usado como argumento no construtor File é um arquivo; false, caso contrário;
  • boolean isDirectory() - retorna true se o nome usado como argumento no construtor File é um diretório; false, caso contrário;
  • long lastModified() - retorna um inteiro longo que representa a data/hora em que o arquivo ou diretório foi modificado pela última vez;
  • long length() - retorna o comprimento do arquivo em bytes. Se for um diretório, o valor 0 será retornado;
  • String[] list() - retorna um array de strings com os nomes de arquivos e diretórios que representam o conteúdo de um diretório. Retorna null se o objeto File for um arquivo;
  • boolean mkdir() - cria o diretório especificado pelo aplicativo retornando true se a operação foi realizada com sucesso; false, caso contrário;
  • boolean mkdirs() - cria toda a estrutura do diretório especificado pelo aplicativo retornando true se a operação foi realizada com sucesso; false, caso contrário.

A classe Exemplo1, apresentada na Listagem 1, utiliza um objeto instanciado a partir da classe File para recuperar informações de um arquivo ou diretório.

Listagem 1. Recuperando as informações de um arquivo ou diretório

  import java.io.File;
  import java.util.Scanner;
   
  public class Exemplo1 {
   
    public static void main(String[] args) {
      Scanner ler = new Scanner(System.in);
   
      System.out.printf("Informe o nome de um arquivo ou diretório:\n");
      String nome = ler.nextLine();
   
      File objFile = new File(nome);
      if (objFile.exists()) {
         if (objFile.isFile()) {
            System.out.printf("\nArquivo (%s) existe - tamanho: %d bytes\n",
              objFile.getName(),  objFile.length());
         }
         else {
           System.out.printf("\nConteúdo do diretório:\n");
           String diretorio[] = objFile.list();
           for (String item: diretorio) {
             System.out.printf("%s\n", item);
           }
         }
      } else System.out.printf("Erro: arquivo ou diretório informado não existe!\n");
    }
  }

A execução do código fonte da Listagem 1 solicita ao usuário final o nome do arquivo ou diretório, instancia o objeto File e verifica se o arquivo ou diretório informado existe. Se o objeto instanciado existe, são exibidas no fluxo de saída padrão as informações recuperadas, para um arquivo: nome e tamanho em bytes; para um diretório: caminho e a lista com os nomes dos arquivos e diretórios que representam o seu conteúdo. A Figura 3 ilustra a execução da classe Exemplo1 na recuperação de informações de um arquivo enquanto que a Figura 4 mostra informações recuperadas de um diretório.

Java: Arquivos e fluxos de dados
Figura 3. Executando a classe Exemplo1 na recuperação de informações de um arquivo
Java: Arquivos e fluxos de dados
Figura 4. Executando a classe Exemplo1 na recuperação de informações de um diretório

A seguir serão abordadas as classes de fluxo do pacote java.io que permitem a um programa Java realizar o processamento, ler e gravar dados, em arquivos.

Processamento em arquivos

A interação de um programa com um dispositivo através de arquivos passa por três etapas: abertura ou criação de um arquivo, leitura ou gravação de dados e fechamento do arquivo.

Os arquivos poderão ter os seus dados acessados para leitura ou gravação na forma sequencial, direta ou indexada. Para o propósito deste artigo não será abordada a forma de acesso indexada.

Em um arquivo de acesso sequencial o processo de leitura e gravação é feito de forma contínua, um byte ou caractere após o outro a partir do início. O acesso direto permite a localização imediata a cada um dos dados, desde que se conheça a sua posição no arquivo.

Fluxo de dados baseado em bytes (arquivo binário)

Primeiramente será demonstrada a criação e a gravação de dados em um arquivo binário sequencial. Como mencionado anteriormente, esses são arquivos em que os dados são armazenados no formato binário de forma contínua a partir do seu início.

A Listagem 2 apresenta uma forma de utilizar as classes FileOutputStream e DataOutputStream na criação e gravação de dados baseada em bytes para um arquivo binário. Os métodos writeUTF(), writeChar(), writeInt() e writeDouble() da classe DataOutputStream são aplicados na gravação de bytes que representam valores do tipo literal, caractere, inteiro e real, respectivamente.

Listagem 2. Gravando dados em um arquivo binário

  import java.io.DataOutputStream;
  import java.io.FileNotFoundException;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.util.Scanner;
   
  public class Exemplo2 {
   
    public static void main(String[] args) throws FileNotFoundException, IOException {
      Scanner ler = new Scanner(System.in);
      String nome;
      char sexo;
      int idade, altura;
      double pc; // peso corporal
   
      FileOutputStream arq = new FileOutputStream("d:\\arquivo.dat");
      DataOutputStream gravarArq = new DataOutputStream(arq);
   
      System.out.printf("Informe o seu nome:\n");
      nome = ler.nextLine();
   
      System.out.printf("\nInforme o seu sexo (M/F)...........: ");
      sexo = (char)System.in.read();
   
      System.out.printf("Informe a sua idade................: ");
      idade = ler.nextInt();
   
      System.out.printf("Informe o seu peso corporal (em kg): ");
      pc = ler.nextDouble();
   
      System.out.printf("Informe a sua altura (em cm).......: ");
      altura = ler.nextInt();
   
      gravarArq.writeUTF(nome);
      gravarArq.writeChar(sexo);
      gravarArq.writeInt(idade);
      gravarArq.writeDouble(pc);
      gravarArq.writeInt(altura);
   
      arq.close();
   
      System.out.printf("\nDados gravados com sucesso em \"d:\\arquivo.dat\".\n");
    }
  }

No código fonte da Listagem 2, o arquivo externo arquivo.dat é aberto para operações de saída através do objeto arq instanciado e criado a partir da classe FileOutputStream. Já o objeto de gravação gravarArq é associado a um fluxo de saída de dados baseado em bytes através da classe DataOutputStream. Definido o arquivo binário externo, foram implementados comandos para a entrada dos dados: nome, sexo, idade, peso corporal e altura. A seguir estes dados são gravados (write) no arquivo, que é fechado através do método close(). A Figura 5 ilustra a execução da classe Exemplo2.

Java: Arquivos e fluxos de dados
Figura 5. Executando a classe Exemplo2 na gravação de dados em um arquivo binário

Para Deitel & Deitel: os dados são armazenados em arquivos de forma que possam ser recuperados para processamento quando necessários. Nos arquivos binários os dados são recuperados, de forma contínua a partir do seu início, no formato binário e traduzidos pelo sistema em valores numéricos ou literais.

A Listagem 3 apresenta uma forma de utilizar as classes FileInputStream e DataInputStream na abertura e leitura de dados baseada em bytes de um arquivo binário sequencial. Os métodos readUTF(), readChar(), readInt() e readDouble() da classe DataInputStream são aplicados na leitura de bytes que representam valores do tipo literal, caractere, inteiro e real, respectivamente.

Listagem 3. Lendo dados de um arquivo binário

  import java.io.DataInputStream;
  import java.io.FileInputStream;
  import java.io.IOException;
   
  public class Exemplo3 {
   
    public static void main(String[] args) throws IOException {
      String nome;
      char sexo;
      int idade, altura;
      double pc;  // peso corporal
      double GEB; // gasto energético basal
   
      FileInputStream arq = new FileInputStream("d:\\arquivo.dat");
      DataInputStream lerArq = new DataInputStream(arq);
   
      nome = lerArq.readUTF();
      sexo = lerArq.readChar();
      idade = lerArq.readInt();
      pc = lerArq.readDouble();
      altura = lerArq.readInt();
      
      // calculando o gasto energético basal
      if ((sexo == 'M') || (sexo == 'm'))
         GEB = 66.47 + (13.75 * pc) + (5 * altura) - (6.76 * idade);
      else
         GEB = 655.1 + (9.56 * pc) + (1.85 * altura) - (4.67 * idade);
   
      System.out.printf("Nome..................: %s\n", nome);
      System.out.printf("Sexo..................: %c\n", sexo);
      System.out.printf("Idade.................: %d anos\n", idade);
      System.out.printf("Peso Corporal.........: %.2f kgs\n", pc);
      System.out.printf("Altura................: %d cm\n", altura);
      System.out.printf("Gasto Energético Basal: %.0f kcal/dia\n", GEB);
   
      arq.close();
    }
  }

Na aplicação Java da Listagem 3, o arquivo externo arquivo.dat, criado no diretório raiz d, é aberto para operações de entrada através do objeto arq instanciado e criado a partir da classe FileInputStream. Já o objeto de leitura lerArq é associado a um fluxo de entrada de dados baseado em bytes através da classe DataInputStream. Definido o arquivo binário externo, foram implementados os comandos para leitura (read) dos dados: nome, sexo, idade, peso corporal e altura que estão gravados no arquivo. Em seguida estes dados são utilizados para calcular o gasto energético basal (GEB) através da fórmula desenvolvida por Haris e Benedict. O GEB estima a quantidade de energia diária em quilocalorias (kcal) para a manutenção das funções vitais do organismo de uma criança ou adulto. A Figura 6 ilustra a execução da classe Exemplo3.

Java: Arquivos e fluxos de dados
Figura 6. Executando a classe Exemplo3 na leitura de dados de um arquivo binário

Fluxo de dados baseado em bytes (arquivo de texto)

Nesta seção será demonstrada a criação e a gravação de um arquivo de texto sequencial. Como mencionado anteriormente, esses são arquivos em que os dados são organizados como uma sequência de caracteres dividida em linhas terminadas por um caractere de fim de linha.

A Listagem 4 apresenta como utilizar as classes FileWriter e PrinterWriter na criação e gravação de dados baseada em caracteres para um arquivo de texto. Os métodos print(), println() e printf() são aplicados na gravação caracteres.

Listagem 4. Gravando dados em um arquivo de texto

  import java.io.FileWriter;
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.util.Scanner;
   
  public class Exemplo4 {
   
    public static void main(String[] args) throws IOException {
      Scanner ler = new Scanner(System.in);
      int i, n;
   
      System.out.printf("Informe o número para a tabuada:\n");
      n = ler.nextInt();
   
      FileWriter arq = new FileWriter("d:\\tabuada.txt");
      PrintWriter gravarArq = new PrintWriter(arq);
   
      gravarArq.printf("+--Resultado--+%n");
      for (i=1; i<=10; i++) {
        gravarArq.printf("| %2d * %d = %2d |%n", i, n, (i*n));
      }
      gravarArq.printf("+-------------+%n");
   
      arq.close();
   
      System.out.printf("\nTabuada do %d foi gravada com sucesso em \"d:\\tabuada.txt\".\n", n);
    }
  }

No código fonte da Listagem 4 o arquivo externo tabuada.txt é aberto para operações de saída através do objeto arq instanciado e criado a partir da classe FileWriter. Já o objeto de gravação gravarArq é associado a um fluxo de saída de dados baseado em caracteres através da classe PrinterWriter. Definido o arquivo de texto externo, foi implementado o processo de repetição (for) para montar a tabuada de n gravando o resultado no arquivo, que é fechado através do método close(). Observe que os símbolos %n usados no método printf() gravam um pula linha (caractere de escape \n) no arquivo de saída. A Figura 7 ilustra a execução da classe Exemplo4.

Java: Arquivos e fluxos de dados
Figura 7. Executando a classe Exemplo4 na gravação de dados em um arquivo de texto

Na Listagem 5 foram utilizadas as classes FileReader e BufferedReader na abertura e leitura de dados de um arquivo de texto. O método read() é empregado na leitura de um caractere, enquanto que o método readLine() deverá ser aplicado na leitura de uma linha do arquivo de texto.

Listagem 5. Lendo dados de um arquivo de texto

  import java.io.BufferedReader;
  import java.io.FileReader;
  import java.io.IOException;
  import java.util.Scanner;
   
  public class Exemplo5 {
   
    public static void main(String[] args) {
      Scanner ler = new Scanner(System.in);
   
      System.out.printf("Informe o nome de arquivo texto:\n");
      String nome = ler.nextLine();
   
      System.out.printf("\nConteúdo do arquivo texto:\n");
      try {
        FileReader arq = new FileReader(nome);
        BufferedReader lerArq = new BufferedReader(arq);
   
        String linha = lerArq.readLine(); // lê a primeira linha
        while (linha != null) {
          System.out.printf("%s\n", linha);
          linha = lerArq.readLine(); // lê da segunda até a última linha
        }
   
        arq.close();
      } catch (IOException e) {
          System.out.printf("Erro na abertura do arquivo: %s.\n", e.getMessage());
      }
      System.out.println();
    }
  }

Na aplicação Java da Listagem 5 o usuário final informa o nome do arquivo que será aberto para operações de entrada através do objeto arq, instanciado e criado a partir da classe FileReader. Já o objeto de leitura lerArq é associado a um fluxo de entrada de dados baseado em carecteres através da classe BufferedReader. Definido o arquivo de texto externo, foi implementado um processo de repetição (while) para ler todas as linhas do arquivo para exibi-las no fluxo padrão de saída System.out. Observe que foi realizado o tratamento de exceções (try) usando a classe IOException, responsável pelas exceções em operações de entrada e saída. Outras importantes classes de exceções derivadas da classe IOException são: FileNotFoundException, que ocorre se o arquivo não for localizado ou um novo arquivo não puder ser criado, e EOFException, que ocorre se houver uma tentativa de ler depois do final do arquivo.

Na Figura 8 é apresentada a execução da classe Exemplo5 na leitura do arquivo de texto tabuada.txt, enquanto a Figura 9 mostra como resultado a leitura de um arquivo de texto que corresponde ao código fonte de uma aplicação Java.

Java: Arquivos e fluxos de dados
Figura 8. Executando a classe Exemplo5 na leitura do arquivo de texto tabuada.txt
Executando a classe Exemplo5 na leitura de um código fonte Java
Figura 9. Executando a classe Exemplo5 na leitura de um código fonte Java

Fluxo de dados baseado em bytes (arquivo de acesso aleatório)

Por último, será demonstrado como utilizar a classe RandomAccessFile na leitura e gravação de dados baseada em bytes para um arquivo binário. Utilizando a classe RandomAccessFile o programa pode ler e gravar os dados a partir do local especificado pelo ponteiro de posição do arquivo. Este ponteiro é similar ao índice de uma tabela em memória e indica a posição do byte onde o usuário se encontra trabalhando dentro do arquivo. No caso de arquivos de acesso direto este ponteiro poderá ser movimentado para qualquer posição do arquivo, através do método seek().

Para definir o tipo de operação que será realizada no arquivo deve-se especificar o modo de abertura através do segundo argumento do construtor. O modo de abertura rw especifica que o arquivo é aberto ou criado para operações de leitura e gravação. Já o modo de abertura r indica que o arquivo será aberto apenas para operações de leitura.

A Listagem 6 demonstra como utilizar a classe RandomAccessFile instanciada com o modo de abertura rw para gravação de “registros” no final de um arquivo binário. Os métodos writeChars() e writeDouble() gravam bytes que representam valores do tipo sequência de caracteres e real, respectivamente. Já o método seek() é utilizado para movimentar o ponteiro de posição para um byte específico no arquivo, e devido a esta característica os arquivos são denominados de arquivos de acesso aleatório ou direto.

Listagem 6. Gravando dados em um arquivo de acesso aleatório

  import java.io.FileNotFoundException;
  import java.io.IOException;
  import java.io.RandomAccessFile;
  import java.util.Scanner;
   
  public class Exemplo6 {
   
    public static void main(String[] args) throws FileNotFoundException, IOException {
      Scanner ler = new Scanner(System.in);
      String nome;
      double nota1, nota2;
      long n;
   
      RandomAccessFile diario = new RandomAccessFile("d:\\diario.dat", "rw");
   
      diario.seek(diario.length()); // posiciona o ponteiro de posição no final do arquivo
      n = (diario.length() / 56) + 1; // número do novo registro
      while (true) {
        System.out.printf("%do. registro-------------------------------\n", n);
        System.out.printf("Informe o nome do aluno, FIM para encerrar:\n");
        nome = ler.nextLine();
        if (nome.equalsIgnoreCase("FIM"))
           break;
        
        System.out.printf("\nInforme a 1a. nota: ");
        nota1 = ler.nextDouble();
   
        System.out.printf("Informe a 2a. nota: ");
        nota2 = ler.nextDouble();
   
        ler.nextLine(); // esvazia o buffer do teclado
   
        gravarString(diario, nome, 20);
        diario.writeDouble(nota1);
        diario.writeDouble(nota2);
   
        n = n + 1;
   
        System.out.printf("\n");
      }
      diario.close();
    }
   
    private static void gravarString(RandomAccessFile arq, String s, int tam) throws IOException {
      StringBuilder result = new StringBuilder(s);
      result.setLength(tam);
      arq.writeChars(result.toString());
    }
  }

No código fonte da Listagem 6 o arquivo externo diario.dat é aberto ou criado no diretório raiz d e fica disponível para operações de entrada e saída (modo rw) através do objeto diario, instanciado e criado a partir da classe RandomAccessFile. A gravação de novos registros é realizada sempre que o ponteiro de posição estiver posicionado no final do arquivo.

Nota: Se o ponteiro de posição estiver localizado em uma posição intermediária, a gravação irá modificar ou sobrepor os bytes indicados.

Ao abrir um arquivo, o ponteiro, por default, é posicionado no primeiro byte, e a utilização do método seek(), combinado com o método length(), desloca o ponteiro para o final do arquivo, possibilitando a inclusão de novos registros. A entrada de dados (nome e notas escolares) foi implementada dentro de um processo de repetição (while), finalizado sempre que o usuário informar a palavra “FIM” no nome de um aluno. A seguir os dados de entrada são gravados (write) no arquivo. O método gravarString() garante que no campo “nome” serão gravados exatamente 20 caracteres, assegurando assim que todos os registros no arquivo tenham o mesmo tamanho. Nota: 56 representa o tamanho, em bytes, de um registro, sendo 20 caracteres para o campo “nome”, que correspondem a 40 bytes, e dois double para os campos “nota1” e “nota2”, que correspondem a 8 bytes cada, totalizando os 56 bytes. A Figura 10 ilustra a execução da classe Exemplo2 na entrada dos dados do quarto registro.

Java: Arquivos e fluxos de dados
Figura 10. Executando a classe Exemplo6 na gravação de dados em um arquivo de acesso aleatório

Na Listagem 7 foi utilizada a classe RandomAccessFile com o modo r para leitura de dados do arquivo. Os métodos readChar() e readDouble() usados na aplicação são empregados na leitura de bytes que representam valores do tipo caractere e real, respectivamente.

Listagem 7. Lendo dados de um arquivo de acesso aleatório

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
 
public class Exemplo7 {
 
  public static void main(String[] args) throws IOException {
    String nome, sit;
    double nota1, nota2, media;
 
    try {
      RandomAccessFile diario = new RandomAccessFile("d:\\diario.dat", "r");
 
      System.out.printf("Reg Nome................ 1aNota 2aNota Média. Situação.\n");
      System.out.printf("-------------------------------------------------------\n");
      int i;
      long n = (diario.length() / 56); // calcula o número de registros do arquivo (sizefile)
      for (i=1; i<=n; i++) {
        nome = lerString(diario, 20);
        nota1 = diario.readDouble();
        nota2 = diario.readDouble();
        media = (nota1 + nota2) / 2;
        sit = (media >= 6.0 ? "aprovado" : "reprovado");
        System.out.printf("%3d %20s %6.2f %6.2f %6.2f %s\n", i, nome, nota1, nota2, media, sit);
      }
      System.out.printf("-------------------------------------------------------\n");
      diario.close();
    } catch (FileNotFoundException e) {
        System.err.printf("Arquivo não encontrado: %s.\n", e.getMessage());
    }
  }
 
  private static String lerString(RandomAccessFile arq, int tam) throws IOException {
    char result[] = new char[tam];
    for (int i=0; i<tam; i++) {
      result[i] = arq.readChar();
    }
    return(new String(result).replace('\0', ' '));
  }
}

Na aplicação Java da Listagem 7 o arquivo externo diario.dat, criado no diretório raiz d, é aberto para operações de entrada (modo r) através do objeto diario, instanciado e criado a partir da classe RandomAccessFile. Definido o arquivo de acesso aleatório externo, foi implementado um processo de repetição (for) para ler os n registros do arquivo. O tamanho do arquivo representado pela variável n é o resultado da expressão: tamanho do arquivo, definido pelo método length(), dividido pelo tamanho em bytes de um registro. O método lerString()tam caracteres do arquivo, sendo 20 o tamanho determinado para o campo “nome”, e retorna uma cadeia com exatos 20 caracteres preenchendo, conforme a necessidade, com espaços em branco no final. Os dados recuperados, nome e as duas notas escolares, são exibidos no fluxo de saída padrão juntamente com a média e a situação do aluno (média superior ou igual a 6.0, aluno aprovado, caso contrário, aluno na situação reprovado). A Figura 11 ilustra a execução da classe Exemplo7.

Java: Arquivos e fluxos de dados
Figura 11. Executando a classe Exemplo7 na leitura de dados de um arquivo de acesso aleatório

Conclusões

Neste artigo foram apresentados os conceitos relacionados a arquivos e fluxos de dados em Java utilizados para persistirem grandes volumes de dados.

Os arquivos são conjuntos de dados que podem armazenar dados na memória secundária de um sistema, usualmente unidades de disco rígido. Os fluxos representam uma abstração utilizada para uniformizar a interação entre um programa Java e os dispositivos reais, como por exemplo, o teclado, monitor de vídeo e arquivos externos.

O processamento de arquivos, ler e gravar dados, é realizado utilizando as classes do pacote java.io. Desta hierarquia de classes, destacam-se: FileInputStream (DataInputStream) e FileOutputStream (DataOutputStream), que disponibilizam os recursos necessários para entrada e saída de dados baseada em bytes de e para um arquivo binário; as classes FileReader (BufferedReader) e FileWriter (PrintWriter), que implementam os processos utilizados na entrada e saída de dados baseada em caracteres de e para um arquivo de texto; e RandomAccessFile, que é utilizada para entrada e saída baseada em bytes em um arquivo binário de acesso aleatório ou direto.

Grande parte das aplicações comerciais não podem se limitar a armazenar seus dados no dispositivo de memória principal correndo o risco de perdê-los definitivamente quando a aplicação é finalizada por algum motivo. Elas precisam implementar uma interação com a base de dados na forma de arquivos, para persistir informações, processá-las e disponibilizá-las conforme a necessidade.

Referência

  • Deitel, H.M. (2005) Java: como programar.São Paulo: Person Prentice Hall, 6ª edição. Capítulo 14: Arquivos e fluxos, páginas 494-550.

Link