La clase ByteArrayInputStream en el paquete java.io se puede utilizar para leer una matriz de entrada (de bytes).

Para crear un flujo de entrada de matriz de bytes, primero debemos importar el paquete java.io.ByteArrayInputStream. Después de importar el paquete, tenemos dos constructores disponibles para crear un flujo de entrada:

ByteArrayInputStream input = new ByteArrayInputStream(arr);
ByteArrayInputStream input = new ByteArrayInputStream(arr, 2, 2);

Hay 4 campos dentro de la clase:

// Byte array provided by the creator of the stream
protected byte buf[];

// Index of the next character to read from the input stream's buffer
protected int pos;

// Current marked position in the stream
protected int mark = 0;

// Index is one greater than the last valid character in the input stream's buffer
protected int count;

Y aquí están nuestros constructores:

public ByteArrayInputStream(byte buf[]) {
    this.buf = buf;
    this.pos = 0;
    this.count = buf.length;
}

public ByteArrayInputStream(byte buf[], int offset, int length) {
    this.buf = buf;
    this.pos = offset;
    this.count = Math.min(offset + length, buf.length);
    this.mark = offset;
}

Metodos de la clase ByteArrayInputStream

Método Acción
int read() Lee el siguiente byte de datos de este flujo de entrada.
int read(byte b[], int off, int len) Lee varios bytes del flujo de entrada y los almacena en el búfer b.
off es un desplazamiento en el arreglo b.
len es el número máximo de bytes a leer.
long skip(long n) Omite n bytes de entrada de este flujo de entrada. Devuelve el número de bytes omitidos (puede ser menor que n si alcanzamos el final del flujo de entrada).
int available() Devuelve el número de bytes restantes que se pueden leer (o saltar) de este flujo de entrada.
void reset() Restablece el búfer a la posición marcada. La posición marcada es 0 a menos que se marque otra posición o se especifique un desplazamiento diferente en el constructor.
boolean markSupported() Verifica si este InputStream admite marcado/reinicio. Devuelve true para ByteArrayInputStream.
void close() No hace nada.
void mark(int readAheadLimit) Establece el campo mark igual a la posición actual. Si se llama al método reset, entonces la lectura posterior comenzará desde esa posición. El parámetro readAheadLimit no se utiliza y no afecta el comportamiento del método.

Echemos un vistazo más de cerca a estos métodos y veamos cómo funcionan en la práctica.

read()

Cuando desees leer bytes de un ByteArrayInputStream de la misma manera que lo harías desde un InputStream ordinario, puedes utilizar el método read().

public static void main(String[] args) {
   byte[] array = {1, 2, 3, 4};

   try (ByteArrayInputStream input = new ByteArrayInputStream(array)) {
       for (int i = 0; i < array.length; i++) {
           int data = input.read();
           System.out.print(data + ", ");
       }
   } catch (IOException e) {
       e.printStackTrace();
   }
}

available()

Si deseas verificar si hay algo en tu búfer, puedes llamar al método available().

public static void main(String[] args) {
   byte[] array = {1, 2, 3, 4};

   try (ByteArrayInputStream input = new ByteArrayInputStream(array)) {
       System.out.println("Bytes available for reading: " + input.available());

       input.read();
       System.out.println("Bytes available for reading " + input.available());

       input.read();
       System.out.println("Bytes available for reading " + input.available());
   } catch (IOException e) {
       e.printStackTrace();
   }
}

Veremos que el número de bytes disponibles para la lectura cambia después de cada lectura del búfer.

Salida:

Bytes available for reading: 4
Bytes available for reading: 3
Bytes available for reading: 2

skip(long n)

Puedes utilizar el método skip() para saltar cierta cantidad de bytes y no leerlos.

public static void main(String[] args) {
   byte[] array = {1, 2, 3, 4};

   try (ByteArrayInputStream input = new ByteArrayInputStream(array)) {
       input.skip(2);

       while (input.available() != 0) {
           int data = input.read();
           System.out.print(data + ", ");
       }
   } catch (IOException e) {
       e.printStackTrace();
   }
}

Salida:

3, 4,

reset()

Este método restablece la posición del flujo almacenado en búfer a la última posición marcada. Es la posición 0 a menos que se establezca una marca diferente.

