Класс ByteArrayOutputStream реализует поток вывода, в котором данные пишутся в байтовый массив. Буфер автоматически растет, поскольку данные пишутся ему.

Поток класса ByteArrayOutputStream создает буфер в памяти, и все данные, отправленные в поток, хранятся в буфере.

Конструкторы ByteArrayOutputStream

У класса ByteArrayOutputStream есть такие конструкторы:

Конструктор
ByteArrayOutputStream() Конструктор создает буфер в памяти в 32 байта.
ByteArrayOutputStream(int a) Конструктор создает буфер в памяти с определенным размером.

А вот так класс выглядит внутри:

//сам буфер, в котором хранятся данные.
protected byte buf[];

//текущее количество байтов, записанных в буфер.
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];
}

Методы ByteArrayOutputStream

Давайте поговорим о методах, которые мы можем использовать в нашем классе.

Попробуем положить что-то в наш поток. Для этого будем использовать метод write() — он может принять один байт или набор байтов, который ему нужно записать.

Метод
void write(int b) Запись одного байта.
void write(byte b[], int off, int len) Запись массива байтов определенного размера.
void writeBytes(byte b[]) Запись массива байтов.
void writeTo(OutputStream out) Записывает все данные текущего выходного потока в указанный выходной поток.

Реализация методов:

public static void main(String[] args) throws IOException {
   ByteArrayOutputStream outputByte = new ByteArrayOutputStream();
   //запись одного байта
   while(outputByte.size()!= 8) {
      outputByte.write("javarush".getBytes());
   }

   //запись массива байтов
   String value = "\nWelcome to Java\n";
   byte[] arrBytes = value.getBytes();
   outputByte.write(arrBytes);

   //запись массива по размерности
   String javaRush = "JavaRush";
   byte[] b = javaRush.getBytes();
   outputByte.write(b, 4, 4);

   //запись в файл
   FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
   outputByte.write(80);
   outputByte.writeTo(fileOutputStream);
}

В результате выполнения у нас создастся файл output.txt с текстом внутри:

Метод toByteArray() возвращает текущее содержимое этого выходного потока в виде массива байтов. А с помощью метода toString() можно получить массив байтов buf в виде текста:

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

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

    byte[] result = outputByte.toByteArray();
    System.out.println("Вывод содержимого:");

    for(int i = 0 ; i < result.length; i++) {
        // Вывод символов
        System.out.print((char)result[i]);
    }
}

Как результат — в нашем буфере лежит массив байтов, которые мы ему передали.

Метод reset() сбрасывает количество действительных байт в выходном потоке байтового массива до нуля (поэтому все накопленное на выходе будет сброшено).

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

   String value = "JavaRush";
   outputByte.write(value.getBytes());
   byte[] result = outputByte.toByteArray();
   System.out.println("Вывод до сброса: ");

   for (byte b : result) {
      // Вывод символов
      System.out.print((char) b);
   }

   outputByte.reset();

   byte[] resultAfterReset = outputByte.toByteArray();
   System.out.println("\nВывод содержимого после сброса:");

   for (byte b : resultAfterReset) {
      // Вывод символов
      System.out.print((char) b);
   }
}

В результате при выводе нашего буфера после метода reset() мы ничего не получим.

Особенности метода close()

Этот метод заслуживает особого внимания. Чтобы понять, что он делает, перейдем внутрь него:

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

Стоит отметить, что метод close() в ByteArrayOutputStream, по факту, ничего не делает.

Почему же так? ByteArrayOutputStream — это поток на основе памяти (то есть, управляемый и заполняемый пользователем в коде), поэтому при вызове close() никакого эффекта не происходит.

Практика

Теперь давайте попробуем реализовать файловую систему, используя наш ByteArrayOutputStream и ByteArrayInputStream.

Напишем класс FileSystem с применением паттерна Singleton и будем использовать статическую HashMap<String, byte[]>, где:

  • String — путь к файлу;
  • byte[] — данные в сохраненном файле.
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");
       }
   }
}

В этом классе мы создали публичные методы:

  • стандартные методы CRUD (create, read, update, delete),
  • метод проверки того, существует ли файл,
  • метод получения инстанса файловой системы.

Для чтения из файла возвращаем пользователю InputStream. Под капотом — реализация ByteArrayInputStream. Буфером выступает байтовый массив, который хранится в мапе files.

