W3docs

Java PrintStream

Cómo PrintStream impulsa System.out y System.err y cómo usarlo para salida formateada orientada a bytes.

PrintStream es la clase que ha estado bajo tu código desde el capítulo 1. System.out es un PrintStream. System.err es un PrintStream. Cada System.out.println(...) que hayas escrito alguna vez ha pasado por esta clase.

Tiene la misma superficie que el PrintWriter que acabas de conocer — print, println, printf, format — y el mismo comportamiento de ignorar las excepciones. La diferencia está en lo que hay debajo: PrintStream extiende OutputStream (bytes), mientras que PrintWriter extiende Writer (caracteres). Para la salida a archivos, la distinción byte/carácter descrita antes en esta parte sigue aplicando: caracteres de entrada, caracteres de salida, y la codificación vive en el límite.

¿Por qué dos clases con la misma API?

Historia. Java 1.0 tenía PrintStream pero ninguna jerarquía de Writer en absoluto — cada "print" iba a un flujo de bytes. Java 1.1 introdujo la jerarquía Reader/Writer para el manejo adecuado de caracteres y añadió PrintWriter para que el código de escritura de archivos pudiera usar la misma API con caracteres. PrintStream no podía retirarse porque System.out y System.err ya estaban tipados como PrintStream en las APIs publicadas, y cambiarlos habría roto todos los programas del mundo.

Por eso existen ambas. La regla práctica:

  • Usa PrintWriter para archivos. La jerarquía orientada a caracteres es donde pertenece la codificación.
  • Usa PrintStream cuando no tengas otra opción — es decir, cuando System.out/System.err es el destino, o cuando escribes en un OutputStream que no quieres envolver.

Los casos de "no tener otra opción" son escasos. La mayoría de las veces puedes hacer esto:

PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8);
out.println("hello");

y olvidarte de que existe PrintStream.

La API

Idéntica a PrintWriter:

void print(boolean | char | int | long | float | double | String | Object);
void println(...);                                  // adds the platform line separator
PrintStream printf(String format, Object... args);
PrintStream format(String format, Object... args);
PrintStream append(CharSequence s);

Más los métodos heredados de OutputStream (write(int), write(byte[]), flush, close). La misma trampa de BufferedWriter y PrintWriter aplica: println escribe System.lineSeparator(), que es \r\n en Windows. Escribe \n explícitamente cuando la salida necesite ser portable.

Constructores

new PrintStream(OutputStream out);                                   // platform default charset
new PrintStream(OutputStream out, boolean autoFlush, Charset cs);    // explicit charset
new PrintStream(File file, Charset charset);                          // open a file
new PrintStream(String filename, Charset charset);

Al igual que con PrintWriter, los constructores sin charset recurren a la codificación predeterminada de la JVM — el mismo riesgo de portabilidad descrito en el capítulo de flujos de caracteres. Siempre pasa un charset.

El indicador autoFlush tiene la misma semántica que PrintWriter: cuando está activado, println, printf, format y write(byte[], int, int) en una nueva línea desencadenan un flush. print no lo hace. Desactivado por defecto.

La IOException ignorada (todavía)

Mismo diseño que PrintWriter. Ninguno de los métodos print/println/printf lanza IOException. Una escritura fallida establece un indicador de error que puedes leer con checkError(). La compensación es la misma: conveniente para código informal, peligroso si no lo compruebas.

Para System.out/System.err específicamente, ignorar el error es la decisión correcta — no hay nada útil que hacer cuando falla una escritura en el terminal. Para un PrintStream respaldado por un archivo, prefiere PrintWriter, o comprueba checkError() antes de cerrar.

System.out y System.err

Estas dos son instancias de PrintStream creadas durante el inicio de la JVM. Envuelven los descriptores de archivo stdout y stderr del sistema operativo. Su codificación de caracteres sigue stdout.encoding (Java 18+) o file.encoding (versiones anteriores), por eso la salida redirigida por pipe a veces muestra caracteres incorrectos en una consola Windows — la página de código de la consola no coincide con la idea de codificación de la JVM.

Puedes reemplazarlos con System.setOut(PrintStream) y System.setErr(PrintStream), lo que en ocasiones es útil para capturar la salida en las pruebas:

