Класс 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);
}
}
При тестировании проверим действия:
- Создаем новый файл.
- Проверяем, что созданный файл пустой.
- Записываем какие-либо данные в файл.
- Читаем записанные данные, убеждаемся, что они соответствуют записанным.
- Удаляем файл.
- Проверяем, что файл удалился.
В результате работы этого кода получим вывод:
Содержимое файла:
Данные записаны в файл
Содержимое файла: JavaRush
Файл существует: false
Зачем нам был нужен этот пример?
Все просто: любые данные — это набор байтов. Если нужно часто и много читать с диска и записывать на диск, код будет работать медленно из-за проблем IO. В таком случае уместно держать в RAM виртуальную файловую систему, с которой можно работать так же, как и с привычным диском. А что может быть проще, чем InputStream и OutputStream?
Естественно, это учебный пример, а не production read код. В нем НЕ учтены (список не исключительный):
- многопоточный доступ;
- ограничения на размер файлов (объем доступной RAM для запущенной JVM);
- нет проверки структуры путей;
- нет проверок на данные, полученные в качестве параметров.
Интересный инсайд:
В чем-то похожий подход используется на сервере валидации задач JavaRush. Поднимается виртуальная FS, определяется, какие именно тесты нужно запустить для валидации задачи, происходит тестирование и чтение результатов.
Итог и главный вопрос
Самым главным вопросом после прочтения лекции будет: “Почему я не могу просто использовать byte[], ведь это удобнее и у меня нет ограничений?”
Преимущество ByteArrayInputStream — он служит очень убедительным указанием на то, что вы собираетесь использовать байты только для чтения (поскольку поток не предоставляет интерфейс для их изменения). Хотя, важно отметить, что программист может по-прежнему напрямую обращаться к байтам.
Но если иногда это byte[], иногда файл, иногда сетевое соединение и так далее, вам будет нужна какая-то абстракция для "потока байтов, и мне все равно, откуда они". Вот что такое InputStream. Когда источником оказывается байтовый массив, ByteArrayInputStream является хорошим InputStream для использования.
Это полезно во многих ситуациях, но приведем два конкретных примера:
Вы пишете библиотеку, которая принимает байты и как-то их обрабатывает (например, это библиотека обработки изображений). Пользователи вашей библиотеки могут предоставлять байты из файла, из byte[] в памяти или из какого-либо другого источника. Итак, вы предоставляете интерфейс, который принимает InputStream — это означает, что если у них есть byte[], им нужно обернуть его в ByteArrayInputStream.
Вы пишете код, который считывает сетевое соединение. Но для модульного тестирования этого кода вам не нужно открывать соединение; вы хотите просто добавить несколько байтов в код. Таким образом, код принимает InputStream, а ваш тест предоставляет ByteArrayInputStream.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