El ByteArrayOutputStream implementa un flujo de salida que escribe datos en una matriz de bytes. El búfer crece automáticamente a medida que se escriben datos en él.

La clase ByteArrayOutputStream crea un búfer en memoria, y todos los datos enviados al flujo se almacenan en el búfer.

Constructores de ByteArrayOutputStream

La clase ByteArrayOutputStream tiene los siguientes constructores:

Constructor
ByteArrayOutputStream() Este constructor crea un búfer en memoria de 32 bytes de longitud.
ByteArrayOutputStream(int a) Este constructor crea un búfer en memoria con un tamaño específico.

Y así es como se ve la clase por dentro:


// The buffer itself, where the data is stored.
protected byte buf[];

// Current number of bytes written to the buffer.
protected int count;

public ByteArrayOutputStream() {
    this(32);
}

public ByteArrayOutputStream(int size) {
    if (size < 0) {
        throw new IllegalArgumentException("Negative initial size: "
                                           + size);
    }
    buf = new byte[size];
}
    

Métodos de la clase ByteArrayOutputStream

Hablemos de los métodos que podemos usar en nuestra clase.

Intentemos poner algo en nuestro flujo. Para hacer esto, utilizaremos el método write() — puede aceptar un byte o un conjunto de bytes para escribir.

Método
void write(int b) Escribe un byte.
void write(byte b[], int off, int len) Escribe un conjunto de bytes de tamaño específico.
void writeBytes(byte b[]) Escribe un array de bytes.
void writeTo(OutputStream out) Escribe todos los datos del flujo de salida actual en el flujo de salida pasado como argumento.

Implementación del método:


public static void main(String[] args) throws IOException {
   ByteArrayOutputStream outputByte = new ByteArrayOutputStream();
   // Write one byte
   while(outputByte.size()!= 7) {
      outputByte.write("codegym".getBytes());
   }

   // Write array of bytes
   String value = "\nWelcome to Java\n";
   byte[] arrBytes = value.getBytes();
   outputByte.write(arrBytes);

   // Write part of an array
   String codeGym = "CodeGym";
   byte[] b = codeGym.getBytes();
   outputByte.write(b, 4, 3);

   // Write to a file
   FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
   outputByte.write(80);
   outputByte.writeTo(fileOutputStream);
}
    

El resultado es un nuevo archivo output.txt que se ve así:

El método toByteArray() devuelve el contenido actual de la secuencia de salida como una matriz de bytes. Y puede usar el método toString() para obtener la matriz de bytes buf como texto:


public static void main(String[] args) throws IOException {
    ByteArrayOutputStream outputByte = new ByteArrayOutputStream();

    String value = "CodeGym";
    outputByte.write(value.getBytes());

    byte[] result = outputByte.toByteArray();
    System.out.println("Result: ");

    for(int i = 0 ; i < result.length; i++) {
        // Display the characters
        System.out.print((char)result[i]);
    }
}
    

Nuestro búfer contiene la matriz de bytes que le pasamos.

El método reset() restablece el número de bytes válidos en el flujo de salida de la matriz de bytes a cero (por lo que todo lo acumulado en la salida se restablece).


public static void main(String[] args) throws IOException {
   ByteArrayOutputStream outputByte = new ByteArrayOutputStream(120);

   String value = "CodeGym";
   outputByte.write(value.getBytes());
   byte[] result = outputByte.toByteArray();
   System.out.println("Output before reset: ");

   for (byte b : result) {
      // Display the characters
      System.out.print((char) b);
   }

   outputByte.reset();

   byte[] resultAfterReset = outputByte.toByteArray();
   System.out.println("\nOutput after reset: ");

   for (byte b : resultAfterReset) {
      // Display the characters
      System.out.print((char) b);
   }
}
    

Cuando mostramos nuestro búfer después de llamar al método reset(), no obtenemos nada.

Características específicas del método close()

Este método merece atención especial. Para entender lo que hace, echemos un vistazo al código:


