Visão Geral do Java NIO
Introdução ao Java NIO e NIO.2 — canais, buffers, seletores e o pacote java.nio.file.
Os quinze capítulos anteriores a este trataram de java.io — streams, Reader/Writer, File, Serializable. Essa API era a I/O original do Java e ainda é amplamente utilizada. NIO é a família de APIs que o Java adicionou posteriormente para cobrir o que o java.io não conseguia. Ela vem em duas partes que compartilham um prefixo de pacote e pouco mais:
- NIO (Java 1.4, 2002) —
java.nio.*— canais, buffers, seletores. Uma forma diferente de I/O: baseada em byte-buffer, opcionalmente não bloqueante, projetada para servidores de alta taxa de transferência. - NIO.2 (Java 7, 2011) —
java.nio.file.*— as classesPath,Files,FileSystemeWatchService. Um substituto mais amigável parajava.io.Filee um lugar para funcionalidades do sistema de arquivos que ojava.ionunca teve (links simbólicos, atributos estendidos, I/O de arquivo assíncrono, monitoramento de diretórios).
Você já vem usando partes do NIO.2 desde o início desta parte: Path, Files.newBufferedReader, Files.newInputStream são todos do java.nio.file. Este capítulo oferece uma visão mais ampla e mostra onde essas peças se encaixam, e para que serve o restante do pacote.
Stream vs canal: duas formas diferentes
InputStream.read() retorna um byte. OutputStream.write(int) escreve um byte. O modelo mental é um pipe byte por vez. Decoradores com buffer tornam isso rápido, mas a abstração é sequencial e unidirecional.
Um canal (java.nio.channels.Channel) é bidirecional, orientado a byte-buffer, e suporta operações que o InputStream não consegue expressar:
- Ler em e escrever a partir de um
ByteBuffer— não umbyte[]. - Mapear em memória uma região de arquivo na RAM e lê-la/escrevê-la como um buffer.
- Dispersar uma leitura em múltiplos buffers (cabeçalho → um, payload → outro).
- Reunir uma escrita de múltiplos buffers (um único
write()produz uma saída contígua). - Marcar um canal como não bloqueante e deixar um
Selectormultiplexar milhares deles em uma única thread.
O custo é a verbosidade. O código de canal lê e escreve através de um ByteBuffer com chamadas explícitas de flip() e position(); o java.io oculta tudo isso atrás de read(byte[]). Para leitura típica de arquivos, prefira as APIs java.io/Files. Use canais quando precisar de um dos recursos exclusivos deles.
// channel-shaped read into a 1 KB buffer
try (FileChannel ch = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(1024);
int n = ch.read(buf); // fills the buffer; updates position
buf.flip(); // switch to "read what was just written"
while (buf.hasRemaining()) {
process(buf.get());
}
}O passo flip() é o momento em que as pessoas percebem que ByteBuffer tem sua própria pequena máquina de estados.
ByteBuffer: position, limit, capacity
Um ByteBuffer é um byte[] de tamanho fixo (ou um pedaço de memória fora do heap) mais três índices:
position— o próximo byte a ser lido ou escrito.limit— o índice após o último byte que você pode tocar.capacity— o tamanho fixo do buffer; não pode mudar.
0 ─────── position ─────── limit ─────── capacity
(consumed) (active region) (untouchable / empty)O buffer está em um de dois modos por convenção:
- Modo de escrita (padrão): você faz
put(byte)nele.positionavança;limit == capacity. - Modo de leitura: você extrai bytes com
get().positionavança;limitestá onde você parou de escrever.
flip() muda do modo de escrita para leitura: define limit = position (marca onde os dados terminam) e redefine position = 0 (começa a ler do início). clear() volta para o modo de escrita (position = 0, limit = capacity). Erros aqui são a fonte mais comum da frustração com "li zero bytes; por quê?".
Buffers fora do heap (ByteBuffer.allocateDirect(n)) contornam o heap da JVM e permitem que o SO leia/escreva neles diretamente sem uma cópia extra. São mais lentos de alocar, mais rápidos para I/O, e a escolha certa apenas para código de I/O no caminho crítico.
Seletores: uma thread, muitos canais
Antes das virtual threads (Java 21), atender milhares de conexões de rede concorrentes em Java significava ou milhares de threads do SO (uma por conexão — caro) ou uma única thread multiplexando com um Selector:
Selector sel = Selector.open();
serverChannel.register(sel, SelectionKey.OP_ACCEPT);
while (true) {
sel.select(); // blocks until any channel is ready
for (SelectionKey k : sel.selectedKeys()) {
if (k.isAcceptable()) accept(k);
if (k.isReadable()) read(k);
}
}O SO notifica a JVM quando qualquer canal registrado pode avançar; a JVM entrega o conjunto pronto; você faz uma leitura ou escrita não bloqueante e volta ao select(). O código de framework por baixo do Netty, gRPC e Spring WebFlux tem essa forma.
Com virtual threads (Thread.startVirtualThread(...)), o padrão mais simples de "uma thread por requisição" escala para a mesma quantidade de conexões sem a coreografia do Selector — as virtual threads ficam paradas em I/O bloqueante praticamente de graça. Para código de aplicação novo no Java 21+, o loop de seletor é cada vez mais uma preocupação de biblioteca; você normalmente não o escreve à mão. Para código de biblioteca e JVMs anteriores ao Loom, é o padrão habitual.
java.nio.file: a API moderna de arquivos
Esta é a metade do NIO que você vai usar no dia a dia. Ela substitui java.io.File e a maior parte das partes relacionadas a arquivos do java.io:
java.io | java.nio.file | Por que a substituição |
|---|---|---|
File | Path | Imutável, agnóstico ao SO, sem métodos de I/O embutidos |
File.list() | Files.list(Path), Files.walk(Path) | Stream<Path>; fechável; respeita links simbólicos |
new FileInputStream(...) | Files.newInputStream(path) | Variantes com suporte a charset para texto; uma API de abertura consistente |
file.delete() retornando false em caso de falha | Files.delete(path) lançando IOException | Falhas são visíveis, não silenciosas |
| sem equivalente | Files.walkFileTree, WatchService, API de link simbólico, views de atributos de arquivo | Capacidades que o java.io nunca teve |
Os próximos dois capítulos cobrem Path e Files em profundidade. A regra geral: para trabalho com arquivos no Java 2024+, use java.nio.file. java.io.File ainda existe porque código antigo o usa, mas código novo deve padronizar com Path.
Um exemplo prático: ida e volta de um arquivo via canal e buffer
O programa abaixo copia um pequeno arquivo de texto usando canais e buffers para tornar position/limit/flip concretos. Ele abre a origem como um FileChannel, lê em um ByteBuffer, faz flip, escreve em um FileChannel de destino e imprime o estado do buffer em cada passo para que você veja como os índices se movem.
O que tirar da execução:
- O loop imprimiu o estado do buffer em cada passo. Após um
read(),positionera o número de bytes lidos elimitainda eracapacity— isso é o "modo de escrita": ainda há espaço no final. Apósflip(),position = 0elimit = o número recém-lido— isso é o "modo de leitura": os bytes ficam entre 0 elimit. Os dois índices codificam "onde os dados estão" sem copiá-los. - O buffer tinha 16 bytes; o arquivo tinha 44. O loop rodou três iterações: 16, 16, 12. Assim que o buffer ficou vazio (após
writeter esvaziado),clear()o redefiniu de volta ao "modo de escrita" para que o próximoread()pudesse reenchê-lo. Este é o padrão de canal em miniatura: reencher, flip, esvaziar, clear, repetir. transferTofez a mesma cópia em uma linha sem nenhumByteBufferenvolvido. No Linux, isso mapeia para uma única syscallsendfile()— os bytes percorrem de kernel a kernel sem cruzar a JVM. Quando você está movendo dados entre dois canais e não precisa examiná-los, esta é a ferramenta certa.- Observe que o arquivo de origem foi criado com
Files.writeStringe o destino foi lido de volta comFiles.readString— ambos são one-liners dojava.nio.fileque ocultam completamente canais e buffers. O loop de canal detalhado no meio é o que você escreveria apenas quando precisar de acesso direto ao buffer (análise binária customizada, mapeamento de memória, scatter/gather). Para "copiar um arquivo",transferToouFiles.copyé mais curto e pelo menos tão rápido. - O construtor
FileChannel.open(path, OPTION)é o paralelo aFiles.newInputStream(path). O enumStandardOpenOption(READ, WRITE, CREATE, APPEND, TRUNCATE_EXISTING, ...) controla o comportamento de abertura — há apenas um lugar para consultar. Esse enum de opções de abertura aparece repetidamente no próximo capítulo.
O que vem a seguir
Este capítulo nomeou as peças — canais, buffers, seletores, java.nio.file. O próximo capítulo, Classe Java Path, aprofunda a mais amigável dessas peças — Path — e os métodos (resolve, relativize, normalize) que você usará toda vez que trabalhar com um caminho do sistema de arquivos.