De nada o de poco servirían los programas si no se pudiera interactuar con ellos y procesar datos personalizados. La lectura y escritura de datos dan sentido al desarrollo de una aplicación, ya que sin esto se tendrían programas que hicieran siempre lo mismo sin ningún valor agregado.
Existen diferentes fuentes de información de las cuales se puede proveer datos a un programa, pero las dos primordiales son los datos que introduce y visualiza directamente un usuario. Es decir, lectura de datos desde el teclado o dispositivo de entrada default y la visualización de la información en pantalla como método de salida, así como la lectura y escritura de datos utilizando archivos que son el mecanismo básico para mantener la información a largo plazo.
Los datos pueden provenir de fuentes tan variadas como una pantalla de celular, la interfaz de una impresora, un escáner o desde otras aplicaciones de Internet, por ejemplo. De la misma manera se pueden visualizar en pantalla, mandar a impresión o enviar a través de Internet a un sinfín de destinos posibles.
Todas comparten los mismos principios, por lo que entender los más elementales permitirán comprender el resto de manera más intuitiva una vez se vuelva necesario.
En muchos de los lenguajes de programación modernos, Java incluido, la lectura y escritura de datos utiliza un mecanismo de interfaces que abstraen la complejidad de las diferentes fuentes y destinos de información, de manera que un programa promedio puede ser escrito de manera que simplemente espere información de una fuente y la envíe a un destino.
Lectura de datos del teclado.
La entrada de datos en la mayoría de las computadoras personales es el teclado y es por ello Java ofrece de clases predefinidas que permiten realizar esta tarea de una manera simple y enfocarse principalmente en la lógica del programa.
Java ofrece un objeto que encapsula toda la lógica necesaria para leer del teclado y es el objeto System.in que deberá ser usado como parámetro de entrada en las clases de lectura para indicar la fuente.
System.in |
Tabla 1. Objeto que representa al teclado como fuente de datos.
Una de las primeras clases especializadas en la lectura es la clase BufferedReader que permite crear un objeto para la lectura desde la fuente que se le haya indicado, en este caso, desde el teclado.
BufferedReader lector = new BufferedReader(new InputStreamReader(System.in)) |
Tabla 2. Ejemplo de creación de un objeto BufferedReader para la lectura del teclado.
Es importante notar cómo el objeto para lectura no se inicializa directamente indicando la fuente, sino que también se requiere de un objeto intermedio de tipo InputStreamReader.
Esto lleva a explicar el concepto de Stream que indica cómo su traducción al español lo indica, un “flujo”. Los flujos pueden ser de entrada o de salida y funcionan como compuertas que permiten la transferencia de información de una fuente hacia un destino. Así, se puede inferir a partir del nombre de la clase InputStreamReader que se trata de una clase especializada en lectura de flujos de entrada (Darwin, 2020).
Por otro lado, es importante destacar que todos los flujos de datos que se abren en Java se deben cerrar explícitamente mediante el método close(). Esto típicamente se realiza dentro de un bloque finally de la estructura try-catch para manejo de excepciones. Sin embargo, el método close() también puede fallar y generar una excepción de lectura, lo que complica el garantizar la integridad de los datos.
Para esto se desarrolló la estructura try with resources que garantiza que los objetos que se creen dentro de esta y que implementen la interfaz AutoClosable siempre cerrarán los flujos de lectura o escritura abiertos.
try( String lectura = lector.readLine(); } catch(IOException excepcion){ |
Tabla 3. Ejemplo de creación de un objeto BufferedReader, dentro de una estructura try with resources.
Para definir correctamente un objeto auto closable, como lo son los que manejan flujos de lectura o escritura, basta con indicar la declaración e inicialización dentro de paréntesis que se posicionan delante de la palabra reservada try y antes de la llave ‘{‘ de apertura.
Sin embargo, Java ofrece una clase que simplifica las cosas en lo que a lectura se refiere, pues encapsula mucha de esta complejidad y ofrece una serie de métodos que convenientemente permiten leer los datos del teclado en diferentes formatos. Esta es la clase Scanner.
Scanner scanner = new Scanner(System.in); |
Tabla 4. Ejemplo de creación de un objeto de tipo Scanner para lectura.
Para crear un objeto de la clase Scanner basta con indicar la fuente de lectura y el resto sucede internamente. Posterior a eso se puede iniciar con la lectura de datos.
Esta clase ofrece varios métodos para leer los datos del teclado, dentro de los que destacan los siguientes (Oracle 2021-a):
Tabla 5. Métodos relevantes de la clase Scanner.
Escritura de datos y mensajes a pantalla.
Aunque las aplicaciones modernas rara vez mandan mensajes directamente a la pantalla en formato de texto simple, en ocasiones es necesario desarrollar aplicaciones que trabajen directamente en una terminal.
Java ofrece un par de alternativas bastante prácticas para imprimir en pantalla, texto sin formato y texto con formato.
En el caso del texto sin formato, seguramente ya conozcas a estas alturas los métodos print() y println() que se encuentran en el objeto System.out y que te permite imprimir cualquier cantidad de mensajes y soporta una gran variedad de tipos de dato como parámetros.
En caso de que quieras mandar mensajes con formato, puedes utilizar el método printf() que te permite indicar cosas como el tipo de dato que se está enviando a impresión o la cantidad de decimales que tendrá un número de tipo float o double (Darwin, 2020).
System.out.printf("%04d - the year of %f%n", 1956, Math.PI); |
Tabla 6. Ejemplos de impresión de texto usando el método printf.
Este método ofrece una gran variedad de opciones para especificar el tipo de dato y el formato a seguir.
Tabla 7. Códigos de formato usados en el método printf.
Lectura y escritura de datos en archivos.
La persistencia de los datos permite generar información estadística que pone las cosas en contexto para tomar mejores decisiones, por lo que almacenar y recuperar datos es una tarea indispensable. Una alternativa directa para solucionar este problema es el manejo de archivos.
En Java, el manejo de archivos se realiza mediante las clases contenidas en el paquete java.nio, de las cuales se destacan la clase Path, por un lado, que te permite interactuar con los archivos del sistema, independientemente del sistema operativo en el que corra tu programa (Oracle, 2021-c), y la clase Files, por otro lado, que contiene varios métodos con funcionalidades orientadas a facilitar el uso e interacción de archivos (Oracle, 2021-b).
ArrayList<String> list = new ArrayList<>(); Path archivo = Path.of("ArchivoEjemplo.txt"); |
Tabla 8. Ejemplo de escritura de una lista en un archivo.
En el ejemplo anterior se pueden destacar varios elementos:
La clase Path se utiliza para representar el archivo sobre el que se va a escribir y la manera de generar una instancia de esta clase es usando el método of que recibe como argumento el nombre y ubicación del archivo. No incluir una ruta para el archivo significa que este se encuentra en el directorio raíz del programa. Como se trata de una interfaz, no se puede crear una instancia directamente y es esta la razón para utilizar métodos auxiliares (Boyarsky, J. 2020).
Posteriormente, es necesario generar una instancia de la clase BufferedWriter para poder escribir contenido dentro del archivo. Esta instancia la puedes generar usando el método newBufferedWriter, perteneciente a la clase Files y que recibe la instancia de la clase Path, creada previamente. En caso de que el archivo no exista, se creará automáticamente (este es el comportamiento default para las operaciones de escritura). El objeto de tipo BufferedWriter resultante tendrá como destino de escritura el archivo referenciado y cuenta con métodos de escritura tales como write() que agregará al archivo lo que se indique como parámetro. En este caso es cada uno de los elementos de la lista de de tipo String; es importante que notes que para agregar un salto de línea es necesario llamar también el método newLine() (Oracle, 2021-e).
Como aprendiste previamente, los objetos especializados en abrir flujos de lectura o escritura deben ser cerrados una vez que se termine su uso para asegurar la integridad de los archivos. No olvides que la mejor manera de asegurarte de esto es usando la estructura try with resources, como se muestra en el ejemplo e incluyendo el manejo de la IOException dentro del bloque catch de la misma estructura.
Por otro lado, para que el almacenar información en archivos tenga mayor sentido, debes saber cómo recuperar dicha información y, para ello, necesitas otro tipo de flujo de datos; en este caso será un flujo de lectura.
Path archivo2 = Path.of("ArchivoEjemplo.txt"); try (BufferedReader lector = Files.newBufferedReader(archivo2)) { |
Tabla 9. Ejemplo de lectura de todas las líneas de un archivo de texto.
Como se observa en la tabla anterior, se necesita nuevamente de un objeto de tipo Path que haga referencia al archivo que se quiere leer. En este caso se debe crear una instancia de la clase BufferedReader para realizar la lectura del archivo; esta instancia la puedes obtener por conveniencia de la clase Files mediante el método newBufferedReader que recibe como parámetro la instancia de tipo Path creada previamente (Oracle, 2021-d).
Una vez que tengas el objeto para lectura puedes leer línea por línea mediante el método readLine(). La manera más práctica de realizar esto es incluyendo la lectura dentro de un ciclo while. Una vez que alcances el final del archivo este método devolverá el valor null, indicando que no hay más información en el archivo, por lo que puedes usar este criterio como condición de término del ciclo while.
Al igual que con la lectura, para proteger los flujos de lectura debes incluir este código dentro de una estructura try with resources y manejando la excepción tipo IOExcepcion dentro del bloque catch.
try { |
Tabla 10. Ejemplo de lectura de todas las líneas de un archivo de texto usando expresiones lambda.
Como dato extra, en la tabla anterior puedes ver cómo realizar la misma lectura de todas las líneas disponibles en un archivo de texto con tan solo 1 línea de código.
La clase Files puede generar un flujo de lectura directamente mediante el método readAllLines, en el cual se indica la fuente de información mediante el objeto de tipo Path; después puedes ver un método llamado stream. Este no es el mismo flujo de lectura, sino que es otro concepto relacionado con programación funcional (en Java, el concepto Stream tiene dos posibles interpretaciones: por un lado, sería un flujo de lectura o escritura y, por otro, un flujo de datos relacionado con colecciones, como listas, arreglos y otros); enseguida, se usa el método foreach() para indicar, mediante el parámetro de entrada, cuál será la operación realizada sobre cada uno de los elementos; en este caso, cada línea del archivo.
Esta alternativa hace uso de una combinación entre programación orientada a objetos y programación funcional, ambos estilos soportados por el lenguaje Java, siendo esta la tendencia más moderna para desarrollo de aplicaciones.
La lectura y escritura de datos es un elemento esencial para la versatilidad y reutilización de un programa. Es gracias a esta función que puedes crear programas que permitan al usuario interactuar con el mismo. Es por tanto indispensable que comprendas las diferentes y mejores maneras para realizarlo.
¿Te imaginas cómo serían las aplicaciones si no pudieras asignar valores de manera dinámica? ¿Tendrían algún sentido si no pudieras publicar los resultados?
Asegúrate de: