Java – Flujos de datos – Entrada y Salida estándar

La entrada y salida estándar son flujos de datos que actúan como canales de comunicación permitiendo la interacción entre un programa y su entorno en el sistema. En Java podemos tener acceso a estos flujos de datos desde la clase java.lang.System.

Salida estándar


La salida estándar esta asociada por defecto a la terminal del sistema, es decir que el resultado de los datos enviados a la salida se observa en pantalla a menos que su destino sea modificado. Existen dos salidas con diferentes propositos, una es la propiamente denominada salida estándar la cual se utiliza para la salida regular de datos, y la otra es la salida estándar de errores la cual se utiliza para la salida de errores.

La salida de datos: System.out

La salida estándar de datos para uso normal (abreviada y en inglés: StdOut) esta representada por un objeto PrintStream llamado out. La clase PrintStream es una implementación especial de FilterOutputStream y por lo tanto también de la clase base OutputStream, de modo que mostrar datos en pantalla es tan sencillo como referirnos a la variable out e invocar el método void println(String x).

Recordando un ejercicio básico que probablemente todo novato haya echo, aquí vemos la línea de código que utilizamos para escribir la famosa cadena de texto "Hola Mundo":

...
System.out.println("Hola Mundo");
...

La salida:

Hola Mundo

Los métodos print(...)println(...) son los más usados. De hecho la clase PrintStream posee estos métodos sobrecargados para todos los datos primitivos (int, long, etc.) y entonces así puede enviar cualquier tipo de dato como texto hacia la salida. Los objetos que se quieran imprimir o concadenar serán implícitamente representados por su implementación del método String toString().

Si miramos la API de PrintStream podemos ver la variedad de métodos para mostrar datos en pantalla, algunos de estos:

Método Descripción
void print(String s) Escribe un string
void println(String x) Escribe un string y termina la línea.
void println() Termina la línea actual.
void printf(String format, Object... args) Utiliza un Formatter para poder escribir un String formateado. (Disponible desde J2SE 5.0)

La salida de errores: System.err

Esta es otra salida estándar pero con el fin de ser utilizada para errores (abreviada y en inglés: StdErr). Al igual que StdOut, también es representada por un objeto PrintStream que en este caso se llama err. Los métodos que podemos invocar aquí son los mismos vistos anteriormente.

No parece muy útil utilizar out y err si su destino es el mismo, o al menos en el caso de la consola del sistema donde las dos salidas son representadas con el mismo color y no notamos diferencia alguna. En cambio en la consola de varios IDEs como NetBeans o Eclipse la salida de err se ve en un color diferente, por ejemplo:

...
System.out.println("Esta es la salida estandar normal");
System.err.println("Esta es la salida estandar de errores");
...

La salida en NetBeans:

Como se puede observar, en el entorno de un IDE como este utilizar las dos salidas nos puede ayudar a tener un registro mas legible de las actividades del programa.

Podríamos sacar mas provecho de estas dos salidas si se modifica su destino. Por ejemplo podríamos guardar los logs de un programa enviando la salida out a un archivo llamado salida_normal.txt y la salida err a un archivo llamado salida_error.txt.

Re-definir la salida estándar

Para mostrar como re-definir la salida estándar pondremos en practica el ejemplo mencionado anteriormente sobre modificar el destino de out y err hacia dos archivos diferentes.

Una forma de modificar las salidas es utilizando los métodos estáticos void setOut(PrintStream out) y void setErr(PrintStream err) de la propia clase System.  Necesitamos abrir flujos de datos hacia los nuevos destinos de cada salida, para cada una se envuelve un objeto FileOutputStream(File file) con un objeto PrintStream(OutputStream out). Véase:

...
System.setOut(new PrintStream(new FileOutputStream("salida_normal.txt")));
System.setErr(new PrintStream(new FileOutputStream("salida_error.txt")));

System.out.println("Esta es la salida estandar normal");
System.err.println("Esta es la salida estandar de errores");

throw new RuntimeException("Error Fatal");
...

En el ejemplo anterior se crea el archivo correspondiente para cada tipo de salida.

Otra forma de modificar las salidas estándar es a través de la consola del sistema, tanto en sistemas Windows como UNIX podemos escribir lo siguiente para un tarro (.jar):

