15. Pacote java.io

Conhecendo uma API

Vamos passar a conhecer APIs do Java. java.io e java.util possuem as classes que você mais comumente vai usar, não importando se seu aplicativo é desktop, web, ou mesmo para celulares.

Apesar de ser importante conhecer nomes e métodos das classes mais utilizadas, o interessante aqui é que você enxergue que todos os conceitos previamente estudados são aplicados a toda hora nas classes da biblioteca padrão.

Não se preocupe em decorar nomes. Atenha-se em entender como essas classes estão relacionadas e como elas estão tirando proveito do uso de interfaces, polimorfismo, classes abstratas e encapsulamento. Lembre-se de estar com a documentação (javadoc) aberta durante o contato com esses pacotes.

Veremos também threads e sockets em capítulos posteriores, que ajudarão a condensar nosso conhecimento, tendo em vista que no exercício de sockets utilizaremos todos conceitos aprendidos, juntamente com as várias APIs.

Orientação a objetos no java.io

Assim como todo o resto das bibliotecas em Java, a parte de controle de entrada e saída de dados (conhecido como io) é orientada a objetos e usa os principais conceitos mostrados até agora: interfaces, classes abstratas e polimorfismo.

A ideia atrás do polimorfismo no pacote java.io é de utilizar fluxos de entrada (InputStream) e de saída (OutputStream) para toda e qualquer operação, seja ela relativa a um arquivo, a um campo blob do banco de dados, a uma conexão remota via sockets, ou até mesmo às entrada e saída padrão de um programa (normalmente o teclado e o console).

As classes abstratas InputStream e OutputStream definem, respectivamente, o comportamento padrão dos fluxos em Java: em um fluxo de entrada, é possível ler bytes e, no fluxo de saída, escrever bytes.

A grande vantagem dessa abstração pode ser mostrada em um método qualquer que utiliza um OutputStream recebido como argumento para escrever em um fluxo de saída. Para onde o método está escrevendo? Não se sabe e não importa: quando o sistema precisar escrever em um arquivo ou em uma socket, basta chamar o mesmo método, já que ele aceita qualquer filha de OutputStream!

InputStream, InputStreamReader e BufferedReader

Para ler um byte de um arquivo, vamos usar o leitor de arquivo, o FileInputStream. Para um FileInputStream conseguir ler um byte, ele precisa saber de onde ele deverá ler. Essa informação é tão importante que quem escreveu essa classe obriga você a passar o nome do arquivo pelo construtor: sem isso o objeto não pode ser construído.

A classe InputStream é abstrata e FileInputStream uma de suas filhas concretas. FileInputStream vai procurar o arquivo no diretório em que a JVM fora invocada (no caso do Eclipse, vai ser a partir de dentro do diretório do projeto). Alternativamente você pode usar um caminho absoluto.

Quando trabalhamos com java.io, diversos métodos lançam IOException, que é uma exception do tipo checked – o que nos obriga a tratá-la ou declará-la. Nos exemplos aqui, estamos declarando IOException através da clausula throws do main apenas para facilitar o exemplo. Caso a exception ocorra, a JVM vai parar, mostrando a stacktrace. Esta não é uma boa prática em uma aplicação real: trate suas exceptions para sua aplicação poder abortar elegantemente.

InputStream tem diversas outras filhas, como ObjectInputStream, AudioInputStream, ByteArrayInputStream, entre outras.

Para recuperar um caractere, precisamos traduzir os bytes com o encoding dado para o respectivo código unicode, isso pode usar um ou mais bytes. Escrever esse decodificador é muito complicado, quem faz isso por você é a classe InputStreamReader.

O construtor de InputStreamReader pode receber o encoding a ser utilizado como parâmetro, se desejado, tal como UTF-8 ou ISO-8859-1.
InputStreamReader é filha da classe abstrata Reader, que possui diversas outras filhas – são classes que manipulam chars.

