Saltar a contenido

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:

1,5 0,75 −2,35 18,0 9,4 3,1416 −15,785
−200,4 2,56 9,3785

Y este el de un archivo con 3 valores de tipo String: "Había", "una" y "vez..." en una línea.

Había una vez...

En un archivo orientado a carácter es posible almacenar cualquier combinación de datos de cualquier tipo (int , double, boolean, String, etc.).

7 10 20,51 6,99
Había una vez...
true false 2020 0,1234

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:

  1. 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.
  2. 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:

  1. Prepare el objeto File con la ubicación del archivo pasado como argumento al constructor de la clase de File.
  2. File.bufferedReader devuelve un nuevo BufferedReader para leer el contenido del archivo.
  3. 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:

  1. Prepare el objeto File con la ubicación pasada como argumento al constructor de la clase de File.
  2. 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 InputStream
  • File.readBytes(): devuelve todo el contenido del archivo como ByteArray
  • File.readLines(): devuelve todo el contenido del archivo como una lista de líneas
  • File.readText(): devuelve todo el contenido del archivo como una sola cadena
  • java.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.

  1. Tenga su contenido como una cadena.
  2. Pase el nombre del archivo al constructor de archivos (File).
  3. Luego llame al método bufferedWriter() de la clase File.
  4. Haciendo uso de la función use() (Veremos que ventajas nos proporciona hacer uso de ella), llama al método writer(content) del bufer escritor devuelto por bufferedWriter(), 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.

  1. Tenga su contenido como una cadena.
  2. Pase el nombre del archivo al constructor de archivos (File).
  3. Luego llame al método printWriter() de la clase File.
  4. Haciendo uso de la función use()(Veremos que ventajas nos proporciona hacer uso de ella), llama al método println(content) del escritor devuelto por printWriter(), 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.

  1. Tenga sus datos listos como una cadena en una variable.
  2. Inicialice un objeto escritor de la clase PrintWriter.
  3. Agregue la cadena al archivo usando la función PrintWriter.append().
  4. 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étodo writer().

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ón with(), 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