FileOutputStream
FileOutputStream es uno de los OutputStream más simple que tenemos en Java, mediante el cual podemos enviar una serie de bytes hacia un archivo que se va a guardar en el sistema de archivos. Con esto también vemos que OutputStream tiene una serie de métodos estándar que podemos usar en cualquier subclase.
Voy a comenzar la parte práctica de este taller contando el sistema de entrada y salida más simple que nos podemos encontrar en Java: cuando directamente instanciamos una clase primitiva que nos permite enviar información al mundo exterior o leer información del mundo exterior.
En este caso, lo que voy a contar es el funcionamiento de las clases FileOutputStream y FileInputStream, que son las que vamos a utilizar para escribir archivos utilizando información que queramos mandar al mundo exterior, y luego leyendo archivos del mundo exterior para recuperar esa información la siguiente vez que ejecutemos el programa.
Para poder fabricar uno de estos FileOutputStreams, lo que tenemos que hacer es instanciar la clase FileOutputStream que nos encontramos en el paquete java.io. Esta clase tiene un constructor que acepta varios parámetros de distintos tipos, y le podemos indicar el nombre del archivo en el que queremos que se escriba esa información, o una referencia a un archivo previamente abierto que contendrá esa misma información. Aparte, le podremos pasar un parámetro adicional llamado append, con el cual le podremos indicar si queremos que, en el caso de que el archivo ya exista, queramos que se incorpore la información al final del archivo, o si queremos que lo trunque y vuelva al principio, y sustituya el archivo viejo por lo que vayamos a escribir en esta ocasión.
Por ejemplo, si yo quisiese escribir sobre un archivo llamado datos.bin, o datos.txt, pese a que esto es un sistema para escribir archivos binarios, podría indicarle que quiero que me escriba sobre el archivo datos.txt. Esto, lógicamente, tendrá más sentido si lo guardo en una variable como fos. De hecho, en este caso, en vez de usar var, voy a usar OutputStream, porque como esta es la clase abstracta base sobre la cual se levantan todos los OutputStreams, así me permite enseñaros todos los métodos que tenemos a nuestra disposición en cualquier OutputStream.
Un detalle que tal vez estéis apreciando es que FileOutputStream puede lanzar una excepción, y de hecho, el tipo de esta excepción va a ser FileNotFoundException. Esta excepción se va a producir cuando el sistema intente escribir sobre un archivo pero se encuentre con que no puede crear ese archivo. Por ejemplo, porque la carpeta no tenga permisos. En general, nos encontraremos que, en el caso de java.io, casi todos los métodos que tienen que ver con entrada salida podrán lanzar un IOException o alguna subclase de IOException, para señalizar que no se ha producido bien esa operación de entrada y salida. Por ejemplo, problemas de permisos, problemas para conectarse con el otro lado, o cualquier otra razón por la que pueda fallar la operación de lectura o escritura. Podríamos tratar este error utilizando algún tipo de try-catch, como voy a hacer ahora, o bien podríamos hacer un throws en la operación main, para lanzarlo hacia fuera, pero no lo estaríamos tratando correctamente.
FileOutputStream es un tipo de OutputStream, donde tenemos una serie de métodos, y los más importantes van a ser close(), flush() y write().
En el caso de close(), este lo utilizaremos cuando hayamos terminado de escribir sobre un archivo, de leer un archivo, o de utilizar en general un OutputStream. Las semánticas van a depender del tipo de OutputStream con el que estemos trabajando. Un FileOutputStream podría decirle al sistema operativo que libere los recursos de sistema para que otras aplicaciones no lo vean como archivo en uso, algo que nos puede dar algún tipo de problema. Sin embargo, como nos indica aquí, si el OutputStream no hace nada en particular, el comportamiento por defecto es no hacer nada
, porque se asume que no pasará nada.
Así que, en general, va a ser buena idea poner un close() al final de cualquier operación de entrada-salida en la que vayamos a utilizar un OutputStream. De hecho, existe una forma auxiliar de trabajar con el bloque try-catch cuando usamos un objeto que tiene un método close(), que es utilizar un bloque try-with, pero de eso os hablaré ahora en un rato. Como veis, este close() también puede lanzar errores, en este caso de tipo IOException, y lo que podemos hacer es tratarlos exactamente igual que cualquier otro tipo de error y avisar de que se ha producido un error, loguearlo o hacer cualquier otra cosa que veamos pertinente.
Lo más importante en el caso de un OutputStream va a ser llamar a la función write() cuando queramos escribir una determinada información hacia el mundo exterior a través de ese stream. Podemos hacerlo de dos formas: o bien le pasamos un único byte, que es lo que nos permite hacer la llamada que tiene como parámetro un int. Probablemente una mente perspicaz se haya dado cuenta de que el método write() acepta un int a pesar de que yo estoy diciendo byte. En realidad, los 24 bits más significativos de este número se van a ignorar. La única razón por la que acepta un int es porque así es más fácil de pasarle el valor y nos evitamos los problemas de si el número tiene signo o no tiene signo. Solo los 8 bits menos significativos del valor que le pasemos como parámetros van a ser mandados al stream. El resto van a ser descartados. Si quiero escribir el valor 44, puedo poner write(44), y eso en este caso literalmente guardará en el archivo datos.txt el byte número 44, que con una tabla ASCII podemos ver a qué caracter se refiere. O a lo mejor es un número binario puro que no tenga representación ASCII. Podemos ponerlo de forma normal, o bien podemos utilizar la sintaxis hexadecimal poniendo 0x, por ejemplo, para guardar el 0x31, que se corresponde con el caracter 1.
También existen otras formas alternativas, como puede ser proporcionar un array de bytes. Cuando vayamos a guardar múltiples valores, esta será la forma más eficiente, porque así le puede pasar el array completo al sistema operativo y la escritura será más fluida que si fuésemos byte a byte enviándole la información. Tenemos dos formas: o bien le proporcionamos el array de forma completa y lo vuelca todo, o bien le pasamos el array de bytes y le pasamos la posición inicial del array para que empiece a volcar datos a partir de esa posición, y le pasamos cuántos bytes queremos que nos envíe a ese buffer.
En el siguiente ejemplo, he hecho un bucle for que itera desde i = 0x30 hasta i = 0x39. Si consultamos una tabla ASCII, coincide justo con los valores del 0 al 9 si lo pasamos a carácter. Lo que estoy haciendo es llamar en sucesivas ocasiones a fos.write(), para proporcionarle ese valor. La consecuencia de esto es que cuando lo ejecute, se guardará dentro del archivo datos.txt los números del 0 al 9 que serán visibles desde el bloc de notas. Si lo abro, nos encontramos con un datos.txt que tiene 0123456789. Tenéis que tener en cuenta que esto es una casualidad. En este caso son valores que se corresponden con caracteres valores y corrientes. Pero si yo escribo valores como los que hay entre las posiciones 0x10 y 0x19 en hexadecimal, probablemente el resultado no tenga mucho sentido, porque son valores que no se corresponden con la tabla ASCII, y por lo tanto cualquier editor de textos lo va a mostrar con algún tipo de problema.
Un último método que nos vamos a encontrar en un OutputStream es el método flush(), el cual podemos usar en cualquier momento para señalizarle a Java que, para todos los bytes que hemos mandado hasta el momento, se asegure de mandarlos hacia el mundo exterior. Es decir, puede ocurrir que algunos OutputStreams de algún modo no envíen la información cuando llamamos a la función write(). Puede que lo mantengan localmente para poder ser más eficientes, y se espere a que haya suficiente información para mandarla toda de golpe. Esto es algo que en principio parece que tiene sentido: en vez de hacerte dar 20 viajes, tal vez es más eficiente que te lleves las 20 cosas de golpe. Java aplica este mismo principio y a veces, puede ocurrir que al llamar a write(), no se esté escribiendo nada, sino que esté esperando a que haya más información para poder escribirla toda de golpe. Cuando se llame a la función flush(), se le pedirá a Java que todo que tiene acumulado hasta el momento sea echado de golpe, de tal manera que cuando se llame a flush(), se apreciará que todo lo que no se haya enviado hasta el momento será enviado. En muchas ocasiones, será más seguro llamar a flush() justo antes del close(), o bien llamar a flush() en cualquier momento que nos queramos asegurar de que por lo menos la información ha sido enviada hasta ese punto.
Una falta de concepción que señala correctamente el Javadoc es que esto no siempre tiene todas las garantías de que va a enviar todo al mundo exterior. En particular, por ejemplo, puede ocurrir que, en el FileOutputStream, cuando se llame a la función flush() todo lo que haga es decirle al sistema operativo toma, aquí tienes todo lo que debes guardar
, pero realmente como tal, flush() no va a esperar a que esté guardado en el disco duro: dependerá del sistema operativo decir ya lo tengo
o no lo tengo
. Si escribes con un flush() sobre un archivo y justo en ese momento se va la luz, a lo mejor si se va la luz después del flush() no se ha guardado realmente en el disco duro, si el sistema operativo no había terminado de escribir las cosas.
Desplegar transcripción del episodio
[Música]
Voy a comenzar la parte práctica de este taller contando el sistema de entrada y salida más simple que nos podemos encontrar en Java: cuando directamente instanciamos una clase primitiva que nos permite enviar información al mundo exterior o leer información del mundo exterior.
En este caso, lo que voy a contar es el funcionamiento de las clases FileOutputStream y FileInputStream, que son las que vamos a utilizar para escribir archivos utilizando información que queramos mandar al mundo exterior, y luego leyendo archivos del mundo exterior para recuperar esa información la siguiente vez que ejecutemos el programa.
Para poder fabricar uno de estos FileOutputStreams, lo que tenemos que hacer es instanciar la clase FileOutputStream que nos encontramos en el paquete java.io. Esta clase tiene un constructor que acepta varios parámetros de distintos tipos, y le podemos indicar el nombre del archivo en el que queremos que se escriba esa información, o una referencia a un archivo previamente abierto que contendrá esa misma información. Aparte, le podremos pasar un parámetro adicional llamado append, con el cual le podremos indicar si queremos que, en el caso de que el archivo ya exista, queramos que se incorpore la información al final del archivo, o si queremos que lo trunque y vuelva al principio, y sustituya el archivo viejo por lo que vayamos a escribir en esta ocasión.
Por ejemplo, si yo quisiese escribir sobre un archivo llamado datos.bin, o datos.txt, pese a que esto es un sistema para escribir archivos binarios, podría indicarle que quiero que me escriba sobre el archivo datos.txt. Esto, lógicamente, tendrá más sentido si lo guardo en una variable como fos. De hecho, en este caso, en vez de usar var, voy a usar OutputStream, porque como esta es la clase abstracta base sobre la cual se levantan todos los OutputStreams, así me permite enseñaros todos los métodos que tenemos a nuestra disposición en cualquier OutputStream.
Un detalle que tal vez estéis apreciando es que FileOutputStream puede lanzar una excepción, y de hecho, el tipo de esta excepción va a ser FileNotFoundException. Esta excepción se va a producir cuando el sistema intente escribir sobre un archivo pero se encuentre con que no puede crear ese archivo. Por ejemplo, porque la carpeta no tenga permisos. En general, nos encontraremos que, en el caso de java.io, casi todos los métodos que tienen que ver con entrada salida podrán lanzar un IOException o alguna subclase de IOException, para señalizar que no se ha producido bien esa operación de entrada y salida. Por ejemplo, problemas de permisos, problemas para conectarse con el otro lado, o cualquier otra razón por la que pueda fallar la operación de lectura o escritura. Podríamos tratar este error utilizando algún tipo de try-catch, como voy a hacer ahora, o bien podríamos hacer un throws en la operación main, para lanzarlo hacia fuera, pero no lo estaríamos tratando correctamente.
FileOutputStream es un tipo de OutputStream, donde tenemos una serie de métodos, y los más importantes van a ser close(), flush() y write().
En el caso de close(), este lo utilizaremos cuando hayamos terminado de escribir sobre un archivo, de leer un archivo, o de utilizar en general un OutputStream. Las semánticas van a depender del tipo de OutputStream con el que estemos trabajando. Un FileOutputStream podría decirle al sistema operativo que libere los recursos de sistema para que otras aplicaciones no lo vean como archivo en uso, algo que nos puede dar algún tipo de problema. Sin embargo, como nos indica aquí, si el OutputStream no hace nada en particular, el comportamiento por defecto es no hacer nada
, porque se asume que no pasará nada.
Así que, en general, va a ser buena idea poner un close() al final de cualquier operación de entrada-salida en la que vayamos a utilizar un OutputStream. De hecho, existe una forma auxiliar de trabajar con el bloque try-catch cuando usamos un objeto que tiene un método close(), que es utilizar un bloque try-with, pero de eso os hablaré ahora en un rato. Como veis, este close() también puede lanzar errores, en este caso de tipo IOException, y lo que podemos hacer es tratarlos exactamente igual que cualquier otro tipo de error y avisar de que se ha producido un error, loguearlo o hacer cualquier otra cosa que veamos pertinente.
Lo más importante en el caso de un OutputStream va a ser llamar a la función write() cuando queramos escribir una determinada información hacia el mundo exterior a través de ese stream. Podemos hacerlo de dos formas: o bien le pasamos un único byte, que es lo que nos permite hacer la llamada que tiene como parámetro un int. Probablemente una mente perspicaz se haya dado cuenta de que el método write() acepta un int a pesar de que yo estoy diciendo byte. En realidad, los 24 bits más significativos de este número se van a ignorar. La única razón por la que acepta un int es porque así es más fácil de pasarle el valor y nos evitamos los problemas de si el número tiene signo o no tiene signo. Solo los 8 bits menos significativos del valor que le pasemos como parámetros van a ser mandados al stream. El resto van a ser descartados. Si quiero escribir el valor 44, puedo poner write(44), y eso en este caso literalmente guardará en el archivo datos.txt el byte número 44, que con una tabla ASCII podemos ver a qué caracter se refiere. O a lo mejor es un número binario puro que no tenga representación ASCII. Podemos ponerlo de forma normal, o bien podemos utilizar la sintaxis hexadecimal poniendo 0x, por ejemplo, para guardar el 0x31, que se corresponde con el caracter 1.
También existen otras formas alternativas, como puede ser proporcionar un array de bytes. Cuando vayamos a guardar múltiples valores, esta será la forma más eficiente, porque así le puede pasar el array completo al sistema operativo y la escritura será más fluida que si fuésemos byte a byte enviándole la información. Tenemos dos formas: o bien le proporcionamos el array de forma completa y lo vuelca todo, o bien le pasamos el array de bytes y le pasamos la posición inicial del array para que empiece a volcar datos a partir de esa posición, y le pasamos cuántos bytes queremos que nos envíe a ese buffer.
En el siguiente ejemplo, he hecho un bucle for que itera desde i = 0x30 hasta i = 0x39. Si consultamos una tabla ASCII, coincide justo con los valores del 0 al 9 si lo pasamos a carácter. Lo que estoy haciendo es llamar en sucesivas ocasiones a fos.write(), para proporcionarle ese valor. La consecuencia de esto es que cuando lo ejecute, se guardará dentro del archivo datos.txt los números del 0 al 9 que serán visibles desde el bloc de notas. Si lo abro, nos encontramos con un datos.txt que tiene 0123456789. Tenéis que tener en cuenta que esto es una casualidad. En este caso son valores que se corresponden con caracteres valores y corrientes. Pero si yo escribo valores como los que hay entre las posiciones 0x10 y 0x19 en hexadecimal, probablemente el resultado no tenga mucho sentido, porque son valores que no se corresponden con la tabla ASCII, y por lo tanto cualquier editor de textos lo va a mostrar con algún tipo de problema.
Un último método que nos vamos a encontrar en un OutputStream es el método flush(), el cual podemos usar en cualquier momento para señalizarle a Java que, para todos los bytes que hemos mandado hasta el momento, se asegure de mandarlos hacia el mundo exterior. Es decir, puede ocurrir que algunos OutputStreams de algún modo no envíen la información cuando llamamos a la función write(). Puede que lo mantengan localmente para poder ser más eficientes, y se espere a que haya suficiente información para mandarla toda de golpe. Esto es algo que en principio parece que tiene sentido: en vez de hacerte dar 20 viajes, tal vez es más eficiente que te lleves las 20 cosas de golpe. Java aplica este mismo principio y a veces, puede ocurrir que al llamar a write(), no se esté escribiendo nada, sino que esté esperando a que haya más información para poder escribirla toda de golpe. Cuando se llame a la función flush(), se le pedirá a Java que todo que tiene acumulado hasta el momento sea echado de golpe, de tal manera que cuando se llame a flush(), se apreciará que todo lo que no se haya enviado hasta el momento será enviado. En muchas ocasiones, será más seguro llamar a flush() justo antes del close(), o bien llamar a flush() en cualquier momento que nos queramos asegurar de que por lo menos la información ha sido enviada hasta ese punto.
Una falta de concepción que señala correctamente el Javadoc es que esto no siempre tiene todas las garantías de que va a enviar todo al mundo exterior. En particular, por ejemplo, puede ocurrir que, en el FileOutputStream, cuando se llame a la función flush() todo lo que haga es decirle al sistema operativo toma, aquí tienes todo lo que debes guardar
, pero realmente como tal, flush() no va a esperar a que esté guardado en el disco duro: dependerá del sistema operativo decir ya lo tengo
o no lo tengo
. Si escribes con un flush() sobre un archivo y justo en ese momento se va la luz, a lo mejor si se va la luz después del flush() no se ha guardado realmente en el disco duro, si el sistema operativo no había terminado de escribir las cosas.