Die ByteArrayOutputStream- Klasse implementiert einen Ausgabestream, der Daten in ein Byte-Array schreibt. Der Puffer wächst automatisch, wenn Daten darauf geschrieben werden.

Die ByteArrayOutputStream- Klasse erstellt einen Puffer im Speicher und alle an den Stream gesendeten Daten werden im Puffer gespeichert.

ByteArrayOutputStream-Konstruktoren

Die ByteArrayOutputStream -Klasse verfügt über die folgenden Konstruktoren:

Konstrukteur
ByteArrayOutputStream() Dieser Konstruktor erstellt einen 32 Byte langen In-Memory-Puffer.
ByteArrayOutputStream(int a) Dieser Konstruktor erstellt einen speicherinternen Puffer mit einer bestimmten Größe.

Und so sieht die Klasse von innen aus:


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

Methoden der ByteArrayOutputStream-Klasse

Lassen Sie uns über die Methoden sprechen, die wir in unserer Klasse verwenden können.

Versuchen wir, etwas in unseren Stream zu integrieren. Dazu verwenden wir die Methode write() – sie kann ein Byte oder eine Reihe von Bytes zum Schreiben akzeptieren.

Methode
void write(int b) Schreibt ein Byte.
void write(byte b[], int off, int len) Schreibt ein Array von Bytes einer bestimmten Größe.
void writeBytes(byte b[]) Schreibt ein Array von Bytes.
void writeTo(OutputStream out) Schreibt alle Daten vom aktuellen Ausgabestream in den übergebenen Ausgabestream.

Methodenimplementierung:


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

Das Ergebnis ist eine neue Datei „output.txt“ , die wie folgt aussieht:

Die Methode toByteArray() gibt den aktuellen Inhalt dieses Ausgabestreams als Array von Bytes zurück. Und Sie können die Methode toString() verwenden , um das Buf- Byte-Array als Text abzurufen :


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

Unser Puffer enthält das Byte-Array, das wir ihm übergeben haben.

Die Methode reset() setzt die Anzahl der gültigen Bytes im Byte-Array-Ausgabestream auf Null zurück (so dass alles, was sich in der Ausgabe angesammelt hat, zurückgesetzt wird).


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

Wenn wir unseren Puffer nach dem Aufruf der Methode reset() anzeigen , erhalten wir nichts.

Spezifische Merkmale der Methode close()

Diese Methode verdient besondere Aufmerksamkeit. Um zu verstehen, was es tut, werfen wir einen Blick hinein:


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

Beachten Sie, dass die Methode close() der ByteArrayOutputStream- Klasse eigentlich nichts bewirkt.

Warum das? Ein ByteArrayOutputStream ist ein speicherbasierter Stream (d. h. er wird vom Benutzer im Code verwaltet und gefüllt), daher hat der Aufruf von close() keine Auswirkung.

Üben

Versuchen wir nun, ein Dateisystem mithilfe unseres ByteArrayOutputStream und ByteArrayInputStream zu implementieren .

Schreiben wir eine FileSystem- Klasse mit dem Singleton- Entwurfsmuster und verwenden eine statische HashMap<String, byte[]> , wobei:

  • String ist der Pfad zu einer Datei
  • Byte[] sind die Daten in der gespeicherten Datei

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

In dieser Klasse haben wir die folgenden öffentlichen Methoden erstellt:

  • Standard-CRUD-Methoden (Erstellen, Lesen, Aktualisieren, Löschen),
  • eine Methode, um zu prüfen, ob eine Datei existiert,
  • eine Methode zum Abrufen einer Instanz des Dateisystems.

Um aus einer Datei zu lesen, geben wir einen InputStream zurück . Unter der Haube verbirgt sich die ByteArrayInputStream- Implementierung. Der Puffer ist ein Byte-Array, das in der Dateizuordnung gespeichert ist .

Eine weitere interessante Methode ist newOutputStream . Wenn diese Methode aufgerufen wird, geben wir ein neues ByteArrayOutputStream- Objekt zurück, das zwei Methoden überschreibt: Flush und Close . Der Aufruf einer dieser Methoden sollte dazu führen, dass der Schreibvorgang ausgeführt wird.

