Klasa ByteArrayOutputStream implementuje strumień wyjściowy, w którym dane są zapisywane w tablicy bajtów. Bufor automatycznie rośnie w miarę zapisywania w nim danych.

Strumień klasy ByteArrayOutputStream tworzy bufor w pamięci i wszelkie dane wysyłane do strumienia są przechowywane w buforze.

Konstruktory ByteArrayOutputStream

Klasa ByteArrayOutputStream ma następujące konstruktory:

Konstruktor
ByteArrayOutputStream() Konstruktor tworzy bufor w pamięci o wielkości 32 bajtów.
ByteArrayOutputStream(int a) Konstruktor tworzy bufor w pamięci o określonym rozmiarze.

A tak wygląda klasa w środku:


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

Metody ByteArrayOutputStream

Porozmawiajmy o metodach, które możemy zastosować na naszej lekcji.

Spróbujmy umieścić coś w naszym strumieniu. W tym celu skorzystamy z metody write() - może przyjąć jeden bajt lub zestaw bajtów, które musi napisać.

metoda
pusty zapis (int b) Napisz jeden bajt.
void write(byte b[], int off, int len) Napisz tablicę bajtów o określonym rozmiarze.
void writeBytes(bajt b[]) Zapisywanie tablicy bajtów.
void writeTo (OutputStream out) Zapisuje wszystkie dane z bieżącego strumienia wyjściowego do określonego strumienia wyjściowego.

Metody realizacji:


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

W wyniku wykonania stworzymy plik output.txt z tekstem w środku:

Metoda toByteArray() zwraca bieżącą zawartość tego strumienia wyjściowego jako tablicę bajtów. Korzystając z metody toString() , możesz uzyskać tablicę bajtów buf jako tekst:


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

W rezultacie nasz bufor zawiera tablicę bajtów, które do niego przekazaliśmy.

Metoda reset() resetuje liczbę prawidłowych bajtów w strumieniu wyjściowym tablicy bajtów do zera (więc wszystko zgromadzone na wyjściu zostanie zresetowane).


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

W rezultacie, gdy wyprowadzimy nasz bufor po metodzie reset() , nic nie otrzymamy.

Cechy metody close().

Ta metoda zasługuje na szczególną uwagę. Aby zrozumieć, co robi, wejdźmy do środka:


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

Warto zauważyć, że metoda close() na ByteArrayOutputStream w rzeczywistości nic nie robi.

Dlaczego tak? ByteArrayOutputStream to strumień oparty na pamięci (to znaczy zarządzany i wypełniany przez użytkownika w kodzie), więc wywołanie metody close() nie ma żadnego efektu .

Ćwiczyć

Teraz spróbujmy zaimplementować system plików przy użyciu naszych ByteArrayOutputStream i ByteArrayInputStream .

Napiszmy klasę FileSystem używając wzorca Singleton i użyjmy statycznej HashMap<String, byte[]> , gdzie:

  • String - ścieżka do pliku;
  • bajt[] - dane w zapisanym pliku.

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

W tej klasie stworzyliśmy metody publiczne:

  • standardowe metody CRUD (tworzenie, odczyt, aktualizacja, usuwanie),
  • sposób sprawdzenia, czy plik istnieje,
  • metoda uzyskiwania instancji systemu plików.

Aby odczytać z pliku, zwracamy InputStream użytkownikowi . Pod maską znajduje się implementacja ByteArrayInputStream . Bufor to tablica bajtów przechowywana w mapie plików .

Drugą interesującą metodą jest newOutputStream . Kiedy ta metoda jest wywoływana, zwracamy użytkownikowi nowy obiekt typu ByteArrayOutputStream z dwiema nadpisanymi metodami: flush i close . Wywołanie dowolnej z tych metod spowoduje uruchomienie zapisu.

Dokładnie to robimy: otrzymujemy tablicę bajtów, w której użytkownik coś zapisał, i zapisujemy jej kopię jakowartośćdo mapy plików z odpowiednim kluczem.

Aby przetestować zapisany system plików (FS), używamy następującego kodu:


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

Podczas testowania sprawdź działania:

  1. Tworzymy nowy plik.
  2. Sprawdź, czy utworzony plik jest pusty.
  3. Zapisz niektóre dane do pliku.
  4. Odczytujemy zarejestrowane dane, upewniamy się, że odpowiadają one zarejestrowanym.
  5. Usuwamy plik.
  6. Sprawdzamy, czy plik został usunięty.

W wyniku działania tego kodu otrzymujemy wynik:

Plik utworzony pomyślnie
Zawartość pliku:
Dane zapisane do pliku
Zawartość pliku: CodeGym
Plik istnieje: false

Dlaczego potrzebujemy tego przykładu?

To proste: dowolne dane to zestaw bajtów. Jeśli musisz często i dużo czytać z dysku i zapisywać na dysku, kod będzie działał wolno z powodu problemów z operacjami we/wy. W takim przypadku należy zachować wirtualny system plików w pamięci RAM, z którym można pracować w taki sam sposób, jak ze znanym dyskiem. A co może być prostszego niż InputStream i OutputStream ?

Oczywiście jest to przykład szkoleniowy, a nie produkcyjny kod do odczytu. NIE bierze pod uwagę (lista nie jest wyłączna):

  • dostęp wielowątkowy;
  • limity wielkości plików (ilość dostępnej pamięci RAM dla uruchomionej maszyny JVM);
  • brak sprawdzania struktury ścieżki;
  • nie ma kontroli danych otrzymanych jako parametry.

Interesujący wgląd:
Nieco podobne podejście jest stosowane na serwerze sprawdzania poprawności zadań CodeGym. Wirtualny FS jest podniesiony, określa się, które testy należy przeprowadzić, aby zweryfikować zadanie, testowanie i odczyt wyników.

Wniosek i główne pytanie

Najważniejszym pytaniem po przeczytaniu wykładu będzie: „Dlaczego nie mogę po prostu użyć byte[] , bo tak jest wygodniej i nie mam ograniczeń?”

Zaletą ByteArrayInputStream jest to, że jest to bardzo silna wskazówka, że ​​zamierzasz używać bajtów tylko do odczytu (ponieważ strumień nie zapewnia interfejsu do ich zmiany). Chociaż ważne jest, aby pamiętać, że programista może nadal uzyskiwać bezpośredni dostęp do bajtów.

Ale jeśli czasami jest to byte[] , czasami plik, czasami połączenie sieciowe itd., Będziesz potrzebował jakiejś abstrakcji dla „strumienia bajtów i nie obchodzi mnie, skąd pochodzą”. Tym właśnie jest InputStream . Gdy źródłem jest tablica bajtów, ByteArrayInputStream jest dobrym strumieniem wejściowym do użycia.

Jest to przydatne w wielu sytuacjach, ale oto dwa konkretne przykłady:

  1. Piszesz bibliotekę, która pobiera bajty i jakoś je przetwarza (na przykład jest to biblioteka do przetwarzania obrazu). Użytkownicy Twojej biblioteki mogą dostarczać bajty z pliku, z bajtu [] w pamięci lub z innego źródła. Zapewniasz więc interfejs, który akceptuje InputStream - co oznacza, że ​​​​jeśli mają byte[] , muszą zawinąć go w ByteArrayInputStream .

  2. Piszesz kod, który odczytuje połączenie sieciowe. Ale aby przetestować ten kod jednostkowo, nie musisz otwierać połączenia; po prostu chcesz dodać kilka bajtów do kodu. Tak więc kod pobiera InputStream , a twój test zapewnia ByteArrayInputStream .