Apesar da classe abstrata Reader já ajudar no trabalho de manipulação de caracteres, ainda seria difícil pegar uma String. A classe BufferedReader é um Reader que recebe outro Reader pelo construtor e concatena os diversos chars para formar uma String através do método readLine:

Como o próprio nome diz, essa classe lê do Reader por pedaços (usando o buffer) para evitar realizar muitas chamadas ao sistema operacional. Você pode até configurar o tamanho do buffer pelo construtor.

É essa a composição de classes que está acontecendo:

 

Esse padrão de composição é bastante utilizado e conhecido. É o Decorator Pattern.

Aqui, lemos apenas a primeira linha do arquivo. O método readLine devolve a linha que foi lida e muda o cursor para a próxima linha. Caso ele chegue ao fim do Reader (no nosso caso, fim do arquivo), ele vai devolver null. Então, com um simples laço, podemos ler o arquivo por inteiro:

Lendo Strings do teclado

Com um passe de mágica, passamos a ler do teclado em vez de um arquivo, utilizando o System.in, que é uma referência a um InputStream o qual, por sua vez, lê da entrada padrão.

Apenas modificamos a quem a variável is está se referindo. Podemos receber argumentos do tipo InputStream e ter esse tipo de abstração: não importa exatamente de onde estamos lendo esse punhado de bytes, desde que a gente receba a informação que estamos querendo. Como na figura:


Repare que a ponta da direita poderia ser qualquer InputStream, seja ObjectInputStream, AudioInputStream, ByteArrayInputStream, ou a nossa FileInputStream. Polimorfismo! Ou você mesmo pode criar uma filha de InputStream, se desejar.

Por isso é muito comum métodos receberem e retornarem InputStream, em vez de suas filhas específicas. Com isso, elas desacoplam as informações e escondem a implementação, facilitando a mudança e manutenção do código. Repare que isso vai ao encontro de tudo o que aprendemos durante os capítulos que apresentaram classes abstratas, interfaces, polimorfismo e encapsulamento.

A analogia para a escrita: OutputStream

Como você pode imaginar, escrever em um arquivo é o mesmo processo:

 

Lembre-se de dar refresh (clique da direita no nome do projeto, refresh) no seu projeto do Eclipse para que o arquivo criado apareça. O FileOutputStream pode receber um booleano como segundo parâmetro, para indicar se você quer reescrever o arquivo ou manter o que já estava escrito (append).

O método write do BufferedWriter não insere o(s) caractere(s) de quebra de linha. Para isso, você pode chamar o método newLine.

Fechando o arquivo com o finally e o try-with-resources

É importante sempre fechar o arquivo. Você pode fazer isso chamando diretamente o método close do FileInputStream/OutputStream, ou ainda chamando o close do BufferedReader/Writer. Nesse último caso, o close será cascateado para os objetos os quais o BufferedReader/Writer utiliza para realizar a leitura/escrita, além dele fazer o flush dos buffers no caso da escrita.

É comum e fundamental que o close esteja dentro de um bloco finally. Se um arquivo for esquecido aberto e a referência para ele for perdida, pode ser que ele seja fechado pelo garbage collector, que veremos mais a frente, por causa do finalize. Mas não é bom você se prender a isso. Se você esquecer de fechar o arquivo, no caso de um programa minúsculo como esse, o programa vai terminar antes que o tal do garbage collector te ajude, resultando em um arquivo não escrito (os bytes ficaram no buffer do BufferedWriter). Problemas similares podem acontecer com leitores que não forem fechados.

No Java 7 há a estrutura try-with-resources, que já fará o finally cuidar dos recursos declarados dentro do try(), invocando close. Pra isso, os recursos devem implementar a interface java.lang.AutoCloseable, que é o caso dos Readers, Writers e Streams estudados aqui:

Uma maneira mais fácil: Scanner e PrintStream

