A classe ByteArrayOutputStream implementa um fluxo de saída que grava dados em uma matriz de bytes. O buffer aumenta automaticamente à medida que os dados são gravados nele.

A classe ByteArrayOutputStream cria um buffer na memória e todos os dados enviados ao fluxo são armazenados no buffer.

Construtores ByteArrayOutputStream

A classe ByteArrayOutputStream tem os seguintes construtores:

Construtor
ByteArrayOutputStream() Esse construtor cria um buffer na memória com 32 bytes de comprimento.
ByteArrayOutputStream(int a) Esse construtor cria um buffer na memória com um tamanho específico.

E é assim que a classe se parece 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 da classe ByteArrayOutputStream

Vamos falar sobre os métodos que podemos usar em nossa classe.

Vamos tentar colocar algo em nosso stream. Para fazer isso, usaremos o método write() — ele pode aceitar um byte ou um conjunto de bytes para escrita.

Método
void escrever(int b) Escreve um byte.
void write(byte b[], int off, int len) Grava uma matriz de bytes de um tamanho específico.
void escreverBytes(byte b[]) Grava uma matriz de bytes.
void writeTo(OutputStream out) Grava todos os dados do fluxo de saída atual no fluxo de saída passado.

Implementação do 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);
}
    

O resultado é um novo arquivo output.txt com a seguinte aparência:

O método toByteArray() retorna o conteúdo atual desse fluxo de saída como uma matriz de bytes. E você pode usar o método toString() para obter a 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]);
    }
}
    

Nosso buffer contém o array de bytes que passamos para ele.

O método reset() redefine o número de bytes válidos no fluxo de saída da matriz de bytes para zero (portanto, tudo acumulado na saída é redefinido).


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);
   }
}
    

Quando exibimos nosso buffer depois de chamar o método reset() , não obtemos nada.

Recursos específicos do método close()

Este método merece atenção especial. Para entender o que ele faz, vamos dar uma olhada no interior:


/**
 * 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 {
}
    

Observe que o método close() da classe ByteArrayOutputStream não faz nada.

Por que é que? Um ByteArrayOutputStream é um fluxo baseado em memória (ou seja, é gerenciado e preenchido pelo usuário no código), portanto, chamar close() não tem efeito.

Prática

Agora vamos tentar implementar um sistema de arquivos usando nosso ByteArrayOutputStream e ByteArrayInputStream .

Vamos escrever uma classe FileSystem usando o padrão de design singleton e usar um HashMap<String, byte[]> estático , onde:

  • String é o caminho para um arquivo
  • byte[] são os dados no arquivo salvo

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");
       }
   }
}
    

Nesta classe, criamos os seguintes métodos públicos:

  • métodos CRUD padrão (criar, ler, atualizar, excluir),
  • um método para verificar se existe um arquivo,
  • um método para obter uma instância do sistema de arquivos.

Para ler um arquivo, retornamos um InputStream . Sob o capô está a implementação ByteArrayInputStream . O buffer é uma matriz de bytes armazenada no mapa de arquivos .

Outro método interessante é o newOutputStream . Quando esse método é chamado, retornamos um novo objeto ByteArrayOutputStream que substitui dois métodos: flush e close . Chamar qualquer um desses métodos deve fazer com que a gravação ocorra.

E é exatamente isso que fazemos: obtemos o array de bytes no qual o usuário escreveu e armazenamos uma cópia como ovalorno mapa de arquivos com uma chave apropriada.

Usamos o seguinte código para testar nosso sistema de arquivos (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);
   }
}
    

Durante o teste, verificamos as seguintes ações:

  1. Criamos um novo arquivo.
  2. Verificamos se o arquivo criado está vazio.
  3. Escrevemos alguns dados no arquivo.
  4. Lemos os dados e verificamos se correspondem ao que escrevemos.
  5. Excluímos o arquivo.
  6. Verificamos se o arquivo foi excluído.

Executar este código nos dá esta saída:

Arquivo criado com sucesso
Conteúdo do arquivo:
Dados gravados no arquivo
Conteúdo do arquivo: CodeGym O
arquivo existe: false

Por que esse exemplo foi necessário?

Simplificando, os dados são sempre um conjunto de bytes. Se você precisar ler/gravar muitos dados de/para o disco, seu código será executado lentamente devido a problemas de E/S. Nesse caso, faz sentido manter um sistema de arquivos virtual na RAM, trabalhando com ele da mesma forma que faria com um disco tradicional. E o que poderia ser mais simples do que InputStream e OutputStream ?

Claro, este é um exemplo para instrução, não um código pronto para produção. NÃO é responsável por (a lista a seguir não é abrangente):

  • multithreading
  • limites de tamanho de arquivo (a quantidade de RAM disponível para uma JVM em execução)
  • verificação da estrutura do caminho
  • verificação dos argumentos do método

Informações privilegiadas interessantes:
O servidor de verificação de tarefas CodeGym usa uma abordagem um tanto semelhante. Criamos um FS virtual, determinamos quais testes precisam ser executados para verificação de tarefas, executamos os testes e lemos os resultados.

Conclusão e a grande questão

A grande questão depois de ler esta lição é "Por que não posso simplesmente usar byte[] , já que é mais conveniente e não impõe restrições?"

A vantagem de ByteArrayInputStream é que ele indica fortemente que você usará bytes somente leitura (porque o fluxo não fornece uma interface para alterar seu conteúdo). Dito isso, é importante observar que um programador ainda pode acessar os bytes diretamente.

Mas se às vezes você tem um byte[] , às vezes você tem um arquivo, às vezes você tem uma conexão de rede e assim por diante, você precisará de algum tipo de abstração para "um fluxo de bytes, e não me importa onde eles vem de onde". E é isso que InputStream é. Quando a origem é uma matriz de bytes, ByteArrayInputStream é um bom InputStream para usar.

Isso é útil em muitas situações, mas aqui estão dois exemplos específicos:

  1. Você está escrevendo uma biblioteca que recebe bytes e os processa de alguma forma (por exemplo, suponha que seja uma biblioteca de utilitários de processamento de imagens). Os usuários de sua biblioteca podem fornecer bytes de um arquivo, de um byte[] na memória ou de alguma outra fonte. Portanto, você fornece uma interface que aceita um InputStream , o que significa que, se eles tiverem um byte[] , eles precisam envolvê-lo em um ByteArrayInputStream .

  2. Você está escrevendo um código que lê uma conexão de rede. Mas, para realizar testes de unidade neste código, você não deseja abrir uma conexão — apenas alimentar alguns bytes no código. Portanto, o código recebe um InputStream e seu teste passa em um ByteArrayInputStream .