Trabalhando com arquivos do Java IO ao NIO 2
Postado em 10. ago, 2011 por Mário Amaral em Inovação, Java
Durante anos, trabalhar com arquivos em Java foi muito trabalhoso. Precisávamos conhecer e interagir com diversas classes do pacote java.io a fim de realizar tarefas simples, como ler um arquivo ou simplesmente copiá-los de uma pasta para outra. Alguns projetos open source sugiram para facilitar essas tarefas, como o Commons-IO da Apache.
Conforme a linguagem foi evoluindo, estas tarefas foram se tornando mais fáceis de serem realizadas sem a necessidade de bibliotecas externas, graças aos recursos incorporados na API padrão. Vamos acompanhar como foi parte dessa evolução, usando o exemplo de copiar uma árvore de diretórios.
A seguir, vemos a árvore de diretórios que iremos copiar:
É uma arvore de arquivos comum. Antes de copiar a árvore inteira, vamos ver o código para copiar um arquivo sozinho:
// Arquivos que iremos copiar
File origem = new File("/home/caelum/Java/vraptor.zip");
File destino = new File("/home/caelum/Java/vraptor-copia.zip");
// abrimos os streams para leitura/escrita
FileInputStream fis = new FileInputStream(origem);
FileOutputStream fos = new FileOutputStream(destino);
// Obtém os canais por onde lemos/escrevemos nos arquivos
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
// copia todos o conteúdo do canal de entrada para o canal de saída
outChannel.transferFrom(inChannel, 0, inChannel.size());
// fecha os streams/channels usados...
Cada FileInputStream/FileOutputStream recebe um objeto do tipo File, que representa o local do arquivo no disco. Após isso criamos canais por onde copiaremos o conteudo do arquivo de origem para o de destino. Usar FileChannels para realizar a cópia pode ser bem mais eficiente do que se lessemos/escrevessemos utilizando os antigos streams diretamente. Essa API não-blocante foi adicionada ao Java 1.4, em 2001.
Para copiar a estrutura de diretórios é muito mais simples, basta chamar o método mkdirs da classe File, que cria o diretório e toda a árvore necessária:
File novoDiretorio = new File("/home/caelum-copia/Java");
novoDiretorio.mkdirs(); //cria o diretório "caelum-copia" e o diretório "Java" dentro da pasta /home/
Mas isso só cria o diretório vazio, sem nenhum arquivo dentro. Para copiar o conteúdo, primeiro precisamos saber o que há dentro dele, para isso usamos o método File.listFiles, que nos retorna um array com todas as entradas do diretório, tanto arquivos quanto subdiretórios:
File raiz = new File("/home/mario/Documents/caelum/Java");
File[] files = raiz.listFiles();
for (File file : files) {
// copia os arquivos / subdiretórios
}
Precisamos repetir esta operação para cada subdiretório encontrado. A melhor solução neste caso é realizar uma invocação recursiva para copiar o conteúdo de cada subdiretório encontrado.
public void copiarArquivos(File origem, File destino) {
// verifica se estamos tentando copiar um diretório
if (origem.isDirectory()) {
File[] files = origem.listFiles();
for (File file : files) {
// invocacao recursiva de cada item de dentro do diretorio
copiaArquivos(file, destinoEquivalente);
}
} else {
// copia o arquivo usando os streams/channels
}
}
Juntando tudo que vimos até aqui, temos um método de cópia de diretórios. Não é um código simples, pois além de usar recursão para realizar a cópia dos subdiretórios, ainda envolve várias diferentes classes na cópia dos arquivos. Também deveria ser refatorado para 2 ou 3 métodos.
Existem diversos outros problemas com as antigas classes do java.io. Podemos pegar como exemplo a classe File, que tem alguns dos métodos para manipulação de arquivos, mas a maioria desses métodos não lança exceptions quando falham, e podem funcionar diferentemente em cada sistema operacional.
Vamos tentar deletar um diretório que possui arquivos dentro. O código segue abaixo:
File root = new File("/home/caelum");
boolean deletou = root.delete(); // retorna false...
Sabemos que o diretório não foi removido pelo retorno do método, mas é dificil saber o motivo, se é um problema de acesso, se o diretório que não está vazio, se não existe, etc., ficando para o programador a responsabilidade de descobrir o que aconteceu.
A partir do Java 7, foi introduzida uma nova API no pacote java.nio.file, visando reduzir a complexidade das operações realizadas em arquivos. A nova classe Files possui diversos métodos utilitários para manipular arquivos, lançando exceptions quando ocorre algum erro na operação, possibilitando ao desenvolvedor entender de forma rápida o motivo da falha. Os métodos de Files utilizam a classe Path, uma nova interface utilizada para representar entradas de arquivos/diretórios no sistema operacional.
O código para deletar o diretório ficaria da seguinte maneira:
Path rootPath = Paths.get("/home/caelum");
Files.delete(rootPath); // o que acontece aqui?
A classe Paths é uma fábrica de Path. O resultado desse código, é uma exception:
Exception in thread "main" java.nio.file.DirectoryNotEmptyException: /home/caelum
Vamos alterar nosso método copiaArquivos para utilizar as novas classes do Java 7:
public void copiarArquivos(Path origem, Path destino) throws IOException {
// se é um diretório, tentamos criar. se já existir, não tem problema.
if(Files.isDirectory(origem)){
Files.createDirectories(destino);
// listamos todas as entradas do diretório
DirectoryStream<Path> entradas = Files.newDirectoryStream(origem);
for (Path entrada : entradas) {
// para cada entrada, achamos o arquivo equivalente dentro de cada arvore
Path novaOrigem = origem.resolve(entrada.getFileName());
Path novoDestino = destino.resolve(entrada.getFileName());
// invoca o metodo de maneira recursiva
copiarArquivos(novaOrigem, novoDestino);
}
} else {
// copiamos o arquivo
Files.copy(origem, destino);
}
}
A estrutura do algoritmo é a mesma utilizada anteriormente, mas agora o número de classes envolvidas é muito menor. Para copiar um arquivo, apenas chamamos o método Files.copy, não precisando mais trabalhar com streams e channels nesses casos.
Ainda é trabalhoso percorrer um diretório inteiro dessa maneira, mas podemos utilizar a nova interface FileVisitor, uma implementação do pattern Visitor, para percorrer toda a árvore de diretórios de maneira mais simples. Os métodos dessa interface são
preVisitDirectoryChamado antes de entrar em um diretório
postVisitDirectoryChamado depois de passar por todos arquivos/subdiretórios de um diretótio
visitFileChamado para cada arquivo
visitFileFailedChamado se acontecer alguma exception em algum dos outros métodos
Todos os métodos retornam FileVisitResult, um enum com os as opções para continuar ou não percorendo a árvore de diretórios.
Vamos então implementar um FileVisitor para copiar nosso diretório. Nem sempre precisamos de todos os métodos da interface; neste caso, podemos estender a classe SimpleFileVisitor e apenas sobreescrever os métodos que realmente necessitamos:
public class CopiadorDeArquivos extends SimpleFileVisitor {
private Path origem;
private Path destino;
public CopiadorDeArquivos(Path origem, Path destino) {
this.origem = origem;
this.destino = destino;
}
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
copiaPath(dir);
return FileVisitResult.CONTINUE;
}
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
copiaPath(file);
return FileVisitResult.CONTINUE;
}
private void copiaPath(Path entrada) throws IOException {
// encontra o caminho equivalente na árvore de cópia
Path novoDiretorio = destino.resolve(origem.relativize(entrada));
Files.copy(entrada, novoDiretorio);
}
}
Na classe acima, usamos apenas dois métodos, o visitFile, chamado para copiar cada arquivo existente na árvore, e o preVisitDirectory, chamado para criar o diretório. Para executar a cópia, faríamos:
Path origem = Paths.get("/home/caelum");
Path destino = Paths.get("/home/caelum-copia");
Files.walkFileTree(origem, new CopiadorDeArquivos(origem, destino));
O método walkFileTree recebe o diretório que será percorido, e um FileVisitor, que irá passar por todos os arquivos.
Apesar de ter tornado alguma operações mais simples, essa nova API de arquivos possui mais métodos estáticos e a maioria recebe uma instância de Path, separando comportamento e dados. Talvez com o futuro Java 8, as closures e defender methods tornarão a interface Path mais amigável, sem depender tanto de métodos estáticos.
Além das classes apresentadas, ainda tivemos outros acréscimos na API de NIO, como leitura/escrita de streams de forma assíncrona, serviços para monitorar alterações em diretórios e métodos para ler metadados de arquivos de maneira simplificadas. Essas novidades são conhecidas como NIO2. Com o passar das versões, o Java traz cada vez mais para suas APIs internas ferramentas necessárias para o dia a dia.

