Java PrintWriter
Escreva texto formatado em streams com a classe PrintWriter do Java — print, println, printf, format.
O capítulo sobre streams de caracteres apresentou Writer e seu método principal, write(String). Isso é suficiente para qualquer coisa, mas não é ergonômico — imprimir um número exige w.write(Integer.toString(n)), imprimir uma linha significa lembrar de acrescentar o terminador de linha manualmente, e a saída formatada requer String.format em cada chamada. PrintWriter é o decorator que corrige a ergonomia: ele adiciona print, println, printf e format sobre qualquer Writer ou OutputStream subjacente.
É o equivalente no lado de arquivos ao System.out que você usa desde o primeiro capítulo — mesma superfície de API, escrevendo em um arquivo em vez do console.
O que PrintWriter adiciona
void print(boolean | char | int | long | float | double | String | Object);
void println(...); // same overloads, plus the line terminator
PrintWriter printf(String format, Object... args); // String.format under the hood
PrintWriter format(String format, Object... args); // alias for printf
PrintWriter append(CharSequence s); // returns this (for chaining)Além dos métodos herdados de Writer (write, flush, close). O ponto são as sobrecargas tipadas: você pode escrever qualquer primitivo ou objeto diretamente e o PrintWriter chama String.valueOf para você.
try (PrintWriter w = new PrintWriter(Files.newBufferedWriter(path))) {
w.println("header");
w.printf("count = %d%n", 42);
w.printf("rate = %.2f%%%n", 0.875 * 100);
w.println(); // blank line
}O close() do try-with-resources libera o buffer; sem ele, a armadilha do buffer final descrita no capítulo sobre streams com buffer se aplica igualmente ao PrintWriter.
Construtores
Os mais úteis, em ordem de preferência:
PrintWriter(Path file, Charset charset); // Java 10+, opens the file with the charset
PrintWriter(Writer out); // wrap any Writer (typical: a BufferedWriter)
PrintWriter(Writer out, boolean autoFlush); // same, with autoFlush on println/printf
PrintWriter(OutputStream out, boolean autoFlush, Charset charset);O construtor Path + Charset é o mais simples para "abrir este arquivo e escrever nele":
try (PrintWriter w = new PrintWriter(path.toFile(), StandardCharsets.UTF_8)) {
w.println("hello");
}Ele abre o arquivo, envolve-o em um OutputStreamWriter com o charset fornecido, envolve isso em um BufferedWriter e lhe entrega um PrintWriter. A pilha de quatro camadas que você montava manualmente se reduz a uma linha.
Sempre passe um charset explícito. Os construtores sem charset — new PrintWriter("file.txt"), new PrintWriter(outputStream) — recorrem à codificação padrão da JVM, que é o mesmo risco de portabilidade descrito no capítulo sobre streams de caracteres. UTF-8 é o padrão correto.
O IOException engolido
PrintWriter difere de todos os outros Writer em um aspecto importante: ele não lança IOException. Nenhum de print, println, printf ou write o declara. Se uma chamada de I/O subjacente falhar, o PrintWriter engole a exceção e define um sinalizador interno de "erro".
Isso é conveniente — você pode escrever um longo bloco de chamadas println sem try/catch em cada uma — mas significa que uma escrita com falha é silenciosa. Você precisa perguntar:
if (w.checkError()) {
// something went wrong; the underlying IOException was swallowed
throw new IOException("write to " + path + " failed");
}checkError() é a única maneira de descobrir. Não há como recuperar o IOException original — quando você verifica, ele já foi perdido. Portanto:
- Para saída no console (o caso de uso do
System.out), engolir a exceção é aceitável: ninguém trata uma escrita com falha em um terminal. - Para arquivos onde uma escrita parcial é um problema real (um arquivo de log, um arquivo de salvamento, um relatório gerado), verifique
checkError()ao final do bloco, ou use umBufferedWritere chamewritevocê mesmo para que a exceção se propague.
Autoflush
O argumento autoFlush do construtor controla se cada chamada a println, printf ou format chama flush() depois. O padrão é desligado:
new PrintWriter(out, false) // explicit close/flush only
new PrintWriter(out, true) // flush after every println/printf/formatprint e write nunca fazem autoflush, mesmo com o sinalizador ativado — somente os métodos de linha e formato o fazem. É por isso que System.out.print("waiting...") pode ficar invisível enquanto o próximo cálculo é executado, e System.out.println("waiting...") aparece imediatamente.
Para arquivos, deixe o autoflush desligado e deixe close() (via try-with-resources) cuidar disso. Para um log interativo que você está monitorando, ative-o ou chame flush() após cada lote.
println e o separador de linha da plataforma
println() escreve System.lineSeparator() — \n no Unix e macOS, \r\n no Windows. O mesmo vale para o especificador %n no printf. Isso é uma funcionalidade para saída em terminal e um bug para arquivos de dados; a discussão no capítulo sobre streams com buffer (em BufferedWriter.newLine) aplica-se aqui da mesma forma:
w.printf("row,%d%n", 1); // platform-dependent terminator
w.printf("row,%d\n", 1); // portable \nQuando a saída será lida por qualquer coisa além da máquina local, escreva \n explicitamente.
Comparação com PrintStream
PrintStream é o irmão orientado a bytes do PrintWriter — mesma API, ancestral diferente. System.out e System.err são instâncias de PrintStream. Para saída em arquivo, prefira PrintWriter: ele obriga você a pensar na codificação de caracteres (porque o construtor recebe um Charset), enquanto PrintStream silenciosamente usará a codificação padrão para qualquer caractere não ASCII que você imprimir.
O próximo capítulo, Java PrintStream, detalha as diferenças.
Exemplo prático: escrevendo um pequeno arquivo CSV
O programa abaixo abre um arquivo temporário com um PrintWriter, escreve um pequeno CSV (cabeçalho + linhas), demonstra a formatação com printf, chama checkError() para confirmar que as escritas foram bem-sucedidas e, por fim, mostra o comportamento de engolir exceções escrevendo em um writer cujo stream subjacente foi fechado.
O que observar na execução:
- O CSV saiu exatamente como o formato
printfespecificou.%.2farredondou o preço para duas casas decimais;%-10salinhou à esquerda em uma coluna de 10 caracteres;\n(não%n) manteve o terminador de linha portável. As strings de formato são o principal motivo para escolherPrintWriterem vez deWritersimples. - O primeiro
try-with-resources comprimiu a pilha de quatro camadas —Path→FileOutputStream→OutputStreamWriter(UTF-8)→BufferedWriter→PrintWriter— em uma única chamada de construtor. O construtor(File, Charset)é o que você quer para "abrir este arquivo, escrever texto nele, fechá-lo corretamente." - A verificação com
checkError()foi feita antes do close. Depois queclose()é executado, verificar o sinalizador é mais difícil de agir — você já saiu do blocotry. Dentro do bloco é o lugar certo para verificar. - O
PrintWritercom autoflush ativado envolvendoSystem.outimprimiu o relatório alinhado em colunas porque cadaprintfterminou com%n, o que acionou o flush. Ele não está envolvido em umtry-with-resources — fechar umPrintWriterque decoraSystem.outfecharia o próprioSystem.oute silenciaria tudo impresso depois, então o exemplo usa flush em vez disso. - O quarto bloco construiu um writer cujo
writesempre lança exceção e apontou umPrintWriterpara ele. Oprintlnretornou normalmente — sem exceção a capturar.checkError()retornoutrue, que era a única maneira de descobrir que a escrita havia falhado. Esse é o compromisso do design de engolir e sinalizar: conveniente para código casual, perigoso se você não verificar.
O que vem a seguir
PrintWriter é o escritor de arquivos orientado a caracteres. Seu irmão, Java PrintStream, é o orientado a bytes que alimenta System.out e System.err — mesma API, ancestral diferente, e a razão pela qual a saída no terminal funciona para qualquer caractere que caiba no charset padrão da plataforma.