A partir do Java 5, temos a classe java.util.Scanner, que facilita bastante o trabalho de ler de um InputStream. Além disso, a classe PrintStream possui um construtor que já recebe o nome de um arquivo como argumento. Dessa forma, a leitura do teclado com saída para um arquivo ficou muito simples:

Nenhum dos métodos lança IOException: PrintStream lança FileNotFoundException se você o construir passando uma String. Essa exceção é filha de IOException e indica que o arquivo não foi encontrado. O Scanner considerará que chegou ao fim se uma IOException for lançada, mas o PrintStream simplesmente engole exceptions desse tipo. Ambos possuem métodos para você verificar se algum problema ocorreu.

A classe Scanner é do pacote java.util. Ela possui métodos muito úteis para trabalhar com Strings, em especial, diversos métodos já preparados para pegar números e palavras já formatadas através de expressões regulares. Fica fácil parsear um arquivo com qualquer formato dado.

Um pouco mais…

Existem duas classes chamadas java.io.FileReader e java.io.FileWriter. Elas são atalhos para a leitura e escrita de arquivos.

O do { .. } while(condicao); é uma alternativa para se construir um laço. Pesquise-o e utilize-o no código para ler um arquivo, ele vai ficar mais sucinto (você não precisará ler a primeira linha fora do laço).

Integer e classes wrappers (box)

Anteriormente, vimos que conseguimos ler e escrever dados em um arquivo no Java utilizando a classe Scanner. Por padrão, quando fazemos essas operações, estamos trabalhando sempre com os dados em forma de String. Mas e se precisássemos ler ou escrever números inteiros em um arquivo? Como faríamos para transformar esses números em String e vice-versa?

Cuidado! Usamos aqui o termo “transformar”, porém o que ocorre não é uma transformação entre os tipos e sim uma forma de conseguirmos um String dado um int e vice-versa. O jeito mais simples de transformar um número em String é concatená-lo da seguinte maneira:

Para formatar o número de uma maneira diferente, com vírgula e número de casas decimais devemos utilizar outras classes de ajuda (NumberFormat, Formatter).

Para transformar uma String em número, utilizamos as classes de ajuda para os tipos primitivos correspondentes. Por exemplo, para transformar a String s em um número inteiro utilizamos o método estático da classe Integer:

As classes Double, Short, Long, Float etc contêm o mesmo tipo de método, como parseDouble e parseFloat que retornam um double e float respectivamente.

Essas classes também são muito utilizadas para fazer o wrapping (embrulho) de tipos primitivos como objetos, pois referências e tipos primitivos são incompatíveis. Imagine que precisamos passar como argumento um inteiro para o nosso guardador de objetos. Um inteiro não é um Object, como fazer?

E, dado um Integer, podemos pegar o int que está dentro dele (desembrulhá-lo):

Autoboxing no Java 5.0

Esse processo de wrapping e unwrapping é entediante. O Java 5.0 em diante traz um recurso chamado de autoboxing, que faz isso sozinho para você, custando legibilidade:

No Java 1.4 esse código é inválido. No Java 5.0 em diante ele compila perfeitamente. É importante ressaltar que isso não quer dizer que tipos primitivos e referências sejam do mesmo tipo, isso é simplesmente um “açúcar sintático” (syntax sugar) para facilitar a codificação.

Você pode fazer todos os tipos de operações matemáticas com os wrappers, porém corre o risco de tomar um NullPointerException.

Você pode fazer o autoboxing diretamente para Object também, possibilitando passar um tipo primitivo para um método que receber Object como argumento:

Para saber mais: java.lang.Math

Na classe Math, existe uma série de métodos estáticos que fazem operações com números como, por exemplo, arredondar(round), tirar o valor absoluto (abs), tirar a raiz(sqrt), calcular o seno(sin) e outros.

Consulte a documentação para ver a grande quantidade de métodos diferentes.

No Java 5.0, podemos tirar proveito do import static aqui:

Isso elimina a necessidade de usar o nome da classe, sob o custo de legibilidade:

  • 16. Finalizando