public static void main(String[] args) {
   byte[] buf = {65, 66, 67, 68, 69};
   try (ByteArrayInputStream input = new ByteArrayInputStream(buf)) {
       System.out.println("Read: " + input.read());
       System.out.println("Read: " + input.read());
       System.out.println("Read: " + input.read());
       System.out.println("Read: " + input.read());

       System.out.println("Calling reset() method");
       input.reset();
       System.out.println("Read: " + input.read());
       System.out.println("Read: " + input.read());
   } catch (IOException e) {
       e.printStackTrace();
   }
}

Veremos que al llamar al método reset() nos lleva al punto de partida de nuestro flujo.

Salida:

Read: 65
Read: 66
Read: 67
Read: 68
Calling reset() method
Read: 65
Read: 66

mark(int readAheadLimit)

El método mark() de la clase ByteArrayInputStream establece la marca interna en la posición actual del byte, es decir, inmediatamente después del byte leído anteriormente. Este método toma un parámetro que indica cuántos bytes se pueden leer después de la marca antes de que el flujo se vuelva inválido. De forma predeterminada, si la marca no se establece explícitamente, un ByteArrayInputStream marca la posición 0 o la posición del desplazamiento pasado a su constructor. Es importante tener en cuenta que la marca readAheadLimit es irrelevante para esta clase.

/* Note: For this class, {@code readAheadLimit}
*  has no meaning.
*
* @since   1.1
*/
public void mark(int readAheadLimit) {
   mark = pos;
}

Aquí hay un ejemplo de cómo establecer una marca en un ByteArrayInputStream usando sus métodos mark() y reset(). Agregaremos una llamada al método mark() en el ejemplo anterior:

public static void main(String[] args) {
   byte[] buf = {65, 66, 67, 68, 69};
   try (ByteArrayInputStream input = new ByteArrayInputStream(buf)) {
       System.out.println("Read: " + input.read());
       System.out.println("Read: " + input.read());
       System.out.println("Read: " + input.read());
       input.mark(5);

       System.out.println("Read: " + input.read());
       System.out.println("Read: " + input.read());

       System.out.println("Calling reset() method");
       input.reset();

       System.out.println("Read: " + input.read());
       System.out.println("Read: " + input.read());

   } catch (IOException e) {
       e.printStackTrace();
   }
}

Podemos ver que la posición actual del flujo ha cambiado.

Salida:

Read: 65
Read: 66
Read: 67
Read: 68
Read: 69
Calling reset() method
Read: 68
Read: 69

markSupported()

El método markSupported() te permite verificar si se puede establecer una marca. Para entender de dónde proviene el valor devuelto, vamos al código del método:

/**
* Tests if this {@code InputStream} supports mark/reset. The
* {@code markSupported} method of {@code ByteArrayInputStream}
* always returns {@code true}.
*
* @since   1.1
*/
public boolean markSupported() {
   return true;
}

El método siempre devuelve true. Probemos esto en la práctica.

public static void main(String[] args) {
   byte[] buf = {65, 66, 67, 68, 69};
   try (ByteArrayInputStream bais = new ByteArrayInputStream(buf)) {
       boolean isMarkSupported = bais.markSupported();

       System.out.println("isMarkSupported: " + isMarkSupported);
       System.out.println("Read: " + bais.read());
       System.out.println("Read: " + bais.read());

       bais.mark(1);
       System.out.println("Read: " + bais.read());
       isMarkSupported = bais.markSupported();
       System.out.println("isMarkSupported: " + isMarkSupported);

       bais.reset();
       isMarkSupported = bais.markSupported();
       System.out.println("isMarkSupported: " + isMarkSupported);
   } catch (IOException e) {
       e.printStackTrace();
   }
}

Después de ejecutar los métodos mark() y reset(), nuestro flujo siempre está listo y admite marcas:

Salida:

isMarkSupported: true
Read: 65
Read: 66
Read: 67
isMarkSupported: true
isMarkSupported: true

close()

Y para entender el método close, también echemos un vistazo dentro de él:

/**
* Closing a {@code ByteArrayInputStream} has no effect. The methods in
* this class can be called after the stream has been closed without
* generating an {@code IOException}.
*/
public void close() throws IOException {
}

La documentación para el método close nos dice que cerrar un ByteArrayInputStream no tiene efecto. Los métodos de la clase ByteArrayInputStream se pueden llamar después de que se cierre el flujo sin lanzar una IOException.

¿Qué podemos concluir?

Necesitamos un ByteArrayInputStream cuando queremos leer datos de una matriz de bytes. Por lo general, tiene sentido utilizar esta clase en combinación con otro código que sepa cómo trabajar con InputStreams en lugar de usarla por sí misma.