Класът ByteArrayOutputStream реализира изходен поток, който записва данни в byteов масив. Буферът нараства автоматично, докато данните се записват в него.

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

ByteArrayOutputStream конструктори

Класът ByteArrayOutputStream има следните конструктори:

Конструктор
ByteArrayOutputStream() Този конструктор създава буфер в паметта с дължина 32 byteа.
ByteArrayOutputStream(int a) Този конструктор създава буфер в паметта с определен размер.

Ето How изглежда класът отвътре:


// 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];
}
    

Методи на класа ByteArrayOutputStream

Нека поговорим за методите, които можем да използваме в нашия клас.

Нека се опитаме да вкараме нещо в нашия поток. За да направим това, ще използваме метода write() — той може да приеме един byte or набор от byteове за запис.

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

Изпълнение на метода:


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

Резултатът е нов файл output.txt , който изглежда така:

Методът toByteArray() връща текущото съдържание на този изходен поток като масив от byteове. И можете да използвате метода toString(), за да получите buf byteовия масив като текст:


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

Нашият буфер съдържа byteовия масив, който сме му предали.

Методът reset() нулира броя на валидните byteове в изходния поток от byteов масив до нула (така че всичко, натрупано в изхода, се нулира).


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

Когато показваме нашия буфер след извикване на метода reset() , не получаваме нищо.

Специфични характеристики на метода close().

Този метод заслужава специално внимание. За да разберем Howво прави, нека надникнем вътре:


/**
 * 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 е базиран на памет поток (т.е. той се управлява и попълва от потребителя в code), така че извикването на close () няма ефект.

Практикувайте

Сега нека се опитаме да внедрим файлова система, използвайки нашите ByteArrayOutputStream и ByteArrayInputStream .

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

  • Низът е пътят до файл
  • 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 методи (създаване, четене, актуализиране, изтриване),
  • метод за проверка дали файлът съществува,
  • метод за получаване на екземпляр на файловата система.

За да четем от файл, връщаме InputStream . Под капака е изпълнението на ByteArrayInputStream . Буферът е byteов масив, съхраняван в картата на файловете .

Друг интересен метод е newOutputStream . Когато този метод бъде извикан, ние връщаме нов обект ByteArrayOutputStream , който замества два метода: flush и close . Извикването на някой от тези методи трябва да доведе до извършване на запис.

И точно това правим: получаваме byteовия масив, в който потребителят е написал, и съхраняваме копие катостойноств картата на файловете с подходящ ключ.

Използваме следния code, за да тестваме нашата файлова система (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);
   }
}
    

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

  1. Създаваме нов файл.
  2. Проверяваме дали създаденият файл е празен.
  3. Записваме някои данни във file.
  4. Прочитаме обратно данните и проверяваме дали съвпадат с написаното от нас.
  5. Изтриваме file.
  6. Проверяваме дали файлът е изтрит.

Изпълнението на този code ни дава този резултат:

Файлът е създаден успешно
Съдържание на file:
Данни, записани във файл
Съдържание на file: CodeGym
Файлът съществува: невярно

Защо беше необходим този пример?

Казано просто, данните винаги са набор от byteове. Ако трябва да прочетете/запишете много данни от/на диск, вашият code ще работи бавно поради I/O проблеми. В този случай има смисъл да поддържате виртуална файлова система в RAM, като работите с нея по същия начин, по който бихте работor с традиционен диск. И Howво може да бъде по-просто от InputStream и OutputStream ?

Разбира се, това е пример за инструкция, а не готов за производство code. Той НЕ отчита (следният списък не е изчерпателен):

  • многопоточност
  • ограничения на размера на file (количеството налична RAM за работеща JVM)
  • проверка на структурата на пътя
  • проверка на аргументите на метода

Интересна вътрешна информация:
Сървърът за проверка на задачите на CodeGym използва донякъде подобен подход. Ние завъртаме виртуален FS, определяме кои тестове трябва да бъдат изпълнени за проверка на задачата, изпълняваме тестовете и четем резултатите.

Заключение и големият въпрос

Големият въпрос след прочитането на този урок е "Защо не мога да използвам просто byte[] , след като е по-удобно и не налага ограничения?"

Предимството на ByteArrayInputStream е, че той силно показва, че ще използвате byteове само за четене (тъй като потокът не предоставя интерфейс за промяна на съдържанието му). Въпреки това е важно да се отбележи, че програмистът все още може да има директен достъп до byteовете.

Но ако понякога имате byte[] , понякога имате файл, понякога имате мрежова връзка и т.н., ще ви трябва няHowва абстракция за „поток от byteове и не ме интересува къде идвам от". И това е InputStream . Когато източникът се окаже byteов масив, ByteArrayInputStream е добър InputStream за използване.

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

  1. Вие пишете библиотека, която получава byteове и ги обработва по няHowъв начин (например, да предположим, че това е библиотека от помощни програми за обработка на изображения). Потребителите на вашата библиотека могат да предоставят byteове от файл, от byte в паметта[] or от няHowъв друг източник. Така че предоставяте интерфейс, който приема InputStream , което означава, че ако имат byte[] , те трябва да го обвият в ByteArrayInputStream .

  2. Вие пишете code, който чете мрежова връзка. Но за да извършите модулни тестове на този code, не искате да отваряте връзка — просто искате да подадете няколко byteа към codeа. Така че codeът приема InputStream и вашият тест преминава в ByteArrayInputStream .