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 CharSequenceStringBuilder 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?