Escrevendo Arquivos em Java
Escreva arquivos de texto e binários em Java com FileWriter, BufferedWriter, PrintWriter e Files.writeString.
Escrever um arquivo em Java significa transformar dados na memória — uma String, uma List de linhas ou um byte[] — em bytes no disco. Este capítulo aborda os cinco writers que você realmente vai usar, quando cada um se encaixa, as flags de StandardOpenOption que decidem o comportamento de sobrescrever versus anexar, e o bug de escrita mais comum: dados que "não foram salvos" porque o writer nunca foi fechado.
A escrita segue a mesma estrutura que a leitura do capítulo anterior — one-liners modernos em cima de Files, decoradores clássicos em cima de FileWriter, e um pequeno conjunto de opções que decide o que acontece quando o arquivo de destino existe ou não.
Files.writeString(path, text) — arquivo inteiro em uma chamada
O par de Files.readString. Adicionado no Java 11.
Files.writeString(Path.of("notes.txt"), "hello world\n", StandardCharsets.UTF_8);As opções de abertura padrão são CREATE, WRITE, TRUNCATE_EXISTING — ou seja, "criar se não existir, sobrescrever se existir." Esse padrão pega desprevenidos quem espera comportamento de append; você opta por ele explicitamente:
Files.writeString(path, "another line\n", StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);Retorna o Path que você forneceu (útil para encadeamento). Use quando: você tem uma pequena quantidade de texto e quer uma única chamada. A mesma ressalva de memória que readString — não construa uma string de 4 GB na memória só para escrevê-la.
Files.write(path, lines) e Files.write(path, bytes)
Duas sobrecargas do mesmo Files.write:
Files.write(Path.of("hosts.txt"), List.of("alpha", "beta", "gamma"), StandardCharsets.UTF_8);
Files.write(Path.of("photo.png"), pngBytes);A sobrecarga Iterable<? extends CharSequence> escreve cada elemento em sua própria linha com separadores \n. A sobrecarga byte[] escreve bytes brutos — sua escolha para dados binários quando os bytes já estão na memória.
Files.newBufferedWriter(path) — a factory de writer moderna
O equivalente baseado em handle e streaming de Files.newBufferedReader.
try (BufferedWriter w = Files.newBufferedWriter(
Path.of("out.txt"), StandardCharsets.UTF_8, StandardOpenOption.CREATE)) {
w.write("first line");
w.newLine();
w.write("second line");
w.newLine();
}Use quando: você está escrevendo muitos pequenos pedaços (um loop sobre registros, uma transformação em streaming, um writer de log) e não quer materializar todo o conteúdo como string primeiro. O buffer agrupa escritas para que o SO veja um pequeno número de syscalls grandes em vez de muitas pequenas.
FileWriter e BufferedWriter — a pilha clássica
A versão legada "monte você mesmo":
try (BufferedWriter w = new BufferedWriter(new FileWriter("out.txt", StandardCharsets.UTF_8))) {
for (String line : lines) {
w.write(line);
w.newLine();
}
}Três camadas, de baixo para cima: FileWriter escreve caracteres brutos usando o charset que você fornece (ou o padrão da plataforma — nunca faça isso); BufferedWriter envolve com um buffer em memória e um método newLine() portátil. Mesma estrutura, mais digitação do que a forma Files.newBufferedWriter. Código novo prefere a factory moderna; você vai encontrar essa pilha em código mais antigo.
O segundo argumento do construtor de FileWriter é append:
new FileWriter("out.txt", true); // append mode (boolean)
new FileWriter("out.txt", StandardCharsets.UTF_8); // overwrite, UTF-8
new FileWriter("out.txt", StandardCharsets.UTF_8, true); // append, UTF-8O construtor (String, boolean) é anterior aos construtores com suporte a charset. Misturar os dois na mesma base de código é um daqueles riscos de manutenção legada — mesma classe, duas ordens de argumentos concorrentes.
PrintWriter — saída formatada
PrintWriter adiciona print, println e printf sobre qualquer Writer. É a mesma API que você usa em System.out (que é um PrintStream, o irmão orientado a bytes).
try (PrintWriter w = new PrintWriter(Files.newBufferedWriter(Path.of("report.txt")))) {
w.println("Report generated");
w.printf("user = %-10s total = %d%n", "alice", 42);
w.printf("user = %-10s total = %d%n", "bob", 17);
}Duas coisas importantes:
printfusa%npara o separador de linha da plataforma.\né LF fixo, que é o que você geralmente quer para arquivos de log e dados lidos por máquina.PrintWritersuprimeIOException.print,printlneprintfnão lançam exceções — eles definem uma flag de erro interna que você verifica comcheckError(). Essa é uma escolha deliberada paraSystem.out(escritas no console não devem derrubar uma ferramenta CLI), mas é um imã de bugs para writers de arquivo. Se o tratamento confiável de erros é importante, passefalseao construtor apropriado e useBufferedWriterpara a escrita,PrintWriterapenas para os auxiliares de formatação — ou consultecheckError()após as escritas.
Flags de StandardOpenOption
Todo writer moderno aceita varargs OpenOption... que alteram a semântica de abertura:
| Opção | Significado |
|---|---|
CREATE | Cria o arquivo se não existir; caso contrário abre o existente. |
CREATE_NEW | Cria; lança FileAlreadyExistsException se o arquivo existir. Atômico. |
TRUNCATE_EXISTING | Se o arquivo existia, limpa-o na abertura. |
APPEND | Escreve no final do arquivo sem truncar. Atômico na maioria dos SOs. |
WRITE | Abre para escrita. Sempre implícito para writers. |
SYNC / DSYNC | Bloqueia cada escrita até o SO reportar que está no disco. Lento; durabilidade para segurança em caso de crash. |
DELETE_ON_CLOSE | Exclui o arquivo quando o stream é fechado. |
As combinações que importam:
- Sobrescrever (padrão):
CREATE, TRUNCATE_EXISTING. O queFiles.writeStringeFiles.newBufferedWriterusam por padrão. - Anexar:
CREATE, APPEND. O padrão para arquivos de log. - Criar ou falhar:
CREATE_NEW. O padrão para arquivo de lock ou "não clobber".
APPEND é atômico no SO em Unix: dois processos anexando ao mesmo arquivo obtêm blocos intercalados, mas sem escritas corrompidas dentro de um único pedaço bufferizado. Esse é o contrato que o torna o padrão de logging padrão.
A armadilha do "writer não escreveu nada"
Este é o bug que toda base de código Java encontra uma vez:
// WRONG — the writer is never closed
BufferedWriter w = Files.newBufferedWriter(path);
w.write("important data");
return; // tail buffer is still in memory; nothing reached the diskBufferedWriter (e PrintWriter) agrupa escritas em um pedaço na memória. Os bytes não chegam ao disco até que o buffer encha ou close() seja executado. Sem try-with-resources você pula o close, e seus dados "salvos" evaporam.
// CORRECT
try (BufferedWriter w = Files.newBufferedWriter(path)) {
w.write("important data");
} // close() runs here; tail buffer is flushedSe você precisa dos dados no disco antes do close — digamos, um tail-watcher precisa ver cada linha de log — chame flush() explicitamente. Files.newBufferedWriter não faz auto-flush após cada escrita; esse é o preço do buffer.
Qual writer usar
| Cenário | Escolha |
|---|---|
| String pequena, uma chamada | Files.writeString |
| Lista de linhas ou array de bytes | Files.write |
| Streaming de muitas linhas | Files.newBufferedWriter |
Precisa de formatação printf | PrintWriter envolvendo um writer bufferizado |
| Apenas código legado | BufferedWriter(new FileWriter(...)) |
Use Files.writeString como padrão para "já tenho o texto" e Files.newBufferedWriter para "vou construir linha a linha." Use PrintWriter apenas quando precisar de printf.
Um exemplo completo: todos os writers lado a lado
O programa abaixo escreve o mesmo conteúdo de três maneiras diferentes — one-shot moderno, streaming linha a linha via BufferedWriter e formatado com printf via PrintWriter — depois demonstra APPEND versus o padrão TRUNCATE_EXISTING e, por fim, o modo de falha do "esqueceu de fechar". Todas as escritas têm como alvo um arquivo temporário para que o exemplo rode em qualquer lugar.
O que tirar da execução:
Files.writeStringeFiles.write(List)são as chamadas corretas quando você já tem todo o conteúdo. Ambos sobrescreveram o arquivo a cada vez porque suas opções padrão incluemTRUNCATE_EXISTING.BufferedWriterePrintWriterforam executados dentro detry-with-resources. Essa é a única coisa que garante que o buffer final chegue ao disco — pule isso e você entrega um bug de "writer não escreveu nada".- A sequência APPEND/TRUNCATE escreveu
base, anexouappended, depois truncou e escreveutruncated. O arquivo final continha apenastruncated\n, que é a armadilha — o modo padrão de todo writer moderno é sobrescrever, não anexar. Você tem que optar por isso. CREATE_NEWem um caminho existente lançouFileAlreadyExistsException. Essa é a semântica de "não clobber" — útil para arquivos de lock e marcadores atômicos de "já rodei antes?".- O writer com vazamento tinha tamanho de arquivo 0 antes de
flush()ser chamado. Os bytes estavam na memória, não no disco; sem oflush()manual (ou umclose()adequado), eles teriam sido perdidos.
O que vem a seguir
O próximo capítulo, Excluindo Arquivos em Java, encerra os capítulos de "operações de arquivo de alto nível" com os três deleters: File.delete(), Files.delete() e Files.deleteIfExists() — e como remover uma árvore de diretórios sem escrever a recursão manualmente.