/**
 * Closing a {@code ByteArrayOutputStream} 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 {
}
    

Observa que el método close() de la clase ByteArrayOutputStream en realidad no hace nada.

¿Por qué? Un ByteArrayOutputStream es un flujo basado en memoria (es decir, es administrado y poblado por el usuario en el código), por lo que llamar al método close() no tiene efecto.

Práctica

Ahora intentemos implementar un sistema de archivos usando nuestro ByteArrayOutputStream y ByteArrayInputStream.

Escribamos una clase FileSystem usando el patrón de diseño singleton y use un HashMap<String, byte[]> estático, donde:

  • String es la ruta a un archivo
  • byte[] es la información del archivo guardado

import java.io.*;
import java.util.HashMap;
import java.util.Map;

class FileSystem {
   private static final FileSystem fileSystem = new FileSystem();
   private static final Map<String, byte[]> files = new HashMap<>();

   private FileSystem() {
   }

   public static FileSystem getFileSystem() {
       return fileSystem;
   }

   public void create(String path) {
       validateNotExists(path);
       files.put(path, new byte[0]);
   }

   public void delete(String path) {
       validateExists(path);
       files.remove(path);
   }

   public boolean isExists(String path) {
       return files.containsKey(path);
   }

   public InputStream newInputStream(String path) {
       validateExists(path);
       return new ByteArrayInputStream(files.get(path));
   }

   public OutputStream newOutputStream(String path) {
       validateExists(path);
       return new ByteArrayOutputStream() {
           @Override
           public void flush() throws IOException {
               final byte[] bytes = toByteArray();
               files.put(path, bytes);
               super.flush();
           }

           @Override
           public void close() throws IOException {
               final byte[] bytes = toByteArray();
               files.put(path, bytes);
               super.close();
           }
       };
   }

   private void validateExists(String path) {
       if (!files.containsKey(path)) {
           throw new RuntimeException("File not found");
       }
   }

   private void validateNotExists(String path) {
       if (files.containsKey(path)) {
           throw new RuntimeException("File exists");
       }
   }
}
    

En esta clase, creamos los siguientes métodos públicos:

  • métodos CRUD estándar (crear, leer, actualizar, eliminar),
  • un método para verificar si un archivo existe,
  • un método para obtener una instancia del sistema de archivos.

Para leer desde un archivo, devolvemos un InputStream. Debajo del capó se encuentra la implementación ByteArrayInputStream. El búfer es una matriz de bytes almacenada en el mapa files.

Otro método interesante es newOutputStream. Cuando se llama a este método, devolvemos un nuevo objeto ByteArrayOutputStream que anula dos métodos: flush y close. Llamar a cualquiera de estos métodos debe provocar que se realice la escritura.

Y eso es exactamente lo que hacemos: obtenemos la matriz de bytes que el usuario ha escrito y almacenamos una copia como el valor en el mapa files con una clave apropiada.

Usamos el siguiente código para probar nuestro sistema de archivos (FS):


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import static java.nio.charset.StandardCharsets.UTF_8;

public class MyFileSystemTest {
   public static void main(String[] args) throws IOException {
       FileSystem fileSystem = FileSystem.getFileSystem();
       final String path = "/user/bin/data.txt";

       // Create a file
       fileSystem.create(path);
       System.out.println("File created successfully");

       // Make sure it's empty
       try (InputStream inputStream = fileSystem.newInputStream(path)) {
           System.out.print("File contents:\t");
           System.out.println(read(inputStream));
       }

       // Write data to it
       try (final OutputStream outputStream = fileSystem.newOutputStream(path)) {
           outputStream.write("CodeGym".getBytes(UTF_8));
           System.out.println("Data written to file");
       }

       // Read data
       try (InputStream inputStream = fileSystem.newInputStream(path)) {
           System.out.print("File contents:\t");
           System.out.println(read(inputStream));
       }

       // Delete the file
       fileSystem.delete(path);

       // Verify that the file does not exist in the FS
       System.out.print("File exists:\t");
       System.out.println(fileSystem.isExists(path));

   }

   private static String read(InputStream inputStream) throws IOException {
       return new String(inputStream.readAllBytes(), UTF_8);
   }
}
    

En esta prueba, verificamos las siguientes acciones:

  1. Crear un nuevo archivo.
  2. Comprobar que el archivo creado está vacío.
  3. Escribir algunos datos en el archivo.
  4. Leer de nuevo los datos y verificar que coinciden con lo que hemos escrito.
  5. Eliminar el archivo.
  6. Verificar que el archivo ha sido eliminado.

La ejecución de este código nos da la siguiente salida:

Archivo creado con éxito
Contenido del archivo:
Datos escritos en el archivo
Contenido del archivo: CodeGym
El archivo existe: false

¿Por qué fue necesario este ejemplo?

Sencillamente, los datos siempre son un conjunto de bytes. Si necesita leer/escribir muchos datos desde/hacia el disco, su código se ejecutará lentamente debido a problemas de E/S. En este caso, tiene sentido mantener un sistema de archivos virtual en la memoria RAM, trabajando con él de la misma manera que lo haría con un disco tradicional. ¿Y qué podría ser más sencillo que InputStream y OutputStream?

Por supuesto, este es un ejemplo para fines educativos, no es un código listo para producción. NO tiene en cuenta (la siguiente lista no es exhaustiva):

  • multithreading
  • límites de tamaño de archivo (la cantidad de RAM disponible para una JVM en ejecución)
  • verificación de la estructura de la ruta
  • verificación de los argumentos del método

Información interna interesante:
El servidor de verificación de tareas de CodeGym utiliza un enfoque algo similar. Creamos un sistema de archivos virtual, determinamos qué pruebas deben ejecutarse para la verificación de tareas, ejecutamos las pruebas y leemos los resultados.

Conclusión y la gran pregunta

La gran pregunta después de leer esta lección es "¿Por qué no puedo simplemente usar byte[], ya que es más conveniente y no impone restricciones?"

La ventaja de ByteArrayInputStream es que indica fuertemente que vas a utilizar bytes de solo lectura (porque el stream no proporciona una interfaz para cambiar su contenido). Dicho esto, es importante tener en cuenta que un programador todavía puede acceder a los bytes directamente.

Pero si a veces tienes un byte[], a veces tienes un archivo, a veces tienes una conexión de red, y así sucesivamente, necesitarás algún tipo de abstracción para "un flujo de bytes, y no me importa de dónde vengan". Y eso es lo que es InputStream. Cuando la fuente resulta ser una matriz de bytes, ByteArrayInputStream es un buen InputStream para usar.

Esto es útil en muchas situaciones, pero aquí hay dos ejemplos específicos:

  1. Estás escribiendo una biblioteca que recibe bytes y los procesa de alguna manera (por ejemplo, supongamos que es una biblioteca de utilidades de procesamiento de imágenes). Los usuarios de tu biblioteca pueden proporcionar bytes desde un archivo, desde un byte[] en memoria, o desde alguna otra fuente. Entonces proporcionas una interfaz que acepta un InputStream, lo que significa que si tienen un byte[], necesitan envolverlo en un ByteArrayInputStream.

  2. Estás escribiendo código que lee una conexión de red. Pero para realizar pruebas unitarias en este código, no quieres abrir una conexión, solo quieres alimentar unos pocos bytes al código. Entonces el código toma un InputStream y tu prueba pasa un ByteArrayInputStream.