java -jar archivo.jar > salida_normal.txt 2> salida_error.txt

Ó lo siguiente para una clase (.class):

java MiClase > salida_normal.txt 2> salida_error.txt

En ambos casos lo que estamos haciendo es cambiar  primero con ">" la salida out hacia salida_normal.txt y luego con "2>" la salida err hacia salida_error.txt. En estos casos esos dos archivos se guardarían en el directorio desde donde se esta ejecutando este comando en la consola, pero también se podría haber especificado una ruta absoluta.

Otra cosa practica que puede hacerse desde la consola es bloquear una o incluso las dos salidas. Para bloquear la salida out escribimos:

java -jar archivo.jar > /null
java MiClase > /null

Ó para bloquear la salida err:

java -jar archivo.jar 2> /null
java MiClase 2> /null

Nota: modificar la salida estándar para un applet es considerada una operación peligrosa, por lo tanto los navegadores no lo permiten.

Entrada estándar


La entrada estándar de datos (abreviada y en inglés: StdIn) esta representada por un objeto InputStream. La clase InputStream es la clase base en el paquete java.io para manejar los flujos entrantes de bytes (8-bit). En la mayoría de los casos no queremos leer directamente en bytes un flujo de datos, sino que esperamos un formato humano. Aunque en esta ocasión no contaremos desde el principio con métodos tan prácticos como void println(String x) en el caso de la salida estándar, veremos como realizar una lectura directamente en bytes, también utilizaremos implementaciones mas avanzadas de tipo Reader para leer caracteres y luego tendremos en cuenta las nuevas posibilidades introducidas a partir de J2SE 5.0.

Lectura en bytes

En principio, para leer datos provenientes del teclado de un usuario utilizaremos la variable in de la clase System y el método int read() de su correspondiente objeto InputStream. Véase:

...
System.out.println("> Introduce un caracter...");
int in = System.in.read();
System.out.println("> Has introducido: " + in);
...

Si introducimos A y pulsamos Intro, la salida:

> Introduce un caracter...
A
> Has introducido: 97

El método int read() de InputStream mantendrá la aplicación bloqueada en espera de que se introduzca algo en la entrada estándar para poder continuar, el resultado de presionar A como mayúscula es: 65, si hubiera sido en minúsculas seria: 97. El método int read() lee concretamente un byte y lo devuelve representado como un int (número entero)  con un valor entre 0 - 255, si la entrada de datos proviniera desde un archivo se podría obtener también -1 en caso de que se llegue al EOF (End Of File: fin de un archivo). En nuestro caso el 65 correspondería a la A mayúscula en el formato de caracteres ASCII.

Cuando se lee directamente un flujo de bytes sabemos que estamos trabajando con ASCII, y podríamos hacer una conversión de int a char (un casting) para obtener el carácter:

...
System.out.println("> Introduce un caracter...");

int in = System.in.read();
char c = (char) in;

System.out.println("> Has introducido: " + c);
...

La salida:

> Introduce un caracter...
A
> Has introducido: A

Normalmente hacer lo anterior no es lo mas correcto en la mayoría de las situaciones ya que se debe de tener en cuenta con que formato de caracteres queremos trabajar, pero dado que se trata solo de leer unos datos introducidos desde el teclado no nos supone un problema en este momento.

De cualquier forma no nos preocuparemos por hacer conversiones nosotros mismos, ya que de esto y más se encargaran las implementaciones de la clase Reader que veremos luego.

Si quisiéramos leer más de un byte podemos intentar con el método int read(byte[] b), véase:

...
System.out.println("> Introduce cinco caracteres...");

byte[] bufferIn = new byte[5];
System.in.read(bufferIn);

for (int i = 0 ; i < bufferIn.length ; i++) {
    System.out.println("> Has introducido: " + bufferIn[i]);
}
...

Si introducimos A, B, C, D, E y pulsamos Intro, la salida:

> Introduce cinco caracteres...
ABCDE
> Has introducido: 65
> Has introducido: 66
> Has introducido: 67
> Has introducido: 68
> Has introducido: 69

En este caso si presionamos más teclas seria en vano, como el buffer que estamos usando es de 5 bytes el espacio de almacenamiento es limitado.

