BufferedOutputStream
Con BufferedOutputStream podemos introducir un buffer, que es una zona de memoria temporal que hace que sea más eficiente la escritura en un OutputStream porque acumula la información para enviarla de golpe en un único taco, en vez de ir poco a poco.
ObjectInputStream y ObjectOutputStream, incluso sus pequeños hermanos, DataInputStream y DataOutputStream, que funcionan de forma parecida, solo que con un protocolo difernete y sin posibilidad de serializar objetos, no son las únicas clases envolventes. Es decir, no son las únicas clases que te dejan envolver otro InputStream u otro OutputStream y modificar cómo se comportan. Existen BufferedInputStream y BufferedOutputStream, que tienen un funcionamiento bastante similar entre sí.
La idea de un Buffered va a consistir en que cuando se haga una operación de lectura o escritura, en esta clase va a existir un buffer, una región de memoria como un array, donde se va a estar guardando información para no hacer tan a menudo llamadas a read() y write() que puedan molestar al sistema operativo para que se comporte mejor.
Es decir, y vamos a verlo con un ejemplo visual para que se entienda mejor: cuando llamo a read(), si estoy todo el rato llamándole con valores pequeños y frecuentes, esto es bastante ineficiente porque estoy todo el rato pidiéndole al ordenador que me traiga datos del disco o de la conexión de red. El disco es un dispositivo que, aunque hoy día no es un problema porque suele ser SSD en casi todos los casos, sigue siendo un acceso más lento que si tuviésemos los datos en memoria, por lo que leer un montón de datos pequeños de forma consecutiva es más lento que si llamásemos a read una única vez y leyésemos un trozo de información más grande.
Pero claro, eso supone estar leyendo mucha información a la vez. Tal vez 1 MB. A lo mejor no queremos leer 1 MB, solo queremos leer 4 bytes. Por lo tanto, BufferedInputStream y BufferedOutputStream son una serie de clases que nos van a ayudar en este propósito. Un BufferedInputStream, que es en lo que me voy a centrar en este momento, va a llevar dentro otro InputStream, que será el que va a quedar envuelto. Cuando llamemos a read() a través de este Buffered; y de hecho voy a ponerle BUFFER
al de fuera para que quede claro que es el de fuera; lo que va a ocurrir en esta circunstancia es que va a llamar al de dentro, a read(), para que lea un montón de información. Todo lo que pueda acumular. Eso lo va a guardar en un buffer, como un array que tal vez tenga 4000 posiciones, para guardar hasta 4000 datos. Lo que hará el buffer del InputStream es proporcionar el byte que nosotros hemos querido leer. Acumula 4000, pero a nosotros nos da 1 o 2, según si llamamos a read(), o con un array pequeño. Nos da lo que le pedimos, pero por dentro va a guardar muchísima más información. La gracia es que la próxima vez que llamemos a read(), si todavía en este buffer queda información pendiente de ser leída, la operación read() irá mucho más rápido porque irá a este array. Es como una zona temporal para no tener que estar todo el rato pidiéndole al InputStream de dentro que haga una lectura al mundo exterior. El Buffered se ocupará de hacer una lectura grande, que permitirá al ordenador darnos todo el contenido del archivo si hace falta, y nosotros podemos ir consumiéndolo byte a byte si hace falta, de forma más eficiente puesto que esa información ya está leída y no tenemos que estar haciendo lecturas todo el tiempo.
En el caso de un BufferedOutputStream, el funcionamiento va a ser parecido. Le proporcionaremos un OutputStream, pero quedará envuelto por un buffer. Cuando llamemos a write(), en realidad no estaremos escribiendo en el archivo o donde sera, sino que lo haremos en un buffer, y a medida que llamemos a write() iremos llamando al array. Una vez el array esté lleno, o si llamamos a flush(), el BufferedOutputStream mandará de golpe todo ese pedazo de información más grande, al OutputStream real. Así que si vamos a escribir en un archivo, puede que escribir en un BufferedOutputStream sea más eficiente, o si vamos a escribir en la red también sea más eficiente, porque permitirá hacer una única escritura de golpe de toda la información que sea necesario escribir. Por ejemplo, en este caso he escrito una función llamada escribirNumeros(), que llama 256 veces a write(), cosa que puede ser un poco ineficiente porque, al fin y al cabo, estoy pidiéndole 256 veces a mi ordenador que escriba en un archivo. A lo mejor, puede ser más eficiente llamar a BufferedOutputStream antes, pasándole en el constructor ese OutputStream, que es el OutputStream real. Yo lo que haré será escribir sobre el BufferedOutputStream en su lugar. Cada vez que llame a write() será un poco más rápido. Aunque tampoco lo vamos a notar mucho, porque cuando he hecho la prueba fuera de cámara apenas ha tardado medio segundo. Lo importante es que será más rápido porque estos writes apuntarán a un buffer que hay aquí dentro, pero el disco duro estará tranquilo porque nadie le estará molestando.
Todo lo que tengo que hacer es recordar llamar a flush() cuando haya terminado de trabajar para asegurarme de que lo que quede en el buffer, ese remanente, sea enviado al archivo de verdad para que se guarde definitivamente. En este caso, cuando lo ejecuto no aprecio ninguna diferencia, pero si trabaje con un gran volumen de datos, sí lo notaría en el volumen de entrada y salida de mi disco duro.
Desplegar transcripción del episodio
[Música]
ObjectInputStream y ObjectOutputStream, incluso sus pequeños hermanos, DataInputStream y DataOutputStream, que funcionan de forma parecida, solo que con un protocolo difernete y sin posibilidad de serializar objetos, no son las únicas clases envolventes. Es decir, no son las únicas clases que te dejan envolver otro InputStream u otro OutputStream y modificar cómo se comportan. Existen BufferedInputStream y BufferedOutputStream, que tienen un funcionamiento bastante similar entre sí.
La idea de un Buffered va a consistir en que cuando se haga una operación de lectura o escritura, en esta clase va a existir un buffer, una región de memoria como un array, donde se va a estar guardando información para no hacer tan a menudo llamadas a read() y write() que puedan molestar al sistema operativo para que se comporte mejor.
Es decir, y vamos a verlo con un ejemplo visual para que se entienda mejor: cuando llamo a read(), si estoy todo el rato llamándole con valores pequeños y frecuentes, esto es bastante ineficiente porque estoy todo el rato pidiéndole al ordenador que me traiga datos del disco o de la conexión de red. El disco es un dispositivo que, aunque hoy día no es un problema porque suele ser SSD en casi todos los casos, sigue siendo un acceso más lento que si tuviésemos los datos en memoria, por lo que leer un montón de datos pequeños de forma consecutiva es más lento que si llamásemos a read una única vez y leyésemos un trozo de información más grande.
Pero claro, eso supone estar leyendo mucha información a la vez. Tal vez 1 MB. A lo mejor no queremos leer 1 MB, solo queremos leer 4 bytes. Por lo tanto, BufferedInputStream y BufferedOutputStream son una serie de clases que nos van a ayudar en este propósito. Un BufferedInputStream, que es en lo que me voy a centrar en este momento, va a llevar dentro otro InputStream, que será el que va a quedar envuelto. Cuando llamemos a read() a través de este Buffered; y de hecho voy a ponerle BUFFER
al de fuera para que quede claro que es el de fuera; lo que va a ocurrir en esta circunstancia es que va a llamar al de dentro, a read(), para que lea un montón de información. Todo lo que pueda acumular. Eso lo va a guardar en un buffer, como un array que tal vez tenga 4000 posiciones, para guardar hasta 4000 datos. Lo que hará el buffer del InputStream es proporcionar el byte que nosotros hemos querido leer. Acumula 4000, pero a nosotros nos da 1 o 2, según si llamamos a read(), o con un array pequeño. Nos da lo que le pedimos, pero por dentro va a guardar muchísima más información. La gracia es que la próxima vez que llamemos a read(), si todavía en este buffer queda información pendiente de ser leída, la operación read() irá mucho más rápido porque irá a este array. Es como una zona temporal para no tener que estar todo el rato pidiéndole al InputStream de dentro que haga una lectura al mundo exterior. El Buffered se ocupará de hacer una lectura grande, que permitirá al ordenador darnos todo el contenido del archivo si hace falta, y nosotros podemos ir consumiéndolo byte a byte si hace falta, de forma más eficiente puesto que esa información ya está leída y no tenemos que estar haciendo lecturas todo el tiempo.
En el caso de un BufferedOutputStream, el funcionamiento va a ser parecido. Le proporcionaremos un OutputStream, pero quedará envuelto por un buffer. Cuando llamemos a write(), en realidad no estaremos escribiendo en el archivo o donde sera, sino que lo haremos en un buffer, y a medida que llamemos a write() iremos llamando al array. Una vez el array esté lleno, o si llamamos a flush(), el BufferedOutputStream mandará de golpe todo ese pedazo de información más grande, al OutputStream real. Así que si vamos a escribir en un archivo, puede que escribir en un BufferedOutputStream sea más eficiente, o si vamos a escribir en la red también sea más eficiente, porque permitirá hacer una única escritura de golpe de toda la información que sea necesario escribir. Por ejemplo, en este caso he escrito una función llamada escribirNumeros(), que llama 256 veces a write(), cosa que puede ser un poco ineficiente porque, al fin y al cabo, estoy pidiéndole 256 veces a mi ordenador que escriba en un archivo. A lo mejor, puede ser más eficiente llamar a BufferedOutputStream antes, pasándole en el constructor ese OutputStream, que es el OutputStream real. Yo lo que haré será escribir sobre el BufferedOutputStream en su lugar. Cada vez que llame a write() será un poco más rápido. Aunque tampoco lo vamos a notar mucho, porque cuando he hecho la prueba fuera de cámara apenas ha tardado medio segundo. Lo importante es que será más rápido porque estos writes apuntarán a un buffer que hay aquí dentro, pero el disco duro estará tranquilo porque nadie le estará molestando.
Todo lo que tengo que hacer es recordar llamar a flush() cuando haya terminado de trabajar para asegurarme de que lo que quede en el buffer, ese remanente, sea enviado al archivo de verdad para que se guarde definitivamente. En este caso, cuando lo ejecuto no aprecio ninguna diferencia, pero si trabaje con un gran volumen de datos, sí lo notaría en el volumen de entrada y salida de mi disco duro.