W3docs

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 classes Path, Files, FileSystem e WatchService. Um substituto mais amigável para java.io.File e um lugar para funcionalidades do sistema de arquivos que o java.io nunca 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 um byte[].
  • 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 Selector multiplexar 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. position avança; limit == capacity.
  • Modo de leitura: você extrai bytes com get(). position avança; limit está 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.iojava.nio.filePor que a substituição
FilePathImutá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 falhaFiles.delete(path) lançando IOExceptionFalhas são visíveis, não silenciosas
sem equivalenteFiles.walkFileTree, WatchService, API de link simbólico, views de atributos de arquivoCapacidades 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.

java— editable, runs on the server

O que tirar da execução:

  • O loop imprimiu o estado do buffer em cada passo. Após um read(), position era o número de bytes lidos e limit ainda era capacity — isso é o "modo de escrita": ainda há espaço no final. Após flip(), position = 0 e limit = o número recém-lido — isso é o "modo de leitura": os bytes ficam entre 0 e limit. 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 write ter esvaziado), clear() o redefiniu de volta ao "modo de escrita" para que o próximo read() pudesse reenchê-lo. Este é o padrão de canal em miniatura: reencher, flip, esvaziar, clear, repetir.
  • transferTo fez a mesma cópia em uma linha sem nenhum ByteBuffer envolvido. No Linux, isso mapeia para uma única syscall sendfile() — 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.writeString e o destino foi lido de volta com Files.readString — ambos são one-liners do java.nio.file que 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", transferTo ou Files.copy é mais curto e pelo menos tão rápido.
  • O construtor FileChannel.open(path, OPTION) é o paralelo a Files.newInputStream(path). O enum StandardOpenOption (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.

Prática

Prática
Você leu 10 bytes de um canal em um `ByteBuffer` de capacidade 1024. Você quer escrever esses 10 bytes em outro canal. O que você precisa fazer entre o `read()` e o `write()`?
Você leu 10 bytes de um canal em um `ByteBuffer` de capacidade 1024. Você quer escrever esses 10 bytes em outro canal. O que você precisa fazer entre o `read()` e o `write()`?
Was this page helpful?