ByteArrayOutputStream

Модуль 1. Java Syntax
25 уровень , 1 лекция
Открыта

Класс 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.

Комментарии (17)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Пётр Уровень 75
23 апреля 2025
Подскажите пожалуйста, не уловил что-то. Почему если массив не четный то тогда делаем так

stream.write(bytes[length / 2])
почему если четный то все ок?
Станислав Уровень 68
17 апреля 2025
Как по мне, не совсем понятная подача материала в некоторых лекциях это пол беды. Лично мне не хватает задачек, чтобы можно было закрепить материал на практике и углубиться в тему самостоятельно. Например, вот сколько методов описано в лекции, столько должно быть и задач с их применением. Так же было бы неплохо, сперва задачи с применением только единственного конкретного метода, а потом пару-тройку задач сразу на несколько. В общем больше практики по теме, а теорию я найду где еще почитать-посмотреть. Подводя итог, в целом курс лично мне нравится. Так же хочу пожелать тем, у кого возникают трудности по изучению материала(я тоже из этого числа😅): главное — не сдавайтесь и не бросайте начатое! Все обязательно получится! Всем удачи!💪😉
Александр Уровень 66
30 июля 2024
Лекции через одну понимаю. Писали, походу, разные люди. Один пишет доступным языком, другой на более профессиональном. Черт пойми что...
Дмитрий Уровень 49
24 июня 2024
На этом уровне я не смог решить сам НИ ОДНОЙ задачи......... Руки опускаются, я не понимаю совсем это! Пятая точка летит впереди корабля spaceX.... Я не могу решать задачи от слова совсем!
zaiats1311 Уровень 41
24 марта 2024
Что это за модуль Java Syntax и как получить к нему доступ?
Anonymous #3414710 Уровень 1
13 февраля 2024
Благодарю автора за интересный пример с виртуальной файловой системой. Несколько дней над ним голову ломал. Хотя в описании всё черным по белому подробно описано как работает.Это было очень увлекательно.
Дмитрий Уровень 32
9 февраля 2024

private void validateExists(String path) {
      if (!files.containsKey(path)) {
           throw new RuntimeException("File not found");
       }
   }
Зачем писали метод isExist?
Andrzej Уровень 32 Expert
30 ноября 2023
Гм, может быть когда нибудь этот уровень станет чуть понятнее написан, а пока - пошел я в гугл-ютуб пытаться понять, что же на самом деле эти потоки ввода-вывода и как с ними работать ☹️
Наталья Уровень 107
13 сентября 2023
та за шо.....
Anonymous #3272489 Уровень 92 Expert
6 апреля 2023
если вы тоже себя не очень уверенно чувствуете в этой теме, советую видос https://youtu.be/r9paa9AJ7Gk