W3docs

Java PrintWriter

Escribe texto formateado en flujos con la clase PrintWriter de Java: print, println, printf, format.

El capítulo sobre flujos de caracteres presentó Writer y su método principal, write(String). Eso es suficiente para cualquier cosa, pero no resulta ergonómico: imprimir un número implica escribir w.write(Integer.toString(n)), imprimir una línea significa recordar añadir el terminador de línea manualmente, y la salida con formato requiere llamar a String.format en cada invocación. PrintWriter es el decorador que soluciona la ergonomía: añade print, println, printf y format sobre cualquier Writer u OutputStream subyacente.

Es el equivalente del lado de los archivos al System.out que has estado usando desde el primer capítulo: la misma superficie de API, pero escribiendo en un archivo en lugar de en la consola.

Qué añade PrintWriter

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)

Además de los métodos heredados de Writer (write, flush, close). La clave son las sobrecargas tipadas: puedes escribir directamente cualquier primitivo u objeto, y PrintWriter llama a String.valueOf por ti.

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
}

El close() del try-with-resources vacía el búfer; sin él, la trampa del búfer final del capítulo sobre flujos con búfer afecta igualmente a PrintWriter.

Constructores

Los más útiles, ordenados por preferencia:

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);

El constructor Path + Charset es el más sencillo para "abrir este archivo y escribir en él":

try (PrintWriter w = new PrintWriter(path.toFile(), StandardCharsets.UTF_8)) {
  w.println("hello");
}

Abre el archivo, lo envuelve en un OutputStreamWriter con el juego de caracteres indicado, lo envuelve en un BufferedWriter y te entrega un PrintWriter. La pila de cuatro capas que antes ensamblarías a mano queda reducida a una sola línea.

Pasa siempre un juego de caracteres explícito. Los constructores sin charset — new PrintWriter("file.txt"), new PrintWriter(outputStream) — recurren a la codificación predeterminada de la JVM, que es el mismo riesgo de portabilidad descrito en el capítulo sobre flujos de caracteres. UTF-8 es el valor predeterminado correcto.

La IOException silenciada

PrintWriter difiere de todos los demás Writer en un aspecto importante: no lanza IOException. Ninguno de print, println, printf ni write la declara. Si una llamada de E/S subyacente falla, PrintWriter absorbe la excepción y activa un indicador de "error" interno.

Esto resulta conveniente — puedes escribir un bloque largo de llamadas println sin un try/catch alrededor de cada una —, pero significa que una escritura fallida pasa desapercibida. Tienes que preguntar:

if (w.checkError()) {
  // something went wrong; the underlying IOException was swallowed
  throw new IOException("write to " + path + " failed");
}

checkError() es la única forma de averiguarlo. No hay manera de recuperar la IOException original: cuando la compruebas, ya ha desaparecido. Por tanto:

  • Para la salida por consola (el caso de uso de System.out), absorber la excepción está bien: nadie gestiona una escritura fallida en un terminal.
  • Para archivos en los que una escritura parcial es un problema real (un fichero de registro, un archivo de guardado, un informe generado), comprueba checkError() al final del bloque, o usa un BufferedWriter y llama a write directamente para que la excepción se propague.

Autoflush

El argumento del constructor autoFlush controla si cada llamada a println, printf o format invoca después a flush(). El valor predeterminado es desactivado:

new PrintWriter(out, false)                          // explicit close/flush only
new PrintWriter(out, true)                           // flush after every println/printf/format

print y write nunca realizan autoflush, aunque el indicador esté activado: solo lo hacen los métodos de línea y formato. Por eso System.out.print("waiting...") puede quedar invisible mientras se ejecuta el siguiente cálculo, y System.out.println("waiting...") aparece de inmediato.

Para archivos, deja el autoflush desactivado y deja que close() (mediante try-with-resources) lo gestione. Para un registro interactivo al que estás haciendo seguimiento, actívalo o llama a flush() después de cada lote.

println y el separador de línea de la plataforma

