quarta-feira, 28 de agosto de 2013

Introdução ao I/O em Java

Introdução

Saudações aos guerreiros da programação. Estamos aqui com mais um post e dessa vez vamos abordar a leitura e escrita de arquivos utilizando os recursos fornecidos pelo Java em sua versão 7. Lembrando que estou aberto a criticas e comentários, acredito que é trocando idéias e experiencia que crescemos, portanto não deixem de expressar sua opinião. E mãos a obra.

A API de I/O do Java

Trata-se dos recursos fornecidos pelo Java para trabalhar com fluxo de Entrada e Saída de dados através de meios como console, arquivos e socket’s. O Fluxo acontece basicamente de duas formas: Leitura de dados (InputStream) e Escrita de dados (OutputStream). O Java trabalha com stream aplicando polimorfismo de forma a ocultar a origem e destino dos dados, ou seja, para alguns métodos, independente da fonte dos dados, a sintaxe para execução será a mesma. Segue abaixo um exemplo do uso da classe para leitura de bytes em um arquivo passado como parâmetro:

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;

public class IO {
       
        public void AbrirArquivo(String arquivo){
                try{
                        InputStream entrada = new FileInputStream(arquivo);
                        int b = entrada.read();               
                }catch(IOException e){
                        System.out.println("Error: " + e.getMessage() + "\n" + " Não foi possível abrir o arquivo");
                }              
        }      

O mesmo exemplo para leitura no console:

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;

public class IO {
       
        public void lerConsole{
                try{
                        InputStream entrada = System.in;
                        int b = entrada.read();               
                }catch(IOException e){
                        System.out.println("Error: " + e.getMessage() + "\n" + " Não foi possível abrir o arquivo");
                }              
        }
}

O processo para escrita de dados é semelhante mudando apenas a classe de stream que no caso é OutputStream. Observe abaixo exemplo de escrita em um arquivo qualquer:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class IO {
       
        private OutputStream saida;

        public void escreverArquivo(String arquivo){
                try{
                        saida = new FileOutputStream(arquivo);
                        saida.write(20);       
                }catch(IOException e){
                        System.out.println("Error: " + e.getMessage() + "\n" + " Não foi possível escrever no arquivo");
                }              
        }
}

O mesmo exemplo para escrita no console:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class IO {
       
        public void escreverConsole(){
                try{
                        OutputStream saida;saida = System.out;
                        saida.write(20);
                }catch(IOException e){
                        System.out.println("Error: " + e.getMessage() + "\n" + " Não foi possível escrever no arquivo");
                }              
        }
}

Perceba que independente do método de entrada será sempre executado “read” para leitura do arquivo, da mesma forma na escrita de dados, independente do destino será sempre executado “write” para escrita.
É importante saber que a  forma do Java para trabalhar com os stream’s só ocorre em bytes, isto significa que para trabalhar com dados mais robustos como caracteres será necessário um tratamento específico. Para realizar este tipo de tratamento é necessário aplicar uma classe que funcionará como ponte para converter os bytes em caracteres. Esta classe é a InputStremReader. Veja abaixo como funciona:

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;


public class IO {
       
        public void AbrirArquivo(String arquivo){
                try{
                        InputStream entrada = new FileInputStream(arquivo);
                        InputStreamReader leitura = new InputStreamReader(entrada);
                        char c = (char)  leitura.read();
                }catch(IOException e){
                        System.out.println("Error: " + e.getMessage() + "\n" + " Não foi possível abrir o arquivo");
                }              
        }
}

Perceba que a InputStreamReader recebe um arquivo de InputStrem para leitura e através do cast para char é atribuído o valor a variável “c” para posterior utilização. Acontece que evetualmente ler caracter a caracter de um arquivo pode não ser uma boa idéia, dessa forma é necessário um recurso que viabilize a leitura de uma linha inteira de do arquivo e atribua o valor a uma String. A classe capaz de fazer esse trabalho é a BufferedReader. Para exemplificar a sua utilização vamos pegar o ultimo exemplo onde estamos com o objeto de leitura do arquivo “fonte.txt”  podemos fazer a leitura pelo BufferedReader:

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;


public class IO {
       
        public void AbrirArquivo(String arquivo){
                try{
                        InputStream entrada = new FileInputStream(arquivo);
                        InputStreamReader leitura = new InputStreamReader(entrada);
                        BufferedReader buff = new BufferedReader(leitura);
                        String s = buff.readLine();
                }catch(IOException e){
                        System.out.println("Error: " + e.getMessage() + "\n" + " Não foi possível abrir o arquivo");
                }              
        }
}

Dessa forma percebemos uma certa hierarquia no processo de leitura, onde começamos no mais baixo nível, como byte, e subimos até o nível mais alto da String respeitando a seguinte ordem:
  • InputStrem: bytes.
  • InputStreamReader: caracteres.
  • BufferedReader: Strings.

O processo para escrita é semelhante mudando apenas o nome das classes envolvidas segundo a ordem abaixo:

