W3docs

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 sink

Duas 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() first

write(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

ClasseO que encapsula
FileReader / FileWriterUm arquivo em disco, decodificado como texto.
CharArrayReader / CharArrayWriterUm char[] em memória.
StringReader / StringWriterUma String/StringBuilder em memória.
BufferedReader / BufferedWriterUma visão com buffer de outro Reader/Writer.
InputStreamReader / OutputStreamWriterClasses ponte: um Reader/Writer sobre um fluxo de bytes subjacente, com um Charset.
PrintWriterUm 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.

java— editable, runs on the server

O que tirar da execução:

  • O arquivo em disco (23 bytes) era maior do que content.length() (20). A String tem length() == 20 (contando cada \n e contando o emoji 🎉 como duas unidades de código UTF-16 — é isso que um char Java 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 Reader tratou 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, \r e \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. Troque ByteArrayInputStream por socket.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á.

Prática

Prática
Por que `new FileReader(path)` e `new FileWriter(path)` (sem argumento de charset) causam bugs do tipo 'funciona na minha máquina, quebrado no servidor'?
Por que `new FileReader(path)` e `new FileWriter(path)` (sem argumento de charset) causam bugs do tipo 'funciona na minha máquina, quebrado no servidor'?
Was this page helpful?