println() escribe System.lineSeparator()\n en Unix y macOS, \r\n en Windows. Lo mismo ocurre con el especificador %n en printf. Es una característica útil para la salida en terminal y un problema para archivos de datos; la discusión del capítulo sobre flujos con búfer (en BufferedWriter.newLine) aplica aquí textualmente:

w.printf("row,%d%n", 1);                             // platform-dependent terminator
w.printf("row,%d\n", 1);                             // portable \n

Cuando la salida vaya a ser leída por algo distinto a la máquina local, escribe \n explícitamente.

Comparación con PrintStream

PrintStream es el hermano orientado a bytes de PrintWriter: misma API, ancestro diferente. System.out y System.err son instancias de PrintStream. Para la salida a archivos, prefiere PrintWriter: te obliga a pensar en la codificación de caracteres (porque el constructor acepta un Charset), mientras que PrintStream utilizará silenciosamente la codificación predeterminada para cualquier carácter no ASCII que imprimas.

El siguiente capítulo, Java PrintStream, detalla las diferencias.

Un ejemplo práctico: escribir un pequeño archivo CSV

El programa siguiente abre un archivo temporal con un PrintWriter, escribe un CSV pequeño (encabezado + filas), muestra el formato con printf, llama a checkError() para confirmar que las escrituras se realizaron correctamente, y por último ilustra el comportamiento de absorción de excepciones al escribir en un writer cuyo flujo subyacente ha sido cerrado.

java— editable, runs on the server

Lo que debes extraer de la ejecución:

  • El CSV salió exactamente como especificó el formato printf. %.2f redondeó el precio a dos decimales; %-10s lo alineó a la izquierda dentro de una columna de 10 caracteres; \n (no %n) mantuvo el terminador de línea portable. Las cadenas de formato son la razón principal para elegir PrintWriter sobre un Writer simple.
  • El primer try-with-resources tomó la pila de cuatro capas — PathFileOutputStreamOutputStreamWriter(UTF-8)BufferedWriterPrintWriter — y la redujo a una sola llamada al constructor. El constructor (File, Charset) es el que querrás usar para "abrir este archivo, escribir texto en él y cerrarlo correctamente".
  • La comprobación de checkError() se ejecutó antes del cierre. Una vez que se ejecuta close(), es más difícil actuar sobre el resultado del indicador: ya has salido del bloque try. Dentro del bloque es el lugar adecuado para comprobarlo.
  • El PrintWriter con autoflush activado que envuelve System.out imprimió el informe con las columnas alineadas porque cada printf terminaba en %n, lo que desencadenó el vaciado. No está envuelto en un try-with-resources: cerrar un PrintWriter que decora System.out cerraría el propio System.out y silenciaría todo lo que se imprima después, por lo que el ejemplo vacía manualmente en su lugar.
  • El cuarto bloque construyó un writer cuyo write siempre lanza una excepción y apuntó un PrintWriter hacia él. El println retornó con normalidad: no hubo ninguna excepción que capturar. checkError() devolvió true, que fue la única forma de saber que la escritura había fallado. Ese es el compromiso del diseño de absorber y marcar: conveniente para código casual, peligroso si no lo compruebas.

Qué viene a continuación

PrintWriter es el escritor de archivos orientado a caracteres. Su hermano, Java PrintStream, es el orientado a bytes que impulsa System.out y System.err: misma API, ancestro diferente, y la razón por la que la salida en terminal funciona para cualquier carácter que quepa en el juego de caracteres predeterminado de la plataforma.

Práctica

Práctica
Escribes un archivo de 10.000 líneas con `PrintWriter` y nunca llamas a `checkError()`. Tres líneas en medio del archivo fallaron al escribirse debido a una condición de disco lleno. ¿Cómo queda el archivo resultante y qué reporta el programa?
Escribes un archivo de 10.000 líneas con `PrintWriter` y nunca llamas a `checkError()`. Tres líneas en medio del archivo fallaron al escribirse debido a una condición de disco lleno. ¿Cómo queda el archivo resultante y qué reporta el programa?
Was this page helpful?