ByteArrayOutputStream captured = new ByteArrayOutputStream();
PrintStream original = System.out;
System.setOut(new PrintStream(captured, true, StandardCharsets.UTF_8));
try {
  runTheCodeUnderTest();
  assertEquals("expected\n", captured.toString(StandardCharsets.UTF_8));
} finally {
  System.setOut(original);
}

Para código de producción, déjalos en paz. Los frameworks de registro (java.util.logging, SLF4J/Logback) adoptan un enfoque diferente y estructurado para escribir salida de diagnóstico.

print(Object) y null

Un comportamiento sutil compartido con PrintWriter: print(Object o) llama a String.valueOf(o), que devuelve la cadena de cuatro caracteres "null" para una referencia null en lugar de lanzar NullPointerException. Por eso

System.out.println(maybeNullList);                  // prints "null", not NPE

funciona. Conveniente para registros informales; engañoso si estás escribiendo la cadena de vuelta en un archivo de datos que re-analizarás más tarde — "null" como string es indistinguible de la palabra literal "null."

write(int) escribe un byte, no un carácter

PrintStream es un OutputStream. El write(int b) heredado escribe el byte de orden inferior:

System.out.write(65);                              // writes 'A' — the byte 0x41
System.out.write('é');                              // writes a single byte 0xE9 — NOT UTF-8 for 'é'

La segunda línea es incorrecta en un terminal UTF-8 — 'é' tiene dos bytes en UTF-8 (0xC3 0xA9), y escribiste uno. No uses write(int) en un PrintStream para caracteres; usa print/println, que pasan por el charset configurado.

Un ejemplo práctico: System.out redirigido e inspeccionado

El programa siguiente captura System.out en un ByteArrayOutputStream para que puedas ver exactamente qué bytes emite la JVM cuando llamas a println. Ejecuta el mismo println("Café") con dos charsets distintos para que el comportamiento de codificación sea concreto, demuestra checkError() en un flujo que falla, y finalmente muestra la diferencia entre print(Object) para una referencia null y una comprobación null deliberada.

java— editable, runs on the server

Lo que se puede aprender de la ejecución:

  • System.setOut(new PrintStream(buffer, ...)) capturó lo que de otro modo habría ido a la consola. Las pruebas usan este patrón continuamente. Restaura el original antes de imprimir el informe — de lo contrario, el informe también irá al buffer y vendrá la confusión.
  • La línea "Café" emitió 5 bytes en UTF-8 (43 61 66 C3 A9) y 4 bytes en ISO-8859-1 (43 61 66 E9). La misma entrada, diferentes anchos de byte, ambos correctos — la codificación es el mapeo de byte → carácter, y PrintStream respeta el charset que le diste a su constructor. El constructor sin charset elegiría cualquiera de estos que la JVM estuviera ejecutando en ese momento.
  • El bloque de flujo roto demostró el engullimiento: println devolvió normalmente, la IOException subyacente desapareció, y checkError() fue la única forma de descubrir que la escritura había fallado. Mismo contrato que PrintWriter. Si te importa el fallo, debes preguntar.
  • La impresión de referencia null produjo la cadena de cuatro caracteres null, no una NullPointerException. Así es como println(someList) funciona incluso cuando someList es null — conveniente, pero significa que no puedes distinguir el texto literal "null" de una referencia null una vez está en el disco. Usa Objects.requireNonNull o una comprobación null explícita en el límite si esa distinción importa.
  • Nada en el ejemplo llamó a un PrintWriter. Para System.out, no necesitas uno — PrintStream es el tipo que Java ya te dio, la API es idéntica, y el comportamiento de autoflush-en-println es lo que quieres en el terminal.

Qué sigue

Los primeros trece capítulos de esta parte han cubierto cada forma de I/O de streaming: bytes, caracteres, buffering, primitivos, texto formateado. Todos transmiten contenido — bytes y chars. El siguiente capítulo, Java Serialization, trata de transmitir grafos de objetos — toda una estructura enlazada de referencias, escrita en un flujo y reconstruida en el otro lado, con una sola anotación en la clase.

Práctica

Práctica
`PrintWriter` y `PrintStream` tienen APIs casi idénticas (`print`, `println`, `printf`). Al escribir texto en un archivo, ¿cuál deberías preferir generalmente, y por qué?
`PrintWriter` y `PrintStream` tienen APIs casi idénticas (`print`, `println`, `printf`). Al escribir texto en un archivo, ¿cuál deberías preferir generalmente, y por qué?
Was this page helpful?