Fluxos de Caracteres em Java
Leia e escreva texto em Java com Reader, Writer, FileReader, FileWriter e considerações sobre codificação de caracteres.
O capítulo anterior abordou os fluxos de bytes — a camada bruta onde tudo é byte. Essa camada é adequada para dados binários e inadequada para texto. Um caractere UTF-8 pode ocupar um, dois, três ou quatro bytes; o UTF-16 usa unidades de código de dois bytes com pares substitutos para tudo além do plano multilíngue básico; até mesmo o texto ASCII precisa de uma decisão de "isto é ASCII" em algum lugar. Chamar InputStream.read() em texto e fazer cast do resultado para char funciona apenas se você tiver sorte e o arquivo for de um byte por caractere — e no momento em que alguém escrever "é" ou "日" ou "🎉", a versão sortuda corrompe os dados.
A hierarquia de fluxos de caracteres existe para manter essa decodificação fora do seu código. Reader e Writer trabalham com char, não com byte. As classes ponte — InputStreamReader e OutputStreamWriter — recebem um Charset e fazem a conversão. Acerte o charset na ponte, e todas as camadas acima dela trabalham com texto decodificado.
O contrato do Reader
Reader é o espelho de InputStream, um par abstrato de métodos (read(char[], int, int) e close()) com conveniências por cima:
int read(); // next char as int 0..65535, or -1 at end
int read(char[] buf); // read up to buf.length chars; return count or -1
int read(char[] buf, int off, int len); // into a slice
String readLine(); // only on BufferedReader — not on Reader itself
long transferTo(Writer out); // Java 10+: pipe straight to a sinkDuas diferenças sutis em relação ao lado de bytes. Primeiro, a unidade é char (uma unidade de código UTF-16 de 16 bits), não byte. Segundo, read() retorna 0..65535 para uma unidade de código e -1 no fim do fluxo — o mesmo truque sentinela do InputStream, mas com um intervalo legal mais amplo.
Um char nem sempre é um "caractere" — caracteres fora do plano multilíngue básico (U+10000 e acima: a maioria dos emojis, escritas antigas) usam dois unidades de código UTF-16 (um par substituto). Se você dividir nos limites de char (por exemplo, lendo 100 chars de cada vez e processando em blocos), pode dividir um par substituto entre duas leituras. Para texto orientado a linhas isso raramente importa; para processamento a nível de caracteres de Unicode arbitrário, trabalhe com pontos de código (String.codePoints()).
O contrato do Writer
Writer espelha OutputStream:
void write(int c); // low 16 bits
void write(char[] buf);
void write(char[] buf, int off, int len);
void write(String s); // convenience — encodes a whole String
void write(String s, int off, int len);
Writer append(CharSequence csq); // chainable: w.append("a").append("b")
void flush();
void close(); // calls flush() firstwrite(String) é a conveniência que você mais usará: a maior parte das E/S de texto são um pequeno número de escritas grandes (um corpo JSON, um relatório gerado) em vez de saída caractere por caractere.
append existe para interoperabilidade com CharSequence — StringBuilder implementa CharSequence, então um Writer pode ser o destino de código que escreve em um ou outro dependendo de uma flag. É o mesmo método append que o próprio StringBuilder possui, via interface.
Fluxos de caracteres concretos
| Classe | O que encapsula |
|---|---|
FileReader / FileWriter | Um arquivo em disco, decodificado como texto. |
CharArrayReader / CharArrayWriter | Um char[] em memória. |
StringReader / StringWriter | Uma String/StringBuilder em memória. |
BufferedReader / BufferedWriter | Uma visão com buffer de outro Reader/Writer. |
InputStreamReader / OutputStreamWriter | Classes ponte: um Reader/Writer sobre um fluxo de bytes subjacente, com um Charset. |
PrintWriter | Um decorador de Writer que adiciona print, println e printf. |
As classes ponte são o ponto estrutural de toda a hierarquia. Todo fluxo de caracteres que se comunica com um arquivo, socket ou pipe é — por baixo — um fluxo de bytes mais um charset. FileReader é um wrapper fino em torno de InputStreamReader(new FileInputStream(...)); FileWriter da mesma forma em torno de OutputStreamWriter(new FileOutputStream(...)).
A armadilha do charset
O bug clássico de E/S em Java:
// WRONG in any code that might run on more than one machine
try (FileReader in = new FileReader("data.txt")) { ... }
try (FileWriter out = new FileWriter("data.txt")) { ... }Os construtores sem charset usam o charset padrão da JVM, que é determinado na inicialização a partir do locale do sistema operacional. Em um Mac de desenvolvedor é quase sempre UTF-8. Em um servidor Linux com locale C pode ser US-ASCII. No Windows com uma instalação em inglês é Cp1252. O bug "funciona no meu Mac, quebrado no servidor de produção" é exatamente esse construtor.
Passe um charset explicitamente:
// Right
try (FileReader in = new FileReader("data.txt", StandardCharsets.UTF_8)) { ... }
try (FileWriter out = new FileWriter("data.txt", StandardCharsets.UTF_8)) { ... }(As formas com dois argumentos recebendo um Charset foram adicionadas no Java 11. Antes disso, você precisava descer até as classes ponte — new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8) — e a linha de decorador encadeado é um dos motivos pelos quais Files.newBufferedReader(path) foi adicionado: ele usa UTF-8 por padrão desde o Java 18 e sempre foi charset-explícito antes disso.)
A API moderna do Files tornou esse padrão mais seguro:
String text = Files.readString(path); // UTF-8 by default (Java 18+)
BufferedReader r = Files.newBufferedReader(path); // UTF-8 by default (always was)Se você está começando do zero, use as fábricas do Files. Se você está mexendo em código legado com FileReader/FileWriter, a correção mais barata é adicionar o segundo argumento StandardCharsets.UTF_8.
As classes ponte diretamente
Você precisa de InputStreamReader e OutputStreamWriter sempre que a fonte não é um arquivo — um ZipEntry, um socket, o corpo de uma resposta HTTP, System.in, um fluxo encapsulado por Inflater — e você quer texto a partir disso:
// Read text from System.in as UTF-8
try (BufferedReader stdin = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
String line = stdin.readLine();
}
// Write the response of an HttpURLConnection as text
try (BufferedReader resp = new BufferedReader(
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
resp.lines().forEach(System.out::println);
}A forma é sempre a mesma: fluxo de bytes → InputStreamReader(stream, charset) → BufferedReader opcional → seu código.
Um exemplo prático: texto em três formas
O programa abaixo escreve um pequeno arquivo de texto UTF-8 contendo ASCII, caracteres acentuados e um emoji multi-byte, depois o lê de quatro maneiras: como uma String, caractere por caractere, linha por linha através de um BufferedReader e pelo construtor legado FileReader(charset). O exemplo também mostra a forma da classe ponte funcionando sobre um ByteArrayInputStream para que você possa ver onde Reader e InputStream se encontram.
O que tirar da execução:
- O arquivo em disco (23 bytes) era maior do que
content.length()(20). AStringtemlength() == 20(contando cada\ne contando o emoji 🎉 como duas unidades de código UTF-16 — é isso que umcharJava mede); o UTF-8 codifica o emoji como quatro bytes eécomo dois, então a contagem de bytes é maior. Em pontos de código, há apenas 19 — o emoji é um ponto de código mas dois chars. O mesmo texto lógico é um número em chars, outro em bytes, outro em pontos de código. Saber qual você quer dizer é metade dos bugs de charset. - O loop caractere por caractere remontou exatamente a mesma string. A API
Readertratou a decodificação UTF-8 para você: um único emoji aparece como duas chamadas(char) read()por causa dos substitutos UTF-16, mas você nunca precisou pensar sobre os limites de bytes. BufferedReader.readLine()retornou três linhas:hello,café,🎉 party. Esse é o vocabulário orientado a texto — linha por linha, ciente do terminador (trata\n,\re\r\n), e construído em cima da classe ponte. Toda chamada de API que este capítulo e o próximo fazem se reduz, em última análise, a "decodificar bytes através de um charset e servir caracteres."- O bloco direto
InputStreamReader(new ByteArrayInputStream(raw), UTF_8)mostra a forma estrutural: fonte de bytes por dentro, charset na ponte, API de caracteres por fora. TroqueByteArrayInputStreamporsocket.getInputStream()e o resto é idêntico — é por isso que clientes HTTP e JDBC convergem para o mesmo idioma. - O bloco final decodificou os mesmos bytes com o charset errado. O
éacentuado e o emoji saíram como lixo — o bug clássico de mojibake. Os bytes em disco estavam corretos; o charset na ponte estava errado. É por isso que fixar o charset explicitamente é o hábito mais útil em E/S de texto em Java.
O que vem a seguir
Tanto os fluxos de bytes quanto os de caracteres usam E/S um de cada vez por padrão, e em um fluxo de arquivo bruto cada chamada é uma syscall. O próximo capítulo, Fluxos com Buffer em Java, aborda os decoradores Buffered* — um buffer em memória entre seu código e o sistema operacional — e a API readLine() que reside lá.