Java IO, Java NIO e NIO.2: Quando Utilizar?

--

Ao trabalhar com I/O em Java, nos deparamos com uma grande variedade de classes e pacotes a nossa disposição. O sistema original de I/O do Java foi lançado nas primeiras versões do JDK no pacote java.io. Mais tarde, surgiu o subsistema NIO (New I/O) no JDK 1.4, trazendo novos pacotes e uma nova API para executar as operações de entrada e saída. Mais recentemente no JDK 7 o Java NIO foi extendido, ganhando novos pacotes e recursos. Essas extensões foram tão significativas que essa versão é conhecida como NIO.2.

Essa variedade de opções acaba por confundir muitos desenvolvedores. Afinal, qual conjunto de classes eu devo utilizar? O NIO veio para substituir o IO ou é apenas um complemento? O que eu devo levar em consideração na hora de escolher? Nesse artigo eu pretendo discutir as características e diferenças entre os sistemas de I/O e oferecer um guia para orientar a escolha de cada API. Eu assumo que o leitor possui uma compreensão básica das API’s de I/O do Java e já as utilizou em algum momento.

Java IO

O pacote java.io contém o sistema de I/O original do Java. Nesse sistema, as operações de entrada e saída são realizadas com a utilização de fluxos (streams). Um fluxo é uma entidade associada a um dispositivo de I/O que enxerga esse dispositivo como uma sequência de bytes ou caracteres, que só podem ser lidos/escritos de forma sequencial. Os fluxos em Java são divididos em fluxos de entrada e fluxos de saída. Ou seja, um fluxo de entrada é capaz de ler os dados de um dispositivo de entrada sequencialmente, um byte por vez, sem armazená-los internamente (a não ser que seja um fluxo específico para buffer, como um BufferedInputStream). Analogamente, um fluxo de saída escreve sequencialmente no dispositivo ao qual está associado, um byte de cada vez.

Vantagens

A principal vantagem do I/O orientando a fluxos é a sua simplicidade. Como um fluxo possui basicamente a capacidade de ler ou escrever um byte por vez, de maneira sequencial, isso implica em uma API enxuta e de fácil aprendizado. É um modelo fácil de entender e utilizar. Além disso, esse mesmo modelo é utilizado para todos os tipos de fluxo do Java IO, seja ele referente a um arquivo, a uma conexão de rede ou a uma área de memória.

Desvantagens

Por ser um modelo simples, o I/O orientado a fluxos possui algumas limitações. As principais são: a falta de flexibilidade para manipular os dados de maneira não sequencial, e a baixa escalabilidade (capacidade de lidar de maneira eficiente com muitas operações de I/O simultaneas).

Por exemplo, se uma aplicação necessita fazer um processamento do conteúdo completo lido de um dispositivo de entrada, cabe ao programador armazenar os bytes ou caracteres lidos em um array ou outra estrutura qualquer e gerencia-los manualmente, já que as classes de fluxo normalmente não permitem “voltar” nos bytes já lidos/escritos. Já a baixa escalabilidade tem relação com o fato das operações de I/O serem bloqueantes: ao realizar uma operação de leitura ou escrita, a thread atual precisa esperar a operação concluir para continuar sua execução. Isso implica que uma aplicação que necessita realizar muitas operações de I/O simultaneamente (como um servidor) deverá necessariamente realizá-las em threads separadas, o que pode ter um custo computacional elevado.

Quando utilizar

Para muitos tipos de aplicação, o I/O baseado em fluxos continua sendo uma opção simples e elegante para se trabalhar. Se a aplicação em questão não faz uso intenso de I/O de forma concorrente, com milhares de threads executando simultaneamente, provavelmente o Java IO é bom o bastante para os seus requisitos.

Java NIO

O Java NIO foi adicionado no JDK 1.4 nos pacotes java.nio, java.nio.channels e java.nio.charset. Ao invés de trabalhar com fluxos, o NIO utiliza duas novas abstrações para executar as operações de I/O: Buffers e Canais (Channels). O objetivo do NIO não é substituir o Java IO, mas sim complementá-lo ao oferecer soluções para as limitações apontadas anteriormente.

Vantagens

A maior vantagem do NIO talvez seja a sua capacidade de realizar I/O não bloqueante, algo que não era possível na API mais antiga. Isso significa que a aplicação é capaz de gerenciar várias operações de I/O em uma única thread, sem precisar esperar pela finalização de cada operação, o que é essencial para a escalabilidade de aplicações como servidores. A implementação do NIO também é mais próxima do sistema operacional do que o Java IO, o que pode significar um menor overhead nas operações de I/O e consequentemente maior desempenho, caso a sua aplicação faça uso intensivo de entrada e saída.

Desvantagens

A flexibilidade e escalabilidade do NIO tem um custo bem evidente: complexidade. Comparado com o Java IO, a API do NIO é claramente mais complexa e difícil de usar. Embora seja possível realizar operações simples de I/O utilizando quase que exclusivamente o NIO, o código resultante é quase sempre maior, mais confuso e difícil de manter.

Quando utilizar

A preferencia pelo NIO geralmente deve ocorrer quando a sua aplicação possui requisitos bem específicos, tais como maior desempenho e escalabilidade frente a utilização massiva de operações de IO e multithreading. Se a sua aplicação faz uso simples de entrada e saída (por exemplo, ler/escrever poucos arquivos em um número reduzido de threads) a complexidade adicional simplesmente não compensa.

Java NIO.2

A partir do JDK 7 o NIO foi consideravelmente extendido, passando a contar com os pacotes java.nio.file e java.nio.file.attribute. Esses pacotes oferecem um suporte completo à manipulação de arquivos e diretórios, centralizados na classe Files e na interface Path.

Vantagens

Se o NIO adicionou complexidade até nas operações mais simples de I/O, o NIO.2 simplificou a maioria das operações envolvendo arquivos e diretórios. Os métodos estáticos da classe Files incluem por exemplo um método copy, o que reduz a operação de cópia de arquivos a uma única linha de código. Muitos outros métodos foram disponibilizados, permitindo criar e deletar arquivos e diretórios, trabalhar com links simbólicos, analisar diversos atributos do sistema de arquivos, etc.

Desvantagens

Levando em conta que o NIO.2 trouxe melhorias exclusivamente na parte da manipulação do sistema de arquivos, ele ainda possui o mesmo problema do NIO quando falamos de I/O não orientado a arquivos: complexidade.

Quando utilizar

Para operações de manipulação de arquivos e diretórios, o NIO.2 é o mais indicado. Os métodos estáticos fornecidos pela classe Files em conjunto com a interface Path oferecem um conjunto completo e consistente de funcionalidades para criar, copiar, deletar e analisar atributos de arquivos e diretórios.

Conclusão

De maneira geral, o NIO e o NIO.2 não tem a intenção de substituir a antiga API Java IO, mas sim de complementá-la, oferecendo uma alternativa escalável para aplicações que necessitam fazer uso intenso de I/O de maneira concorrente. No entanto, os novos recursos referentes a arquivos e diretórios do NIO.2 oferecem uma alternativa simples e mais completa do que a API original, devendo obter uma adoção cada vez maior em código novo.

--

--