ByteArrayOutputStream - klassen implementerer en outputstrøm, der skriver data til en byte-array. Bufferen vokser automatisk, efterhånden som data skrives til den.

ByteArrayOutputStream - klassen opretter en buffer i hukommelsen, og alle data, der sendes til strømmen, gemmes i bufferen.

ByteArrayOutputStream-konstruktører

ByteArrayOutputStream - klassen har følgende konstruktører:

Konstruktør
ByteArrayOutputStream() Denne konstruktør opretter en buffer i hukommelsen, der er 32 byte lang.
ByteArrayOutputStream(int a) Denne konstruktør opretter en buffer i hukommelsen med en bestemt størrelse.

Og sådan ser klassen ud indeni:


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

Metoder i klassen ByteArrayOutputStream

Lad os tale om de metoder, vi kan bruge i vores klasse.

Lad os prøve at lægge noget i vores stream. For at gøre dette bruger vi metoden write() - den kan acceptere en byte eller et sæt bytes til skrivning.

Metode
void skrive(int b) Skriver en byte.
void skrive(byte b[], int off, int len) Skriver et array af bytes af en bestemt størrelse.
void skriveBytes(byte b[]) Skriver et array af bytes.
void writeTo(OutputStream ud) Skriver alle data fra den aktuelle outputstrøm til den beståede outputstrøm.

Implementering af metode:


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

Resultatet er en ny output.txt- fil, der ser sådan ud:

Metoden toByteArray() returnerer det aktuelle indhold af denne outputstrøm som en matrix af bytes. Og du kan bruge toString() -metoden til at få buf- byte-arrayet som 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]);
    }
}
    

Vores buffer indeholder byte-arrayet, som vi sendte til det.

Reset () -metoden nulstiller antallet af gyldige bytes i byte-array-outputstrømmen til nul (så alt akkumuleret i outputtet nulstilles).


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

Når vi viser vores buffer efter at have kaldt reset() metoden, får vi intet.

Specifikke træk ved close()-metoden

Denne metode fortjener særlig opmærksomhed. For at forstå, hvad det gør, lad os tage et kig ind:


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

Bemærk, at ByteArrayOutputStream- klassens close() -metode faktisk ikke gør noget.

Hvorfor det? En ByteArrayOutputStream er en hukommelsesbaseret strøm (det vil sige, den administreres og udfyldes af brugeren i kode), så at kalde close() har ingen effekt.

Øve sig

Lad os nu prøve at implementere et filsystem ved hjælp af vores ByteArrayOutputStream og ByteArrayInputStream .

Lad os skrive en FileSystem- klasse ved hjælp af singleton- designmønsteret og bruge et statisk HashMap<String, byte[]> , hvor:

  • String er stien til en fil
  • byte[] er dataene i den gemte fil

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

I denne klasse har vi oprettet følgende offentlige metoder:

  • standard CRUD metoder (oprette, læse, opdatere, slette),
  • en metode til at kontrollere, om en fil eksisterer,
  • en metode til at få en instans af filsystemet.

For at læse fra en fil returnerer vi en InputStream . Under hætten er ByteArrayInputStream implementeringen. Bufferen er et byte-array, der er gemt i filkortet .

En anden interessant metode er newOutputStream . Når denne metode kaldes, returnerer vi et nyt ByteArrayOutputStream- objekt, der tilsidesætter to metoder: flush og close . Kaldning af en af ​​disse metoder bør få skrivningen til at finde sted.

Og det er præcis, hvad vi gør: vi får det byte-array, som brugeren har skrevet til, og gemmer en kopi somværdii filkortet med en passende nøgle.

Vi bruger følgende kode til at teste vores filsystem (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);
   }
}
    

Under testen verificerer vi følgende handlinger:

  1. Vi opretter en ny fil.
  2. Vi kontrollerer, at den oprettede fil er tom.
  3. Vi skriver nogle data til filen.
  4. Vi læser dataene tilbage og kontrollerer, at de stemmer overens med det, vi skrev.
  5. Vi sletter filen.
  6. Vi bekræfter, at filen er blevet slettet.

At køre denne kode giver os dette output:

Fil oprettet med succes
Filindhold:
Data skrevet til fil
Filindhold: CodeGym
Filen findes: falsk

Hvorfor var dette eksempel nødvendigt?

Enkelt sagt er data altid et sæt bytes. Hvis du skal læse/skrive en masse data fra/til disk, vil din kode køre langsomt på grund af I/O problemer. I dette tilfælde giver det mening at vedligeholde et virtuelt filsystem i RAM og arbejde med det på samme måde, som du ville gøre med en traditionel disk. Og hvad kunne være enklere end InputStream og OutputStream ?

Dette er selvfølgelig et eksempel på instruktion, ikke produktionsklar kode. Det tager IKKE højde for (følgende liste er ikke udtømmende):

  • multithreading
  • filstørrelsesgrænser (mængden af ​​tilgængelig RAM for en kørende JVM)
  • verifikation af stistrukturen
  • verifikation af metodeargumenter

Interessant insiderinformation:
CodeGym opgavebekræftelsesserveren bruger en noget lignende tilgang. Vi opretter en virtuel FS, bestemmer, hvilke test der skal køres til opgaveverifikation, kører testene og læser resultaterne.

Konklusion og det store spørgsmål

Det store spørgsmål efter at have læst denne lektion er "Hvorfor kan jeg ikke bare bruge byte[] , da det er mere bekvemt og ikke pålægger begrænsninger?"

Fordelen ved ByteArrayInputStream er, at det stærkt indikerer, at du vil bruge skrivebeskyttede bytes (fordi streamen ikke giver en grænseflade til at ændre indholdet). Når det er sagt, er det vigtigt at bemærke, at en programmør stadig kan få direkte adgang til bytes.

Men hvis du nogle gange har en byte[] , nogle gange har du en fil, nogle gange har du en netværksforbindelse og så videre, har du brug for en form for abstraktion for "en strøm af bytes, og jeg er ligeglad med hvor de kommer fra". Og det er, hvad InputStream er. Når kilden tilfældigvis er et byte-array, er ByteArrayInputStream en god InputStream at bruge.

Dette er nyttigt i mange situationer, men her er to specifikke eksempler:

  1. Du skriver et bibliotek, der modtager bytes og behandler dem på en eller anden måde (antag for eksempel, at det er et bibliotek af billedbehandlingsværktøjer). Brugere af dit bibliotek kan levere bytes fra en fil, fra en byte i hukommelsen [] , eller fra en anden kilde. Så du giver en grænseflade, der accepterer en InputStream , hvilket betyder, at hvis de har en byte[] , skal de pakke den ind i en ByteArrayInputStream .

  2. Du skriver kode, der læser en netværksforbindelse. Men for at udføre enhedstests på denne kode, ønsker du ikke at åbne en forbindelse - du vil blot føre et par bytes til koden. Så koden tager en InputStream , og din test består i en ByteArrayInputStream .