Si introducimos unicamente A, B y C veríamos:

> Introduce cinco caracteres...
ABC
> Has introducido: 65
> Has introducido: 66
> Has introducido: 67
> Has introducido: 13
> Has introducido: 10

Los valores 65, 66 y 67 corresponden a las teclas presionadas A, B y C respectivamente. Pero también obtenemos otros valores por presionar la tecla Intro en la consola del sistema (en este caso de Windows), el valor 13 representa un CR (Carriage Return: retorno de carro, el fin de una línea) y el valor 10 representa un LF (Line Feed: salto de línea, una nueva línea). Si se ejecuta desde la consola de un sistema UNIX o en la consola de un IDE solo se obtendrá el 10 (LF) ya que diferentes sistemas utilizan diferentes formas de terminar una línea.

Para leer bytes indefinidamente en el próximo ejemplo capturaremos todo lo que se escriba usando de nuevo int read() pero en un bucle while:

...
System.out.println("> Introduce lo que quieras...");

int in = 0;
while (in != -1) {
    in = System.in.read();
    System.out.println("> Has introducido: " + in);
}
...

Introduciendo cualquier cosa veremos los valores de cada carácter junto a los valores adicionales de cada pulsación de Intro. El bucle solo se detendrá hasta recibir el valor -1 que representa un EOF (End Of File: el fin de un archivo) y como la entrada esta asociada al teclado y no a un archivo lo que haremos es emular un EOF, en la consola de Windows esto se puede hacer presionando CTRL + Z y en sistemas UNIX presionando CTRL + D. Si no logras detenerlo simplemente finaliza el proceso desde el administrador de tareas.

La salida en mi caso:

> Introduce lo que quieras...
A
> Has introducido: 65
> Has introducido: 13
> Has introducido: 10
a
> Has introducido: 97
> Has introducido: 13
> Has introducido: 10
ABCD
> Has introducido: 65
> Has introducido: 66
> Has introducido: 67
> Has introducido: 68
> Has introducido: 1
> Has introducido: 10
escribo algo!
> Has introducido: 101
> Has introducido: 115
> Has introducido: 99
> Has introducido: 114
> Has introducido: 105
> Has introducido: 98
> Has introducido: 111
> Has introducido: 32
> Has introducido: 97
> Has introducido: 108
> Has introducido: 103
> Has introducido: 111
> Has introducido: 33
> Has introducido: 13
> Has introducido: 10
^Z
> Has introducido: -1

Antes de J2SE 5.0 - Lectura de caracteres con clases de tipo Reader

La clase Reader es la clase base en el paquete java.io para manejar los flujos entrantes de caracteres (16-bit), de esta clase heredan varias clases cuyas implementaciones se pueden utilizar en combinación con otras clases para diferentes propósitos. De estas implementaciones tomaremos para combinar con el InputStream (que representa la entrada estándar) la clase InputStreamReader que convierte bytes en caracteres. Véase:

...
System.out.println("> Introduce un caracter...");

InputStreamReader isr = new InputStreamReader(System.in);
int in = isr.read();

System.out.println("> Has introducido: " + in);
...

Si introducimos A y pulsamos Intro, la salida:

> Introduce un caracter...
A
> Has introducido: 97

El método int read() de InputStreamReader mantendrá la aplicación bloqueada en espera de que se introduzca algo en la entrada estándar para poder continuar, el resultado de presionar A como mayúscula es: 65, si hubiera sido en minúsculas seria: 97. Hasta aquí parece lo mismo que utilizar el método int read() directamente desde InputStream como vimos anteriormente, pero en este caso el método int read() lee concretamente un carácter y lo devuelve representado como un int (número entero)  con un valor entre 0 - 65535 (notar que el rango es mucho mayor, esto es Unicode), y -1 en caso de que se llegue al EOF (End Of File: fin de un archivo).

Como los caracteres ASCII (y también el superset de ASCII ISO Latin 1) están incluidos en Unicode esperamos el mismo resultado de ambos métodos int read() siempre que usemos caracteres normales que están cubiertos por el formato ASCII. Pero ¿porque int read() en InputStreamReader no devuelve directamente el char?, posiblemente sea para poder devolver el -1 que significa un EOF ya que este no tiene un carácter que lo represente.