Und genau das machen wir: Wir holen uns das Byte-Array, in das der Benutzer geschrieben hat, und speichern eine Kopie alsWertin der Dateizuordnung mit einem entsprechenden Schlüssel.

Wir verwenden den folgenden Code, um unser Dateisystem (FS) zu testen:


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

Während des Tests überprüfen wir die folgenden Aktionen:

  1. Wir erstellen eine neue Datei.
  2. Wir prüfen, ob die erstellte Datei leer ist.
  3. Wir schreiben einige Daten in die Datei.
  4. Wir lesen die Daten noch einmal und überprüfen, ob sie mit dem übereinstimmen, was wir geschrieben haben.
  5. Wir löschen die Datei.
  6. Wir überprüfen, ob die Datei gelöscht wurde.

Wenn wir diesen Code ausführen, erhalten wir diese Ausgabe:

Datei erfolgreich erstellt.
Dateiinhalt:
In Datei geschriebene Daten.
Dateiinhalt: CodeGym.
Datei vorhanden: falsch

Warum war dieses Beispiel notwendig?

Vereinfacht ausgedrückt handelt es sich bei Daten immer um eine Reihe von Bytes. Wenn Sie viele Daten von/auf die Festplatte lesen/schreiben müssen, wird Ihr Code aufgrund von E/A-Problemen langsam ausgeführt. In diesem Fall ist es sinnvoll, ein virtuelles Dateisystem im RAM zu verwalten und damit genauso zu arbeiten wie mit einer herkömmlichen Festplatte. Und was könnte einfacher sein als InputStream und OutputStream ?

Natürlich handelt es sich hierbei um ein Beispiel für Anweisungen und nicht um produktionsbereiten Code. Es berücksichtigt NICHT (die folgende Liste ist nicht vollständig):

  • Multithreading
  • Dateigrößenbeschränkungen (die Menge an verfügbarem RAM für eine laufende JVM)
  • Überprüfung der Pfadstruktur
  • Überprüfung von Methodenargumenten

Interessante Insider-Informationen:
Der CodeGym-Aufgabenverifizierungsserver verwendet einen etwas ähnlichen Ansatz. Wir starten einen virtuellen FS, bestimmen, welche Tests zur Aufgabenüberprüfung ausgeführt werden müssen, führen die Tests aus und lesen die Ergebnisse.

Fazit und die große Frage

Die große Frage nach dem Lesen dieser Lektion lautet: „Warum kann ich nicht einfach byte[] verwenden , da es bequemer ist und keine Einschränkungen mit sich bringt?“

Der Vorteil von ByteArrayInputStream besteht darin, dass es deutlich anzeigt, dass Sie schreibgeschützte Bytes verwenden werden (da der Stream keine Schnittstelle zum Ändern seines Inhalts bereitstellt). Allerdings ist es wichtig zu beachten, dass ein Programmierer immer noch direkt auf die Bytes zugreifen kann .

Aber wenn Sie manchmal ein byte[] haben, manchmal eine Datei, manchmal eine Netzwerkverbindung usw., benötigen Sie eine Art Abstraktion für „einen Bytestrom, und es ist mir egal, wo sie sind.“ komme aus". Und genau das ist InputStream . Wenn die Quelle zufällig ein Byte-Array ist, ist ByteArrayInputStream ein guter InputStream .

Dies ist in vielen Situationen hilfreich, aber hier sind zwei konkrete Beispiele:

  1. Sie schreiben eine Bibliothek, die Bytes empfängt und sie irgendwie verarbeitet (angenommen, es handelt sich beispielsweise um eine Bibliothek mit Bildverarbeitungsdienstprogrammen). Benutzer Ihrer Bibliothek können Bytes aus einer Datei, aus einem In-Memory- Byte[] oder aus einer anderen Quelle bereitstellen. Sie stellen also eine Schnittstelle bereit, die einen InputStream akzeptiert . Das heißt, wenn sie einen byte[] haben , müssen sie ihn in einen ByteArrayInputStream einschließen .

  2. Sie schreiben Code, der eine Netzwerkverbindung liest. Um jedoch Unit-Tests für diesen Code durchzuführen, möchten Sie keine Verbindung öffnen, sondern lediglich ein paar Bytes in den Code einspeisen. Der Code benötigt also einen InputStream und Ihr Test wird in einem ByteArrayInputStream übergeben .