Escritura de archivos en Java
Escribe archivos de texto y binarios en Java con FileWriter, BufferedWriter, PrintWriter y Files.writeString.
Escribir un archivo en Java significa convertir datos en memoria — un String, una List de líneas o un byte[] — en bytes en disco. Este capítulo cubre los cinco escritores que realmente usarás, cuándo encaja cada uno, los indicadores de StandardOpenOption que determinan el comportamiento de sobrescritura frente a agregación, y el error de escritura más común: datos que "no se guardaron" porque el escritor nunca se cerró.
La escritura sigue la misma estructura que la lectura del capítulo anterior — instrucciones de una línea modernas sobre Files, decoradores clásicos sobre FileWriter, y un pequeño conjunto de opciones que determinan qué sucede cuando el archivo destino existe o no existe.
Files.writeString(path, text) — archivo completo en una llamada
El equivalente de Files.readString. Añadido en Java 11.
Files.writeString(Path.of("notes.txt"), "hello world\n", StandardCharsets.UTF_8);Las opciones de apertura predeterminadas son CREATE, WRITE, TRUNCATE_EXISTING — es decir, "crear si no existe, sobrescribir si está presente". Ese comportamiento predeterminado sorprende a quienes esperan el modo de agregación; se activa explícitamente:
Files.writeString(path, "another line\n", StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);Devuelve el Path que le diste (práctico para encadenamiento). Úsalo cuando: tienes una pequeña cantidad de texto y quieres una sola llamada. La misma advertencia de memoria que readString — no construyas un string de 4 GB en memoria solo para escribirlo.
Files.write(path, lines) y Files.write(path, bytes)
Dos sobrecargas del mismo Files.write:
Files.write(Path.of("hosts.txt"), List.of("alpha", "beta", "gamma"), StandardCharsets.UTF_8);
Files.write(Path.of("photo.png"), pngBytes);La sobrecarga Iterable<? extends CharSequence> escribe cada elemento en su propia línea con separadores \n. La sobrecarga byte[] escribe bytes sin procesar — tu opción preferida para datos binarios cuando los bytes ya están en memoria.
Files.newBufferedWriter(path) — la fábrica moderna de escritores
El equivalente basado en handles y orientado al 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();
}Úsalo cuando: estás escribiendo muchos fragmentos pequeños (un bucle sobre registros, una transformación en streaming, un escritor de logs) y no quieres materializar todo el contenido como string primero. El buffer agrupa las escrituras para que el SO vea un puñado de llamadas al sistema grandes en lugar de muchas pequeñas.
FileWriter y BufferedWriter — la pila clásica
La versión heredada de "constrúyelo tú mismo":
try (BufferedWriter w = new BufferedWriter(new FileWriter("out.txt", StandardCharsets.UTF_8))) {
for (String line : lines) {
w.write(line);
w.newLine();
}
}Tres capas, de abajo hacia arriba: FileWriter escribe caracteres sin procesar usando el charset que proporcionas (o el predeterminado de la plataforma — nunca hagas esto); BufferedWriter lo envuelve con un buffer en memoria y un método newLine() portátil. Misma estructura, más pulsaciones de teclado que la forma con Files.newBufferedWriter. El código nuevo prefiere la fábrica moderna; verás esta pila en código antiguo.
El segundo argumento del constructor de FileWriter es 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-8El constructor (String, boolean) es anterior a los que soportan charset. Mezclar los dos en la misma base de código es uno de esos riesgos de mantenimiento heredado — misma clase, dos órdenes de argumentos en competencia.
PrintWriter — salida formateada
PrintWriter añade print, println y printf sobre cualquier Writer. Es la misma API que has estado usando en System.out (que es en sí mismo un PrintStream, el hermano 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);
}Dos cosas a tener en cuenta:
printfusa%npara el separador de líneas de la plataforma.\nes LF fijo, que es lo que generalmente quieres para archivos de log y datos leídos por máquinas.PrintWritersilenciaIOException.print,printlnyprintfno lanzan excepciones — establecen un indicador de error interno que puedes verificar concheckError(). Es una elección deliberada paraSystem.out(las escrituras en consola no deberían hacer fallar una herramienta CLI), pero es una fuente de errores para escritores de archivos. Si el manejo confiable de errores es importante, pasafalseal constructor apropiado y usaBufferedWriterpara la escritura,PrintWritersolo para los helpers de formateo — o consultacheckError()después de las escrituras.
Indicadores de StandardOpenOption
Cada escritor moderno acepta varargs OpenOption... que cambian la semántica de apertura:
| Opción | Significado |
|---|---|
CREATE | Crear el archivo si no existe; de lo contrario, abrir el existente. |
CREATE_NEW | Crear; lanzar FileAlreadyExistsException si el archivo existe. Atómico. |
TRUNCATE_EXISTING | Si el archivo existía, vaciarlo al abrir. |
APPEND | Escribir al final del archivo sin truncar. Atómico en la mayoría de los SO. |
WRITE | Abrir para escritura. Siempre implícito para escritores. |
SYNC / DSYNC | Bloquear cada escritura hasta que el SO confirme que está en disco. Lento; durabilidad para seguridad ante fallos. |
DELETE_ON_CLOSE | Eliminar el archivo cuando el stream se cierre. |
Las combinaciones que importan:
- Sobrescribir (predeterminado):
CREATE, TRUNCATE_EXISTING. Lo que usan por defectoFiles.writeStringyFiles.newBufferedWriter. - Agregar:
CREATE, APPEND. El patrón para archivos de log. - Crear o fallar:
CREATE_NEW. El patrón de archivo de bloqueo o "no sobreescribir".
APPEND es atómico a nivel del SO en Unix: dos procesos que agregan al mismo archivo obtienen bloques intercalados pero sin escrituras rotas dentro de un único fragmento con buffer. Ese es el contrato que lo convierte en el patrón estándar de logging.
La trampa del "escritor no escribió nada"
Este es el error que toda base de código Java encuentra alguna 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 (y PrintWriter) agrupa las escrituras en un fragmento en memoria. Los bytes no llegan al disco hasta que el buffer se llena o se ejecuta close(). Sin try-with-resources te saltas el cierre, y tus datos "guardados" desaparecen.
// CORRECT
try (BufferedWriter w = Files.newBufferedWriter(path)) {
w.write("important data");
} // close() runs here; tail buffer is flushedSi necesitas datos en disco antes del cierre — por ejemplo, un observador de cola necesita ver cada línea de log — llama a flush() explícitamente. Files.newBufferedWriter no hace auto-flush después de cada escritura; ese es el precio del buffer.
Qué escritor usar
| Escenario | Elige |
|---|---|
| String pequeño, una sola operación | Files.writeString |
| Lista de líneas o array de bytes | Files.write |
| Streaming de muchas líneas | Files.newBufferedWriter |
Necesitas formateo con printf | PrintWriter envolviendo un escritor con buffer |
| Solo código heredado | BufferedWriter(new FileWriter(...)) |
Usa Files.writeString por defecto para "ya tengo el texto" y Files.newBufferedWriter para "lo construiré línea por línea". Recurre a PrintWriter solo cuando necesites printf.
Un ejemplo práctico: todos los escritores lado a lado
El programa siguiente escribe el mismo contenido de tres maneras diferentes — instrucción única moderna, líneas en streaming con BufferedWriter, y formateo con printf mediante PrintWriter — luego demuestra APPEND frente al TRUNCATE_EXISTING predeterminado, y finalmente el modo de fallo por "olvidé cerrar". Todas las escrituras apuntan a un archivo temporal para que el ejemplo funcione en cualquier lugar.
Qué aprender de la ejecución:
Files.writeStringyFiles.write(List)son las llamadas correctas cuando ya tienes todo el contenido. Ambas sobrescribieron el archivo cada vez porque sus opciones predeterminadas incluyenTRUNCATE_EXISTING.BufferedWriteryPrintWriterse ejecutaron dentro detry-with-resources. Eso es lo único que garantiza que el buffer de cola llegue al disco — omítelo y tendrás un bug de "escritor no escribió nada".- La secuencia APPEND/TRUNCATE escribió
base, agregóappended, luego truncó y escribiótruncated. El archivo final contenía solotruncated\n, que es la trampa — el modo predeterminado de todos los escritores modernos es sobrescribir, no agregar. Hay que activarlo explícitamente. CREATE_NEWsobre una ruta existente lanzóFileAlreadyExistsException. Esa es la semántica de "no sobreescribir" — útil para archivos de bloqueo y marcadores atómicos de "¿ya se ha ejecutado?".- El escritor con fuga tenía un tamaño de archivo de 0 antes de que se ejecutara
flush(). Los bytes estaban en memoria, no en disco; sin elflush()manual (o unclose()apropiado), se habrían perdido.
Qué sigue
El siguiente capítulo, Eliminación de archivos en Java, cierra los capítulos de "operaciones de archivos de alto nivel" con los tres eliminadores: File.delete(), Files.delete() y Files.deleteIfExists() — y cómo eliminar un árbol de directorios sin escribir la recursión a mano.