Podemos hacer la conversión de int a char como hicimos antes:

...
System.out.println("> Introduce algo...");
InputStreamReader isr = new InputStreamReader(System.in);

int in = isr.read();
char c = (char) in;

System.out.println("> Has introducido: " + c);
...

La salida es:

> Introduce algo...
A
> Has introducido: A

La diferencia es que en este caso es mas confiable y adecuado realizar esta conversión porque estamos trabajando con el  rango de caracteres Unicode, aunque tratando con caracteres normales (como lo es "A") no notemos ninguna diferencia.

Si quisiéramos leer mas de un carácter podemos intentar con el método int read(char[] cbuf), véase:

...
System.out.println("> Introduce cinco caracteres...");

InputStreamReader isr = new InputStreamReader(System.in);
char[] bufferIn = new char[5];
isr.read(bufferIn);

for (int i = 0 ; i < bufferIn.length ; i++) {
    System.out.println("> Has introducido: " + bufferIn[i]);
}
...

Si introducimos A, B, C, D, E y presionamos Intro, la salida:

> Introduce cinco caracteres...
ABCDE
> Has introducido: A
> Has introducido: B
> Has introducido: C
> Has introducido: D
> Has introducido: E

En este caso si presionamos más teclas seria en vano, como el buffer que estamos usando es de 5 bytes el espacio de almacenamiento es limitado.

Lo nuevo aquí es que ahora efectivamente tenemos los caracteres y no sus representaciones en int. Si introducimos solo ABC veríamos:

> Introduce cinco caracteres...
ABC
> Has introducido: A
> Has introducido: B
> Has introducido: C
> Has introducido:
> Has introducido:


Ya podemos ver los caracteres A, B y C. En este caso los otros valores que obtenemos por presionar Intro (CR y LF, en Windows) no son visibles como un carácter, pero el que demuestra su presencia es LF que provoca una nueva línea en la salida de la consola.

Para leer caracteres indefinidamente en el próximo ejemplo capturaremos todo lo que se escriba utilizando de nuevo int read() pero en un bucle while, y como el método int read() aquí también retorna una representación en int para cada carácter realizaré la conversión de int a char:

...
System.out.println("> Introduce lo que quieras...");

InputStreamReader isr = new InputStreamReader(System.in);
int in = 0;

while (in != -1) {
    in = isr.read();
    char c = (char) in;
    System.out.println("> Has introducido: " + c);
}
...

Introduciendo cualquier cosa veremos cada carácter, un espacio en blanco (en Windows) y una nueva línea con cada pulsación de Intro. El bucle solo se detendrá hasta recibir el valor -1 que representa un EOF (End Of File: el fin de un archivo), como hicimos anteriormente tenemos que emular un EOF. Si no lo logras simplemente finaliza el proceso desde el administrador de tareas.

La salida en mi caso:

> Introduce lo que quieras...
A
> Has introducido: A
> Has introducido:
> Has introducido:

ABCDE
> Has introducido: A
> Has introducido: B
> Has introducido: C
> Has introducido: D
> Has introducido: E
> Has introducido:
> Has introducido:

escribo algo!
> Has introducido: e
> Has introducido: s
> Has introducido: c
> Has introducido: r
> Has introducido: i
> Has introducido: b
> Has introducido: o
> Has introducido:
> Has introducido: a
> Has introducido: l
> Has introducido: g
> Has introducido: o
> Has introducido: !
> Has introducido:
> Has introducido:^Z

> Has presionado: ?

Notar que el -1 en su conversión a char resulta en el símbolo "?", esto sucede cuando una entrada no puede ser representada por el formato de caracteres.

La forma normalmente más cómoda, eficiente y utilizada (al menos de los recursos que tenemos antes del J2SE 5.0) para leer caracteres desde el teclado es combinando también otra de las clases que heredan de Reader, se trata de la clase BufferedReader y utilizar su método String readLine().

Construiremos un objeto InputStreamReader envuelto en un BufferedReader. Véase:

...
System.out.println("> Introduce algo...");

InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr);

String s = br.readLine();