Второй интересный метод — newOutputStream. При вызове этого метода мы возвращаем пользователю новый объект типа ByteArrayOutputStream с двумя переопределенными методами: flush и close. Вызов любого из этих методов выступает триггером того, что нужно произвести запись.

Именно это мы и делаем: получаем байтовый массив, в который пользователь писал что угодно, и сохраняем его копию как value в мапу files с соответствующим ключом.

Для тестирования написанной файловой системы (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";

       // Создаем файл
       fileSystem.create(path);
       System.out.println("Файл успешно создан");

       // Убеждаемся что он пустой
       try (InputStream inputStream = fileSystem.newInputStream(path)) {
           System.out.print("Содержимое файла:\t");
           System.out.println(read(inputStream));
       }

       // Записываем в него данные
       try (final OutputStream outputStream = fileSystem.newOutputStream(path)) {
           outputStream.write("JavaRush".getBytes(UTF_8));
           System.out.println("Данные записаны в файл");
       }

       // Читаем данные
       try (InputStream inputStream = fileSystem.newInputStream(path)) {
           System.out.print("Содержимое файла:\t");
           System.out.println(read(inputStream));
       }

       // Удаляем файл
       fileSystem.delete(path);

       // Проверяем что такого файла в FS не существует
       System.out.print("Файл существует:\t");
       System.out.println(fileSystem.isExists(path));

   }

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

При тестировании проверим действия:

  1. Создаем новый файл.
  2. Проверяем, что созданный файл пустой.
  3. Записываем какие-либо данные в файл.
  4. Читаем записанные данные, убеждаемся, что они соответствуют записанным.
  5. Удаляем файл.
  6. Проверяем, что файл удалился.

В результате работы этого кода получим вывод:

Файл успешно создан
Содержимое файла:
Данные записаны в файл
Содержимое файла: JavaRush
Файл существует: false

Зачем нам был нужен этот пример?

Все просто: любые данные — это набор байтов. Если нужно часто и много читать с диска и записывать на диск, код будет работать медленно из-за проблем IO. В таком случае уместно держать в RAM виртуальную файловую систему, с которой можно работать так же, как и с привычным диском. А что может быть проще, чем InputStream и OutputStream?

Естественно, это учебный пример, а не production read код. В нем НЕ учтены (список не исключительный):

  • многопоточный доступ;
  • ограничения на размер файлов (объем доступной RAM для запущенной JVM);
  • нет проверки структуры путей;
  • нет проверок на данные, полученные в качестве параметров.

Интересный инсайд:
В чем-то похожий подход используется на сервере валидации задач JavaRush. Поднимается виртуальная FS, определяется, какие именно тесты нужно запустить для валидации задачи, происходит тестирование и чтение результатов.

Итог и главный вопрос

Самым главным вопросом после прочтения лекции будет: “Почему я не могу просто использовать byte[], ведь это удобнее и у меня нет ограничений?”

Преимущество ByteArrayInputStream — он служит очень убедительным указанием на то, что вы собираетесь использовать байты только для чтения (поскольку поток не предоставляет интерфейс для их изменения). Хотя, важно отметить, что программист может по-прежнему напрямую обращаться к байтам.

Но если иногда это byte[], иногда файл, иногда сетевое соединение и так далее, вам будет нужна какая-то абстракция для "потока байтов, и мне все равно, откуда они". Вот что такое InputStream. Когда источником оказывается байтовый массив, ByteArrayInputStream является хорошим InputStream для использования.

Это полезно во многих ситуациях, но приведем два конкретных примера:

  1. Вы пишете библиотеку, которая принимает байты и как-то их обрабатывает (например, это библиотека обработки изображений). Пользователи вашей библиотеки могут предоставлять байты из файла, из byte[] в памяти или из какого-либо другого источника. Итак, вы предоставляете интерфейс, который принимает InputStream — это означает, что если у них есть byte[], им нужно обернуть его в ByteArrayInputStream.

  2. Вы пишете код, который считывает сетевое соединение. Но для модульного тестирования этого кода вам не нужно открывать соединение; вы хотите просто добавить несколько байтов в код. Таким образом, код принимает InputStream, а ваш тест предоставляет ByteArrayInputStream.