  • OutputStream: bytes.
  • OutputStreamWriter: caracteres.
  • BufferedWriter: Strings.
Assim, um exemplo para o procedimento seria:

import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;

public class IO {
       
        public void escreverArquivo(String arquivo){
                try{
                        OutputStream esc = new FileOutputStream(arquivo);
                        OutputStreamWriter escrita = new OutputStreamWriter(esc);
                        BufferedWriter bw = new BufferedWriter(escrita);
                        bw.write(123);

                }catch(IOException e){
                        System.out.println("Error: " + e.getMessage() + "\n" + " Não foi possível escrever no arquivo");
                }              
        }

}

FileReader e FileWriter

Você deve ter percebido até aqui que existem classes demais para se trabalhar com arquivos de texto, deixando o processo de certo modo um pouco confuso. Foi pensando em minimizar isso que foram criadas as classes FileReader e FileWriter para leitura e escrita de caracteres respectivamente. A forma de instanciar estas classes segue o mesmo padrão das demais e utilizá-las além de ser mais simples torna o código mais legível. No exemplo abaixo elas são utilizadas juntamente com as classes BufferedReader e BufferedWriter, pois como visto anteriormente, estas trabalham com strings. Observe:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;


public class IO {
        public void escreverArquivo(String arquivo){
                try{
                        FileWriter w = new FileWriter(arquivo);
                        BufferedWriter bw = new BufferedWriter(w);
                        bw.write("Olá mundo");
                }catch(Exception e){
                        System.out.println("Error: " + e.getMessage());
                }
        }
        public void lerArquivo(String arquivo){
                try{
                FileReader r = new FileReader(arquivo);
                BufferedReader br = new BufferedReader(r);
                 String s = br.readLine();
                }catch(Exception e){
                        System.out.println("Error: " + e.getMessage());
                }
        }
}

Perceba que a classe FileReader substitui as classes InputStream e InputStreamReader, e da mesma forma acontece com FileWriter e as classes OutputStream e OutputStreamWriter. Lembrando que as mesmas trabalham apenas com arquivos de texto diferentemente dos pares de Stream que trabalham byte a byte independente do tipo do dado.

Scanner e PrintStream

São outras duas opções para se trabalhar com Stream. Estas duas são ainda mais simples de se utilizar que as demais, tanto pela simplicidade quanto pela clareza do código. Tal qual nos exemplos anteriores estas duas classes trabalham aos pares onde,

Scanner: lê dados de uma Stream de entrada.


PrintStream: escreve dados em uma Stream de saída. 

Veja abaixo um exemplo de utilização da classe Scanner:

import java.io.FileInputStream;
import java.util.Scanner;

public class IO {
        @SuppressWarnings("finally")
        public String lerArquivo(String arquivo){
                StringBuilder texto = new StringBuilder("");
                try{           
                Scanner s = new Scanner(new FileInputStream(arquivo));
                while (s.hasNextLine()){
                        texto.append(s.nextLine());
                        texto.append("\n");
                }
                }catch(Exception e){
                        System.out.println("Error: Falha ao abrir arquivo");
                }finally{
                        return texto.toString();
                }
        }
}

Esta classe possui um método que retorna em uma String o conteúdo de um arquivo passado como parâmetro. Para este exemplo criei um arquivo de nome "entrada.txt" e salvei na raiz da unidade C com um conteúdo qualquer. Para demonstrar a utilização da classe foi criado a classe com o método main conforme descrito a seguir:

public class App {

        /**
         * @param args
         */
        public static void main(String[] args) {
                // TODO Auto-generated method stub
                IO a = new IO();
                System.out.println(a.lerArquivo("C:/entrada.txt"));
        }
}


Agora um exemplo da utilização da classe PrintStream:

import java.io.FileOutputStream;
import java.io.PrintStream;

public class IO {
        @SuppressWarnings("finally")
        public boolean escreverArquivo(String arquivo, String msg){
                boolean retorno = false;
                try{
                PrintStream ps = new PrintStream(new FileOutputStream(arquivo));
                ps.print(msg);
                retorno = true;
                }catch(Exception e){
                        System.out.println("Error: Não foi possivel escrever no arquivo");
                }finally{
                        return retorno;
                }
        }

}

Nesta classe criada perceba que ela possui um método escreverArquivo justamente para realizarmos a demonstração. Este método recebe dois parâmetros do tipo String, um é o destino da Stream e o outro, o conteúdo. É interessante notar que a  classe PrintStream possui os métodos print() e println(), não por mera coincidência, os métodos são idênticos aos que utilizamos na classe System.out. Isto ocorre pelo fato de System.out ser uma PrintStream. Por fim, o método retorna true caso tenha ocorrido tudo bem. Veja a utilização da classe em uma pequena aplicação:

public class LerArquivo {