ASSINE NOSSO RSS
Roger Almeida
17. ago, 2011
Oi, olha só, tem um errinho ai. No primeiro código você declara duas vezes a mesma variável:
// Arquivos que iremos copiar
File origem = new File(“/home/caelum/Java/vraptor.zip”);
File origem = new File(“/home/caelum/Java/vraptor-copia.zip”);
Mais abaixo você faz referência a uma variável chamada destino. Acho que foi um pequeno typo ai.
Bruno Laturner
17. ago, 2011
Às vezes me pergunto em alguns casos básicos se não seria melhor detectar o SO ou os comandos nele instalados, e chamar um prompt com ‘cp -R’, e deixar a manipulação por programação somente para casos que precisamos de um controle fino.
Bruno Laturner
17. ago, 2011
E complementado o que disse acima, deixar essa parte “suja” para bibliotecas prontas, como a dica do CommonsIO no começo do tópico.
http://commons.apache.org/io/apidocs/org/apache/commons/io/FileUtils.html#copyDirectory(java.io.File, java.io.File)
Paulo Silveira
17. ago, 2011
obrigado Roger! corrigido.
Paulo Silveira
17. ago, 2011
Oi Bruno. Com essa api nova os truques sujos de Runtime.exec() nao sao mais necessarios.
Infelizmente a api velha nao poderia ser modificada, pois algumas coisas passariam a funcionar diferente da forma esperada… o velho problema de resolver um bug que acabou virando feature.
Leandro Moreira
17. ago, 2011
“Durante anos, trabalhar com arquivos em Java foi muito trabalhoso. Precisávamos conhecer e interagir com diversas classes do pacote java.io a fim de realizar tarefas simples”
E durante esse tempo vi também grandes desenvolvedores regorgizando sobre essa complexidade dizendo que era devido ao OO máximo do Java.
Paulo Silveira
17. ago, 2011
Pois é Leandro, hoje muita coisa mudou. Já se pensa em sacrificar um pouco algumas práticas antes consideradas boas, balanceando.
Daniel
18. ago, 2011
Ola Leandro e Paulo,
Tambem ouvi durante anos, alguns desenvolvedores reclamarem que os problemas da API de arquivos do Java, eram devido as praticas do OO, mas devo discordar um pouco.
Se lembrarmos de tratar de orientação a objetos e divisao de responsabilidades, a muito tempo, coisas basicas como mover arquivos, copiar, criar, etc… nao existiam e saiam do contexto da responsabilidade de um objeto. Um objeto File, (ao meu entendimento), deve implementar suas açoes de mover, deletar, copiar e etc. Devido a isso nao culpo a OO, pelo trabalho excessivo que tinhamos, e sim uma má implementação inicial da API.
Tambem ja escrevi diversos codigos que de maneira muito feia, chamava o classico Runtime.exec(). E com todo respeito, pra quem usa, mas esse é o tipo de comando que só deve ser usado em casos extremos e com muitas exclamações no comentario e documentação. Pois causa uma dependencia absurda dos comandos utilizados, alem de dificultar a manutenção.
Anderson
18. ago, 2011
De fato. Aquele monte de Decorator´s me incomodava.
Otávio
26. ago, 2011
Hmm, esse post me inspirou a fazer uns testes. Peguei aqui um sistema que faz uso de java.io e escrevi com java.nio, e realmente ficou bem bacana.
Só achei bem estranho ficar toda hora chamando métodos estáticos como Files.getSize ao invés de chamar direto .size na instância criada de Path.
O interessante que agora a API ganhou uns extras como a possibilidade de pegar atributos de segurança e outros metadados dos arquivos. Também é possível saber o content-type de um arquivo através do probeContentType. Isso é bem útil para componentes como o File Download do VRaptor.
Na página da documentação que você passou tem uma página bem legal sobre o legado java.io, comparando ambas APIs e mostrando métodos similares.
Vicente Paulo Maciel
09. mar, 2013
Ocorreu no método copiarArquivos um erro na linha:
for (Path entrada : entradas ) {
Error(207,29): incompatible types
Não estou conseguindo resolver o erro.
Vicente Paulo Maciel
10. mar, 2013
resolvi o problema da linha substituindo por:
DirectoryStream entradas = Files.newDirectoryStream(origem);
Mário Amaral
10. mar, 2013
Oi Vicente
Para funcionar, basta alterar a linha de cima por :
DirectoryStream<Path> entradas = Files.newDirectoryStream(origem);Se não indicar o tipo genérico o iterator não devolve o tipo correto.
Já corrigi no post, muito obrigado!
Emanuel Lima
15. abr, 2013
Olá, esses métodos funcionariam se utilizássemos em um programa cliente-servidor usando sockets TCP. Seria semelhante a um servidor FTP. Nesse caso como ficaria a implementação da copia de arquivos?