7.4.-Lectura/Escritura Archivos
7.4. Lectura y escritura de archivos¶
Normalmente las aplicaciones que utilizan archivos no están centradas en la gestión del sistema de archivos del ordenador. El objetivo principal de usar archivos es poder almacenar datos de modo que entre diferentes ejecuciones del programa, incluso en diferentes equipos, sea posible recuperar los datos almacenados. El caso más típico es un editor de documentos, que mientras se ejecuta se encarga de gestionar los datos relativos al texto que está escribiendo, pero en cualquier momento puede guardarlo en un archivo para poder recuperar este texto cuando se desee, y añadir otros nuevos si fuera necesario. El archivo con los datos del documento lo puede abrir tanto en el editor de su ordenador como en el de otro compañero.
Para saber cómo tratar los datos de un archivo en un programa, hay que tener muy claro cómo se estructuran. Dentro de un archivo se pueden almacenar todo tipo de valores de cualquier tipo de datos. La parte más importante es que estos valores se almacenan en forma de secuencia, uno tras otro. Por lo tanto, como pronto veréis, la forma más habitual de tratar archivos es secuencialmente, de forma parecida a como se hace para leer los datos desde teclado, mostrarlas por pantalla o recorrer las posiciones de un array.
Se denomina acceso secuencial al procesamiento de un conjunto de elementos de manera que sólo es posible acceder a ellos de acuerdo a su orden de aparición. Para procesar un elemento es necesario procesar primero todos los elementos anteriores.
Kotlin, junto con otros lenguajes de programación, diferencia entre dos tipos de archivos según cómo se representan los valores almacenados en un archivo.
En los archivos orientados a carácter, los datos se representan como una secuencia de cadenas de texto, donde cada valor se diferencia del otro usando un delimitador. En cambio, en los archivos orientados a byte, los datos se representan directamente de acuerdo a su formato en binario, sin ninguna separación. Estos últimos archivos son no son legibles a simple vista, y son interpretados por programas que entienden su formato. Por ejemplo, pdf, doc, xls.
Nos centraremos principalmente en el procesamiento de archivos orientados a carácter.
1. Archivos orientados a carácter¶
Un archivo orientado a carácter no es más que un documento de texto, como el que podría generar con cualquier editor de texto simple. Los valores están almacenados según su representación en cadena de texto, exactamente en el mismo formato que ha usado hasta ahora para entrar datos desde el teclado. Del mismo modo, los diferentes valores se distinguen al estar separados entre ellos con un delimitador, que por defecto es cualquier conjunto de espacios en blanco o salto de línea. Aunque estos valores se puedan distribuir en líneas de texto diferentes, conceptualmente, se puede considerar que están organizados uno tras otro, secuencialmente, como las palabras en la página de un libro.
El siguiente podría ser el contenido de un archivo orientado a carácter donde hay diez valores de tipo float
, 7 en la primera línea y 3 en la segunda:
Y este el de un archivo con 3 valores de tipo String
: "Había"
, "una"
y "vez..."
en una línea.
En un archivo orientado a carácter es posible almacenar cualquier combinación de datos de cualquier tipo (int
, double
, boolean
, String
, etc.).
La principal ventaja de un archivo de este tipo es que resulta muy sencillo inspeccionar su contenido y generarlos de acuerdo a nuestras necesidades.
Para el caso de los archivos orientados a carácter, hay que usar dos clases diferentes según si lo que se quiere es leer o escribir datos en un archivo. Normalmente esto no es muy problemático, ya que en un bloque de código dado solo se llevarán a cabo operaciones de lectura o de escritura sobre un mismo archivo, pero no los dos tipos de operaciones a la vez.
Una diferencia importante a la hora de tratar con archivos respecto a leer datos del teclado es que las operaciones de lectura no son producto de una interacción directa con el usuario, que es quien escribe los datos. Solo se puede trabajar con los datos que hay en el archivo y nada más. Esto tiene dos efectos sobre el proceso de lectura:
- Por un lado, recuerda que cuando se lleva a cabo el proceso de lectura de una secuencia de valores, siempre hay que tener cuidado de usar el método adecuado al tipo de valor que se espera que venga a continuación . Qué tipo de valor se espera es algo que habréis decidido vosotros a la hora de hacer el programa que escribió ese archivo, por lo que es vuestra responsabilidad saber qué hay que leer en cada momento. De todos modos nada garantiza que no se haya cometido algún error o que el archivo haya sido manipulado por otro programa o usuario. Como operamos con archivos y no por el teclado, no existe la opción de pedir al usuario que vuelva a escribir el dato. Por lo tanto, el programa debería decir que se ha producido un error ya que el archivo no tiene el formato correcto y finalizar el proceso de lectura.
- Por otra parte, también es necesario controlar que nunca se lean más valores de los que hay disponibles para leer. En el caso de la entrada de datos por el teclado el programa simplemente se bloqueaba y espera a que el usuario escribiera nuevos valores. Pero con archivos esto no sucede. Intentar leer un nuevo valor cuando el apuntador ya ha superado el último disponible se considera erróneo y lanzará una excepción. Para evitarlo, habrá que utilizar algún procedimiento que nos permita saber si se ha llegado al final de archivo en vez de suponer que siguen existiendo datos que leer.
1.1. Lectura de archivo¶
En Kotlin demos leer el contenido de un archivo utilizando los métodos estándar de la clase java.io.File
o los métodos que proporciona Kotlin como una extensión de java.io.File
.
Examinaremos programas de ejemplo para los métodos de extensión, proporcionados por Kotlin a la clase java.io.File
de Java, para leer el contenido de un archivo.
Usar java.io.File.bufferedReader()
de Java¶
BufferedReader
lee texto desde un flujo de entrada de caracteres, almacenando los caracteres para proporcionar una lectura eficiente de caracteres, arreglos y líneas.
Se puede configurar específicamente el tamaño del buffer, o usar el que se otorga por default, el cual es suficientemente grande para la mayoría de los casos.
Dado que esta clase extiende de Reader
, cada petición de lectura causa una petición de lectura del flujo de entrada, por lo que es aconsejable envolverla con la clase InputStreamReader
o FileReader
, según el propósito de la lectura.
A continuación podemos ver cómo leer el contenido de un archivo en BufferedReader
, El proceso es el siguiente:
- Prepare el objeto
File
con la ubicación del archivo pasado como argumento al constructor de la clase deFile
. File.bufferedReader
devuelve un nuevoBufferedReader
para leer el contenido del archivo.- Utilice
BufferedReader.readLines()
para leer el contenido del archivo.
Un ejemplo
import java.io.File
fun main(args: Array<String>) {
val file = File("input" + File.separator + "contents.txt")
val bufferedReader = file.bufferedReader()
val text: List<String> = bufferedReader.readLines()
for (line in text) {
println(line)
}
}
El contenido del archivo se imprime en la consola.
Usar java.io.File.forEachLine()
de Kotlin¶
Lee un archivo línea por línea en Kotlin. El proceso es el siguiente:
- Prepare el objeto
File
con la ubicación pasada como argumento al constructor de la clase deFile
. - Use la función
File.forEachLine
y lea cada línea del archivo.
Un ejemplo
import java.io.File
fun main(args: Array<String>) {
val file = File("input" + File.separator + "contents.txt")
file.forEachLine { println(it) }
}
El contenido del archivo se imprime en la consola.
Oros métodos de lectura¶
Existen otras formas de leer archivos:
File.inputStream().readBytes()
: Lee el contenido del archivo en InputStreamFile.readBytes()
: devuelve todo el contenido del archivo como ByteArrayFile.readLines()
: devuelve todo el contenido del archivo como una lista de líneasFile.readText()
: devuelve todo el contenido del archivo como una sola cadenajava.util.Scanner
: permite leer indicando el tipo de dato a leer.
1.2. Escritura en archivo¶
Con en el lenguaje de programación Kotlin tambien se puede escribir en un archivo. Por lo general, en los archivos orientados a caracteres se escriben cadenas de texto.
Igual que para la lectura, haciendo uso de Kotlin podremos escribir en un archivo usando las funciones de extensión proporcionadas por Kotlin o también puede usar el código Java existente que escribe contenido en un archivo.
A continuación veremos ejemplos de cómo usar clases de Java como PrintWriter
para escribir en un archivo y más ejemplos usando funciones de extensión de Kotlin.
Usar java.io.File.bufferedWriter
¶
Podemos usar la función de extensión java.io.File.bufferedWriter()
para obtener el objeto de escritura y luego usar la función write()
en el objeto de escritura para escribir contenido en el archivo.
- Tenga su contenido como una cadena.
- Pase el nombre del archivo al constructor de archivos (
File
). - Luego llame al método
bufferedWriter()
de la claseFile
. - Haciendo uso de la función
use()
(Veremos que ventajas nos proporciona hacer uso de ella), llama al métodowriter(content)
del bufer escritor devuelto porbufferedWriter()
, y que se encarga de escribir el contenido en el archivo.
import java.io.File
/**
* Example to use File.bufferedWriter() in Kotlin to write content to a text file
*/
fun main(args: Array<String>) {
// content to be written to file
var content = "Hello World. Welcome to Kotlin!!"
// write content to file
File("file.txt").bufferedWriter().use { out ->
out.write(content)
}
}
Aplicamos la función
use()
para garantizar que todos los recursos se liberen correctamente cuando hayamos terminado
Usar java.io.File.writeText()
¶
Si está escribiendo exclusivamente texto en un archivo, puede usar la función de extensión java.io.File.writeText()
.
En el siguiente ejemplo, hemos usado esta función de extensión de kotlin para escribir texto en un archivo.
import java.io.File
/**
* Example to use File.writeText in Kotlin to write text to a file
*/
fun main(args: Array<String>) {
// content to be written to file
var content = "Hello World. Welcome to Kotlin!!"
// write content to file
File("file.txt").writeText(content)
}
Usar java.io.File.printWriter
¶
En este ejemplo, usaremos la función de extensión de Kotlin printWriter()
para la clase java.io.File
. El siguiente es el proceso para escribir en el archivo.
- Tenga su contenido como una cadena.
- Pase el nombre del archivo al constructor de archivos (
File
). - Luego llame al método
printWriter()
de la claseFile
. - Haciendo uso de la función
use()
(Veremos que ventajas nos proporciona hacer uso de ella), llama al métodoprintln(content)
del escritor devuelto porprintWriter()
, y que se encarga de escribir el contenido en el archivo.
import java.io.File
/**
* Example to use File.printWriter in Kotlin to write content to a text file
*/
fun main(args: Array<String>) {
// content to be written to file
var content = "Hello World. Welcome to Kotlin!!"
// write content to file
File("file.txt").printWriter().use { out ->
out.println(content)
}
}
Usar java.io.PrintWriter
¶
En este ejemplo, tomamos una cadena y la escribimos en un archivo usando la clase java.io.PrintWriter
. Para ello se siguen los siguientes pasos.
- Tenga sus datos listos como una cadena en una variable.
- Inicialice un objeto escritor de la clase
PrintWriter
. - Agregue la cadena al archivo usando la función
PrintWriter.append()
. - Cerrar el escritor.
import java.io.PrintWriter
/**
* Example to use standard Java method in Kotlin to write content to a text file
*/
fun main(args: Array<String>) {
// content to be written to file
var content = "Hello World. Welcome to Kotlin!!"
// using java class java.io.PrintWriter
val writer = PrintWriter("file.txt")
writer.append(content)
writer.close()
}
En los ejemplso, se creará un nuevo archivo con el nombre file.txt
, como se especifica para el argumento de PrintWriter()
, con el contenido. Si el archivo ya está presente, primero se borra el contenido del archivo y luego se escribe el nuevo contenido en el archivo.
Oros métodos de escritura¶
Existen otras formas de leer archivos:
java.io.FileWriter
: Escribe en un archivo haciendo uso del métodowriter()
.
2. Archivos binarios.¶
Los Data Stream (Flujos de datos) se utilizan para escribir datos binarios. DataOutputStream
escribe datos binarios de tipos primitivos(Int
, Long
, String
) mientras que DataInputStream
lee datos del flujo binario y los convierte en tipos primitivos.
A continuación veremos un programa de ejemplo que escribe datos en un archivo y luego los vuelve a leer a memoria para finalmente imprimirlos por salida estándar.
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.FileInputStream
import java.io.FileOutputStream
fun main(args : Array<String>){
val burgers = "data.burgers"
//Open the file in binary mode
DataOutputStream(FileOutputStream(burgers)).use { dos ->
with(dos){
//Notice we have to write our data types
writeInt("Bob is Great\n".length) //Record length of the array
writeChars("Bob is Great\n") //Write the array
writeBoolean(true) //Write a boolean
writeInt("How many burgers can Bob cook?\n".length) //Record length of array
writeBytes("How many burgers can Bob cook?\n") //Write the array
writeInt(Int.MAX_VALUE) //Write an int
for (i in 0..5){
writeByte(i) //Write a byte
writeDouble(i.toDouble()) //Write a double
writeFloat(i.toFloat()) //Write a float
writeInt(i) //Write an int
writeLong(i.toLong()) //Write a long
}
}
}
//Open a binary file in read mode. It has to be read in the same order
//in which it was written
DataInputStream(FileInputStream(burgers)).use {dis ->
with (dis){
val bobSize = readInt() //Read back the size of the array
for (i in 0 until bobSize){
print(readChar()) //Print the array one character at a time
}
println(readBoolean()) //Read a boolean
val burgerSize = readInt() //Length of the next array
for (i in 0 until burgerSize){
print(readByte().toChar()) //Print array one character at a time
}
println(readInt()) //Read an int
for (i in 0..5){
println(readByte()) //Read a byte
println(readDouble()) //Read a double
println(readFloat()) //Read a float
println(readInt()) //Read an int
println(readLong()) //Read a long
}
}
}
}
El programa crea un objeto FileOutputStream
, para ello pasa el nombre del archivo a su constructor. Luego, el objeto FileOutputStream
se pasa como parámetro al constructor de DataOutputStream
.
Hacemos uso de la función use()
para garantizar que todos los recursos se liberen correctamente cuando hayamos terminado. El archivo ahora está abierto para escritura en modo binario.
Cuando deseamos usar el mismo objeto repetidamente, podemos pasarlo a la función with()
. En nuestro caso, tenemos la intención de seguir usando nuestro objeto DataOutputStream
, por lo que en la línea 11, lo pasamos a la función with()
. Dentro de la función with()
, todas las llamadas a métodos apuntarán al objeto dos
ya que se proporcionó a with()
como parámetro.
Cuando deseamos usar un mismo objeto repetidamente, podemos pasarlo a la función
with()
. Cuando un objeto es pasado a la funciónwith()
, dentro de esta, todas las llamadas a métodos apuntarán al objeto que se le ha pasado por parámetro.
Siguiendo con el ejemplo, dado que tenemos la intención de escribir un String
en el archivo, necesitamos registrar la longitud de la cadena, ya que de otra forma no sabriamos cuantos bytes se han escrito. Hacemos esto usando la función writeInt
y pasándole la longitud de nuestra cadena. Luego podemos usar writeChars()
para escribir un string, puesto que el argumento String
se convierte en una matriz de caracteres. Finalmente, llamamos a writeBoolean()
para escribir valores true
/false
en el archivo.
La siguiente sección es una repetición de la primera. Tenemos la intención de escribir otro String
en el archivo, pero al hacerlo, necesitamos registrar la longitud en el archivo. Una vez más, recurrimos a writeInt()
para registrar un valor int
. En la siguiente línea, usamos writeBytes()
en lugar de writeChars()
para demostrar cómo podemos escribir una matriz de bytes en lugar de una cadena. La clase DataOutputStream
se ocupa de los detalles de convertir un String
en una matriz de bytes. Finalmente, escribimos otro valor int en la secuencia.
A continuación, se ejecuta un ciclo for
en la línea 21. Dentro del ciclo for
, demostramos como escribir diferentes tipos primitivos en el archivo. Podemos usar writeByte()
para un byte
, writeDouble()
para un double
, y así sucesivamente para cada tipo primitivo. La clase DataOutputStream
conoce el tamaño de cada tipo primitivo y escribe el número correcto de bytes para cada primitivo.
Cuando terminamos de escribir el objeto, lo abrimos nuevamente para leerlo. La línea 33 crea un objeto FileInputStream
que acepta la ruta al archivo en su constructor. El objeto FileInputStream
está encadenado a DataInputStream
pasándolo al constructor de DataInputStream
. Aplicamos la función use()
para garantizar que todos los recursos estén correctamente cerrados.
La lectura del archivo requiere que el archivo se lea en el mismo orden en que se escribe. Nuestra primera orden por tanto, debería ser tomar el tamaño de la matriz de caracteres que escribimos en el archivo anteriormente. Usamos readInt()
en la línea 35 seguido de un ciclo for
que termina en el tamaño de la matriz en la línea 36. Cada iteración del ciclo for
llama a readChar()
y la cadena se imprime en la consola. Cuando terminamos, leemos un booleano en la línea 39.
Nuestra siguiente matriz fue una matriz de bytes. Una vez más, necesitamos su tamaño final, por lo que llamamos a readInt()
en la línea 41. Las líneas 42-44 recorren la matriz y llaman a readByte()
hasta que finaliza el bucle. Cada byte
se convierte en un objeto de carácter mediante toChar()
. En la línea 45, leemos un int
usando readInt()
.
La parte final del programa repite el ciclo for encontrado anteriormente. En este caso, se hace uso de un bucle for
que termina después de cinco iteraciones (línea 47). Dentro de este, se llama a los métodos readByte()
, readDouble()
, readFloat()
, y así sucesivamente. Después de cada llamada se imprime el valor recuperado en la consola.
Fuente¶
- Apuntes de programación de Joan Arnedo Moreno (Institut Obert de Catalunya, IOC)
- Apuntes de programación de Natividad Prieto, Francisco Marqués y Javier Piris (E.T.S. de Informática, Universidad Politécnica de Valencia).
- Apuntes de programación de Jose Luis Comesaña.
- Create File
- Kotlinn data streams
- Read File
- Inputstream to String