        /**
         * @param args
         */
        public static void main(String[] args) {
                // TODO Auto-generated method stub
                IO a = new IO();
                System.out.println(a.escreverArquivo("C:/entrada.txt", "Alo Mundo!"));
        }
}


A Classe java.io.File

A classe File é muito útil para representar, em forma de objeto, arquivos e pastas do sistema. Uma vez instanciado, através de seus métodos é possível obter praticamente todas as informações necessárias. Alguns destes métodos são:

isDiretory() informa se é arquivo ou diretório

exists() informa se o arquivo ou diretório existe

getName() obtém o nome do arquivo ou diretório

getPath() obtém o caminho completo do arquivo ou diretório

listFiles() lista os arquivos de um diretório

Para exemplificar criei uma classe de nome Arquivo que instancia um objeto do tipo de File e faz uso dos seus métodos para atender a chamadas de métodos próprios na classe Arquivo. Observe o código abaixo:

import java.io.File;


public class Arquivo {
        public File arquivo;
       
        public Arquivo(String arquivo){
                this.arquivo = new File(arquivo);
        }
       
        public boolean isDiretorio(){
                return this.arquivo.isDirectory();
        }
       
        public boolean existe(){
                return this.arquivo.exists();
        }
       
        public String getNome(){
                return this.arquivo.getName();
        }
       
        public String getCaminho(){
                return this.arquivo.getPath();
        }
       
        public File[] lisArquivos(){
                return this.arquivo.listFiles();
        }

}

Em seguida uma pequena aplicação para fazer uso de nossa classe Arquivo:

import java.io.File;


public class App {
        public static void main(String[] args){
               
                Arquivo arq = new Arquivo("C:/temp/");
                System.out.println(arq.isDiretorio());
                System.out.println(arq.getCaminho());
               
                File[] f;
               
                f = arq.lisArquivos();
               
                for (File x : f){
                        System.out.println(x.getName());
                }
        }
       

}

Observe no código da aplicação que fiz um pequeno trabalho para listar o nome dos arquivos dentro da pasta fornecida na instanciação do arquivo. Como o método lisArquivo() que faz uso do método listFiles() da classe File, retorna um vetor com nome de arquivos, então tive de utilizar uma estrutura de laço para obter o nome de cada arquivo na pasta. Eu poderia ainda Alterar o metodo lisArquivo() para retornar a string com os nomes de arquivos pronta para exibição no console, mas essa opção pode ficar como exercício a você caro leitor.

Try-with-resources

Quando trabalhamos com Stream seja para ler ou escrever, é necessário abrir o arquivo através de meios já demostrado neste mesmo post. O fato é que todo arquivo aberto precisa ser fechado. O método correspondente a este procedimento é o close(). Nas ultimas versões do java o procedimento era feito acrescentando-se no bloco  finally do try-cath, a chamada ao método. No entanto a partir da versão 7 do java dispomos do recurso try-with-resources no qual basta declarar a leitura do arquivo entre parenteses do lado da palavra chave try que assim, após a execução do bloco de código, o java automaticamente executa o método close() do arquivo aberto. Neste exemplo demonstrarei o método mais comum :

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;

public class IO {
       
        public void AbrirArquivo(String arquivo){
                InputStream entrada = null;
                try {
                        entrada = new FileInputStream(arquivo);
                        int b = entrada.read();               
                }catch(IOException e){
                        System.out.println("Error: " + e.getMessage() + "\n" + " Não foi possível abrir o arquivo");
                }finally{
                        if  (entrada != null){
                               try {
                                       entrada.close();
                               } catch (IOException e) {
                                       // TODO Auto-generated catch block
                                       System.out.println("Error: não foi possivel fechar arquivo");
                               }
                        }
                }
        }      

}

No exemplo acima utilizamos o bloco finally para que o método tente fechar o arquivo de qualquer maneira lançando uma mensagem de erro no console caso algo não funcione como esperado. Mas como dito anteriormente, com o novo recurso do java, o mesmo código pode ser escrito assim:

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;

public class IO {
       
        public void AbrirArquivo(String arquivo){
                
                try (InputStream entrada = new FileInputStream(arquivo)){
                        int b = entrada.read();               
                }catch(IOException e){
                        System.out.println("Error: " + e.getMessage() + "\n" + " Não foi possível abrir o arquivo");
                }
        }      

}


Finalizando

Bem pessoal, chegamos ao final de mais um post. Vimos um pouco da API de IO do java, as classes stream, como trabalhar com elas, alternativas de uso. Espero que tenha sido útil e meu desejo como sempre é que este possa servir como um ponto de partida, um ponta-pé inicial na busca do aprendizado. Um forte abraço, fiquem com Deus e até a próxima.

Nenhum comentário:

Postar um comentário