System.out.println("> Has introducido: " + s);
...

La salida es:

> Introduce algo...
Esto es una linea!
> Has introducido: Esto es una linea!

El método String readLine() de BufferedReader mantendrá la aplicación bloqueada en espera de que se introduzca algo en la entrada estándar para poder continuar, este método lee líneas de texto hasta que terminen en un LF, CR u ambos. Esta una forma muy practica de leer y trabajar con strings en Java ya que es cómodo y adecuado al lenguaje, no obstante si esperamos algún tipo de número debemos de hacer las conversiones correspondientes, por ejemplo:

...
System.out.println("> Introduce algo...");

InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr);

String s = br.readLine();
int n = Integer.parseInt(s);

System.out.println("> Has escrito: " + n);
...

La salida de este ejemplo seria exactamente igual que el caso anterior, pero por alguna razón suponiendo que estamos trabajando con números y necesitáramos hacer algún calculo aritmético no podíamos mantener como String nuestro tipo de dato obtenido, por eso mediante el método estático int parseInt(String s) de la clase Integer se convierte nuestro String en int.

Desde J2SE 5.0 - Lectura de caracteres con java.util.Scanner

Desde J2SE 5.0 se introduce la clase Scanner en el paquete de utilidades java.util. Esta clase toma en cuenta un delimitador para procesar la entrada del flujo de datos en sub-cadenas, es muy flexible y ya posee una gran variedad de métodos para diferentes necesidades. Para utilizar esta clase solo tenemos que construir un objeto Scanner pasándole System.in como argumento, el flujo de bytes es convertido a caracteres en el formato de caracteres por defecto del sistema, aunque la clase también posee otros constructores para especificar el formato de preferencia.

A continuación algunos ejemplos del uso de esta clase:

  • Leyendo una línea de texto:
...
System.out.println("> Introduce una linea de texto...");

Scanner sc = new Scanner(System.in);
String s = sc.nextLine();

System.out.println("> Has introducido: " + s);
...

Salida:

> Introduce una linea de texto...
Hola a todos
> Has introducido: Hola a todos
  • Leyendo un número entero:
...
System.out.println("> Introduce un numero entero...");

Scanner sc = new Scanner(System.in);
int i = sc.nextInt();

System.out.println("> Has introducido: " + i);
...

Salida:

> Introduce un numero entero...
14
> Has introducido: 14

También hay muchos otros métodos que se pueden utilizar, la clase Scanner es muy potente y merece su propio tutorial. Pero teniendo en cuenta estos ejemplos lo que falta es el control de excepciones, sobretodo la excepción InputMismatchException que siempre es generada cuando un método recibe un tipo de dato que no puede manejar. Aunque no estemos obligados a tratarla deberíamos de hacerlo, por ejemplo si esperamos como en el segundo ejemplo un número entero con int nexInt() pero introducimos un número largo se generaría esta excepción.

Una forma más correcta de esperar el número entero:

...
System.out.println("> Introduce un numero entero...");

Scanner sc = new Scanner(System.in);
int i = -1;

try {
    i = sc.nextInt();
    System.out.println("> Has introducido: " + i);
} catch (InputMismatchException imme) {
    System.out.println("> El dato ingresado no es valido!");
}
...

La salida luego de haber ingresado un número largo:

> Introduce un número entero...
111111111111111111111111111111111
> El dato ingresado no es valido!

En general cuando se lee desde la entrada estándar y se pretende obtener algún dato especifico la clase Scanner puede reemplazar la utilización de BufferedReader simplificando el trabajo de lectura.

Re-definir la entrada estándar

No es algo habitual, pero se puede re-definir la entrada estándar con el método estático void setIn(InputStream in) de la clase System.


Ejemplos:
* Java – Clase java.io.BufferedReader – Lectura de entrada estándar (teclado)
* Java – Clase java.util.Scanner – Lectura de entrada estándar (teclado)

Documentación:
* Java™ Platform, Standard Edition 6 API Specification


, , , , , , ,

  1. #1 por Ing. Juan Mancilla el diciembre 23, 2012 - 3:09 pm

Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Seguir

Recibe cada nueva publicación en tu buzón de correo electrónico.

%d personas les gusta esto: