BufferedInputStream y rebobinado de streams
La idea de BufferedInputStream es que con una única llamada a read() podamos abarcar tanto como sea posible, para no tener que estar continuamente accediendo al stream que va por debajo. Lo importante es que con esto podemos usar métodos como mark() y reset() para poder rebobinar un stream
BufferedInputStream es una clase que va a funcionar de forma parecida a BufferedOutputStream. Es decir, nos va a permitir envolver otro InputStream para poder acumular información, y no tener que estar haciendo llamadas tan rápido al método read() que acaben en el sistema de archivos, o donde sea que esté apuntando nuestro InputStream.
Vamos a hacer un ejemplo porque aquí lo que me interesa más son una serie de métodos que tienen más sentido utilizar en clases como BufferedInputStream. Voy a fabricar en un bucle try, y de hecho voy a hacerlo en una función aparte para que no tenga que separarlo y que sea más fácil de estructurar. Y voy a hacer una apertura de un archivo. Voy a fabricar también un BufferedInputStream... ahí está. Va a apuntar a mi FileInputStream. Es decir, tengo mi archivo para leerlo, pero lo vamos a envolver en un BufferedInputStream. Ahora, voy a ponerle un IOException para asegurarme de que si falla la lectura, por lo menos se responda. Lo que voy a hacer es llamar, en principio, a read(), del BufferedInputStream.
Esto lo que va a provocar es lo que os contaba antes cuando os presentaba estas dos clases. Con este read() se va a leer un único byte, pero por debajo el BufferedInputStream leerá tantos como pueda, para que la próxima vez que llame a read() no tenga que irme otra vez al disco; como en este caso, sino que aquí dentro habrá un buffer, secreto, donde se va a leer cómodamente la información. De hecho vamos a hacer una prueba. Voy a sacar a través de una variable este valor, y lo que voy a hacer es envolverlo en un bucle do{}, para saber cuándo hay que parar.
Si habéis prestado atención antes, sabréis que read() devuelve el byte que se haya leído, pero si devuelve -1, significa que hemos llegado al final. Podemos repetirlo hasta que octeto valga -1, porque eso significa que se ha terminado de leer el contenido de este buffer. Si octeto es diferente de -1, sólo en ese caso tendrá sentido imprimir el valor que ha sido leído. Le voy a poner un espacio para ir separando las cositas. En principio esta sería la forma más simple de usar un BufferedInputStream. Dirás que tampoco tiene mucha diferencia con respecto a un FileInputStream normal. Efectivamente: si lo ejecuto, vemos acá que se escriben los 255 números.
¿Sabéis qué? Voy a hacer que si octeto es múltiplo de 20 se imprima un salto de línea, para que no salga una línea tan larga. Ahora sí. Parece un cartón de bingo, pero tiene más sentido ahora.
Fijaos lo que voy a hacer: voy a ponerle un breakpoint. Con esto, el depurador detendrá la ejecución del código cuando llegue a esta línea, después de haber hecho la lectura. Para así poder demostraros que una vez que llamo a read, nuestro BufferedInputStream, dentro, si miramos sus tripas, tiene un array de bytes, que es donde se van a estar guardando esos bytes que se han leído masivamente. Si os fijáis en octeto, hemos leído un único valor, el número 0, aunque voy a dar una vuelta más para que tenga un valor más simpático, como el 1. Dentro de este buffer, nos encontramos todo. Lo que ha hecho el BufferedInputStream es decir bueno, ya que voy, voy a leerlo todo, y me traigo todo lo que pueda leer
. Y vaya si se ha leído todo: se ha leído los 256 valores que había dentro del archivo. De hecho, aquí hay otra variable interna que se llama count, que vale 256, no se ve muy bien pero ahí está. Nos da que pensar que sabe cuántos valores se han leído. También está pos, que ahora vale 2, pero que si le vuelvo a dar más vueltas valdrá 3, 4... Este va a ser el puntero que sabe qué valor hay que devolver la próxima vez que llame a read. Así es como funcionaría en esencia, no tiene mucho más.
Excepto... si usamos las funciones propias que tiene más sentido utilizar en el caso de BufferedInputStream, entre otras clases que soportan, por ejemplo, las marcas. La idea de una marca es establecer una especie de marcapáginas, o bookmark. Con esta marca podemos recuperar una posición anterior. Esto contrasta un poco con lo que os dije al principio. Os dije que un InputStream es como una cinta transportadora, de donde una vez se consume un byte ya no puede volver a ser recuperado. En realidad, mark(), y también reset(), sirven precisamente para hacer un poco de trampa y de algún modo acumular una serie de bytes para poder volver atrás en el tiempo y así volver atrás en el tiempo.
Deja estos bytes en esta cajita, y si alguien llama a reset(), tú simplemente lo que vas a hacer es colocarlos de nuevo en la cinta transportadora para poder leerlos una segunda vez
. Esto es precisamente lo que hace, de hecho, la función mark(). Si yo llamo a bis.mark(), y le paso como parámetro el número que dice la variable readLimit, que indica cuánto quiero que se acumule, lo que puedo hacer aquí es colocar una marca que dice vale, pues los siguientes 50 bytes que leas, quiero que los vayas guardando por ahí para poder volver atrás y recuperar donde yo estaba cuando llamé a mark()
. Es decir, a partir de este momento, yo puedo leer hasta 50 bytes, llamando a read hasta 50 veces incluso, y podré volver atrás en el tiempo porque he creado aquí una marca. Si utilizo la función reset(), lo que hará será volver a la posición en la que estaba mi InputStream la última vez que llamé a mark().
Esto es algo que así a priori no tiene sentido pero lo vamos a ver con un ejemplo claro. Supongamos que así de buenas a primeras nada más abrir mi InputStream llamo a mark() y decirle que me establezca un límite de 100 bytes a leer. Se va a crear un marcador al principio del InputStream para decir que recuerde los últimos 100 bytes que se han leído todo el rato. No los descarta sin más: los va recordando, porque tarde o temprano tendrá que volver atrás en el tiempo. En principio puedo hacer una lectura normal y corriente y seguiría funcionando como siempre. Sin embargo, si en algún momento le digo, que por ejemplo si octeto es igual a 50, es decir, si ya he leído 50, quiero que me hagas una llamada a bis.reset(). Esto lo que hará es volver en la cinta transportadora a donde estaba la última vez que llamé a mark(). Por lo tanto, lo que va a ocurrir es que al llamar a reset() voy a volver al principio en este caso. Esto va a provocar un bucle infinito, donde iremos viendo que se imprimen los números del 0 al 50 infinitamente. ¿Por qué está ocurriendo esto? Porque cada vez que alcanzo la posición 50, le estoy mandando con un reset() a la última marca que establecí, así que es como si de repente la cinta volviese hacia atrás y volviesemos a empezar desde el principio. Pero claro, la próxima vez que llegue a 50 la voy a empujar otra vez hacia atrás. Lógicamente, a lo mejor tiene sentido que lo que haga es contar el número de veces que ha ocurrido esto, poner retornos = 0
, y decir que ++retornos es menor que 4, para que por lo menos pueda ponerle un límite. De este modo, ahora ya el programa termina y lo único que se va a escribir varias veces porque va a volver atrás un número fijo de veces hasta que ya se acabe y pueda continuar donde estaba.
Una cosita más, por cierto, es que a pesar de que el contrato establece que puede ocurrir que en algunas circunstancias se vuelva al principio, en la práctica, si no llamas en ningún momento a mark(), lo que va a ocurrir, al menos con BufferedInputStream, es que va a lanzar un error. ¿Por qué? Porque tienes que llamar alguna vez a mark() para poder hacer un reset(). Esto lo digo porque hay tipos de clases donde si no llamas a mark() pero sí llamas a reset() simplemente vuelves al principio. Tened en cuenta que no todas van a soportar esta operación.
Con reset y con mark podéis volver atrás en la cinta, como digo. Este número, simplemente está ahí porque si nosotros pusiésemos un valor más grande, imagina que le digo 150, puede fallar o puede tener un comportamiento no definido -dependerá de cada InputStream hacer lo que vea conveniente. En este caso, si yo le digo que me recuerde los próximos 100 bytes pero trato de volver más allá de esa posición, a lo mejor ya ha perdido la memoria de lo que tenía al principio, porque hemos ido más allá de su capacidad para retener. Eso puede ser un problema. En este caso no, lógicamente, porque el array es bastante grande, pero puede haber InputStreams donde una vez se hayan pasado los 100 bytes, se empiece a descartar la información. Tened en cuenta que este valor puede ser utilizado, por ejemplo, como otro buffer. Si os pasáis y llenáis el buffer y se desborda, tal vez una llamada a reset puede no comportarse como cabe esperar. En este caso sí, porque de 150 pasa a 0, pero no tiene por qué.
De todos modos, mark y reset son métodos que no vamos a usar muchísimo, pero igualmente me he decidido a incluirlos en esta parte, porque si en algún momento alguien los necesita, tal vez agradezca que haya hecho esta explicación.
Desplegar transcripción del episodio
[Música]
BufferedInputStream es una clase que va a funcionar de forma parecida a BufferedOutputStream. Es decir, nos va a permitir envolver otro InputStream para poder acumular información, y no tener que estar haciendo llamadas tan rápido al método read() que acaben en el sistema de archivos, o donde sea que esté apuntando nuestro InputStream.
Vamos a hacer un ejemplo porque aquí lo que me interesa más son una serie de métodos que tienen más sentido utilizar en clases como BufferedInputStream. Voy a fabricar en un bucle try, y de hecho voy a hacerlo en una función aparte para que no tenga que separarlo y que sea más fácil de estructurar. Y voy a hacer una apertura de un archivo. Voy a fabricar también un BufferedInputStream... ahí está. Va a apuntar a mi FileInputStream. Es decir, tengo mi archivo para leerlo, pero lo vamos a envolver en un BufferedInputStream. Ahora, voy a ponerle un IOException para asegurarme de que si falla la lectura, por lo menos se responda. Lo que voy a hacer es llamar, en principio, a read(), del BufferedInputStream.
Esto lo que va a provocar es lo que os contaba antes cuando os presentaba estas dos clases. Con este read() se va a leer un único byte, pero por debajo el BufferedInputStream leerá tantos como pueda, para que la próxima vez que llame a read() no tenga que irme otra vez al disco; como en este caso, sino que aquí dentro habrá un buffer, secreto, donde se va a leer cómodamente la información. De hecho vamos a hacer una prueba. Voy a sacar a través de una variable este valor, y lo que voy a hacer es envolverlo en un bucle do{}, para saber cuándo hay que parar.
Si habéis prestado atención antes, sabréis que read() devuelve el byte que se haya leído, pero si devuelve -1, significa que hemos llegado al final. Podemos repetirlo hasta que octeto valga -1, porque eso significa que se ha terminado de leer el contenido de este buffer. Si octeto es diferente de -1, sólo en ese caso tendrá sentido imprimir el valor que ha sido leído. Le voy a poner un espacio para ir separando las cositas. En principio esta sería la forma más simple de usar un BufferedInputStream. Dirás que tampoco tiene mucha diferencia con respecto a un FileInputStream normal. Efectivamente: si lo ejecuto, vemos acá que se escriben los 255 números.
¿Sabéis qué? Voy a hacer que si octeto es múltiplo de 20 se imprima un salto de línea, para que no salga una línea tan larga. Ahora sí. Parece un cartón de bingo, pero tiene más sentido ahora.
Fijaos lo que voy a hacer: voy a ponerle un breakpoint. Con esto, el depurador detendrá la ejecución del código cuando llegue a esta línea, después de haber hecho la lectura. Para así poder demostraros que una vez que llamo a read, nuestro BufferedInputStream, dentro, si miramos sus tripas, tiene un array de bytes, que es donde se van a estar guardando esos bytes que se han leído masivamente. Si os fijáis en octeto, hemos leído un único valor, el número 0, aunque voy a dar una vuelta más para que tenga un valor más simpático, como el 1. Dentro de este buffer, nos encontramos todo. Lo que ha hecho el BufferedInputStream es decir bueno, ya que voy, voy a leerlo todo, y me traigo todo lo que pueda leer
. Y vaya si se ha leído todo: se ha leído los 256 valores que había dentro del archivo. De hecho, aquí hay otra variable interna que se llama count, que vale 256, no se ve muy bien pero ahí está. Nos da que pensar que sabe cuántos valores se han leído. También está pos, que ahora vale 2, pero que si le vuelvo a dar más vueltas valdrá 3, 4... Este va a ser el puntero que sabe qué valor hay que devolver la próxima vez que llame a read. Así es como funcionaría en esencia, no tiene mucho más.
Excepto... si usamos las funciones propias que tiene más sentido utilizar en el caso de BufferedInputStream, entre otras clases que soportan, por ejemplo, las marcas. La idea de una marca es establecer una especie de marcapáginas, o bookmark. Con esta marca podemos recuperar una posición anterior. Esto contrasta un poco con lo que os dije al principio. Os dije que un InputStream es como una cinta transportadora, de donde una vez se consume un byte ya no puede volver a ser recuperado. En realidad, mark(), y también reset(), sirven precisamente para hacer un poco de trampa y de algún modo acumular una serie de bytes para poder volver atrás en el tiempo y así volver atrás en el tiempo.
Deja estos bytes en esta cajita, y si alguien llama a reset(), tú simplemente lo que vas a hacer es colocarlos de nuevo en la cinta transportadora para poder leerlos una segunda vez
. Esto es precisamente lo que hace, de hecho, la función mark(). Si yo llamo a bis.mark(), y le paso como parámetro el número que dice la variable readLimit, que indica cuánto quiero que se acumule, lo que puedo hacer aquí es colocar una marca que dice vale, pues los siguientes 50 bytes que leas, quiero que los vayas guardando por ahí para poder volver atrás y recuperar donde yo estaba cuando llamé a mark()
. Es decir, a partir de este momento, yo puedo leer hasta 50 bytes, llamando a read hasta 50 veces incluso, y podré volver atrás en el tiempo porque he creado aquí una marca. Si utilizo la función reset(), lo que hará será volver a la posición en la que estaba mi InputStream la última vez que llamé a mark().
Esto es algo que así a priori no tiene sentido pero lo vamos a ver con un ejemplo claro. Supongamos que así de buenas a primeras nada más abrir mi InputStream llamo a mark() y decirle que me establezca un límite de 100 bytes a leer. Se va a crear un marcador al principio del InputStream para decir que recuerde los últimos 100 bytes que se han leído todo el rato. No los descarta sin más: los va recordando, porque tarde o temprano tendrá que volver atrás en el tiempo. En principio puedo hacer una lectura normal y corriente y seguiría funcionando como siempre. Sin embargo, si en algún momento le digo, que por ejemplo si octeto es igual a 50, es decir, si ya he leído 50, quiero que me hagas una llamada a bis.reset(). Esto lo que hará es volver en la cinta transportadora a donde estaba la última vez que llamé a mark(). Por lo tanto, lo que va a ocurrir es que al llamar a reset() voy a volver al principio en este caso. Esto va a provocar un bucle infinito, donde iremos viendo que se imprimen los números del 0 al 50 infinitamente. ¿Por qué está ocurriendo esto? Porque cada vez que alcanzo la posición 50, le estoy mandando con un reset() a la última marca que establecí, así que es como si de repente la cinta volviese hacia atrás y volviesemos a empezar desde el principio. Pero claro, la próxima vez que llegue a 50 la voy a empujar otra vez hacia atrás. Lógicamente, a lo mejor tiene sentido que lo que haga es contar el número de veces que ha ocurrido esto, poner retornos = 0
, y decir que ++retornos es menor que 4, para que por lo menos pueda ponerle un límite. De este modo, ahora ya el programa termina y lo único que se va a escribir varias veces porque va a volver atrás un número fijo de veces hasta que ya se acabe y pueda continuar donde estaba.
Una cosita más, por cierto, es que a pesar de que el contrato establece que puede ocurrir que en algunas circunstancias se vuelva al principio, en la práctica, si no llamas en ningún momento a mark(), lo que va a ocurrir, al menos con BufferedInputStream, es que va a lanzar un error. ¿Por qué? Porque tienes que llamar alguna vez a mark() para poder hacer un reset(). Esto lo digo porque hay tipos de clases donde si no llamas a mark() pero sí llamas a reset() simplemente vuelves al principio. Tened en cuenta que no todas van a soportar esta operación.
Con reset y con mark podéis volver atrás en la cinta, como digo. Este número, simplemente está ahí porque si nosotros pusiésemos un valor más grande, imagina que le digo 150, puede fallar o puede tener un comportamiento no definido -dependerá de cada InputStream hacer lo que vea conveniente. En este caso, si yo le digo que me recuerde los próximos 100 bytes pero trato de volver más allá de esa posición, a lo mejor ya ha perdido la memoria de lo que tenía al principio, porque hemos ido más allá de su capacidad para retener. Eso puede ser un problema. En este caso no, lógicamente, porque el array es bastante grande, pero puede haber InputStreams donde una vez se hayan pasado los 100 bytes, se empiece a descartar la información. Tened en cuenta que este valor puede ser utilizado, por ejemplo, como otro buffer. Si os pasáis y llenáis el buffer y se desborda, tal vez una llamada a reset puede no comportarse como cabe esperar. En este caso sí, porque de 150 pasa a 0, pero no tiene por qué.
De todos modos, mark y reset son métodos que no vamos a usar muchísimo, pero igualmente me he decidido a incluirlos en esta parte, porque si en algún momento alguien los necesita, tal vez agradezca que haya hecho esta explicación.