Klassen ByteArrayOutputStream implementerar en utdataström som skriver data till en bytearray. Bufferten växer automatiskt när data skrivs till den.

Klassen ByteArrayOutputStream skapar en buffert i minnet och all data som skickas till strömmen lagras i bufferten.

ByteArrayOutputStream-konstruktörer

Klassen ByteArrayOutputStream har följande konstruktorer:

Konstruktör
ByteArrayOutputStream() Denna konstruktor skapar en buffert i minnet som är 32 byte lång.
ByteArrayOutputStream(int a) Den här konstruktören skapar en buffert i minnet med en specifik storlek.

Och så här ser klassen ut inuti:


// 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 för klassen ByteArrayOutputStream

Låt oss prata om metoderna vi kan använda i vår klass.

Låt oss försöka lägga något i vår stream. För att göra detta använder vi metoden write() — den kan acceptera en byte eller en uppsättning byte för skrivning.

Metod
void write(int b) Skriver en byte.
void write(byte b[], int off, int len) Skriver en array av byte av en viss storlek.
void writeBytes(byte b[]) Skriver en array av byte.
void writeTo(OutputStream out) Skriver all data från den aktuella utströmmen till den passerade utströmmen.

Metodimplementering:


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 är en ny output.txt- fil som ser ut så här:

Metoden toByteArray() returnerar det aktuella innehållet i denna utdataström som en array av byte. Och du kan använda metoden toString() för att få buff -byte-arrayen som text:


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

Vår buffert innehåller byte-arrayen som vi skickade till den.

Metoden reset() återställer antalet giltiga byte i byte-matrisens utgångsström till noll (så att allt som ackumulerats i utgången återställs).


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 visar vår buffert efter att ha anropat metoden reset() får vi ingenting.

Specifika funktioner för close()-metoden

Denna metod förtjänar särskild uppmärksamhet. För att förstå vad det gör, låt oss ta en titt inuti:


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

Observera att ByteArrayOutputStream -klassens close() -metod faktiskt inte gör någonting.

Varför är det så? En ByteArrayOutputStream är en minnesbaserad ström (det vill säga den hanteras och fylls i av användaren i kod), så att anropa close() har ingen effekt.

Öva

Låt oss nu försöka implementera ett filsystem med våra ByteArrayOutputStream och ByteArrayInputStream .

Låt oss skriva en FileSystem- klass med singleton -designmönstret och använda en statisk HashMap<String, byte[]> , där:

  • Sträng är sökvägen till en fil
  • byte[] är data i den sparade filen

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 den här klassen skapade vi följande offentliga metoder:

  • standard CRUD-metoder (skapa, läsa, uppdatera, ta bort),
  • en metod för att kontrollera om en fil finns,
  • en metod för att få en instans av filsystemet.

För att läsa från en fil returnerar vi en InputStream . Under huven finns ByteArrayInputStream -implementationen. Bufferten är en byte-array som lagras i filkartan .

En annan intressant metod är newOutputStream . När den här metoden anropas returnerar vi ett nytt ByteArrayOutputStream -objekt som åsidosätter två metoder: flush och close . Att anropa någon av dessa metoder bör göra att skrivningen äger rum.

Och det är precis vad vi gör: vi får byte-arrayen som användaren har skrivit till och lagrar en kopia somvärdei filkartan med en lämplig nyckel.

Vi använder följande kod för att testa vårt 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 testet verifierar vi följande åtgärder:

  1. Vi skapar en ny fil.
  2. Vi kontrollerar att den skapade filen är tom.
  3. Vi skriver en del data till filen.
  4. Vi läser tillbaka uppgifterna och verifierar att de stämmer överens med det vi skrev.
  5. Vi tar bort filen.
  6. Vi verifierar att filen har raderats.

Att köra den här koden ger oss denna utdata:

Fil skapad framgångsrikt
Filinnehåll:
Data skriven till fil
Filinnehåll: CodeGym
Filen finns: falsk

Varför var detta exempel nödvändigt?

Enkelt uttryckt är data alltid en uppsättning byte. Om du behöver läsa/skriva mycket data från/till disk kommer din kod att köras långsamt på grund av I/O-problem. I det här fallet är det vettigt att upprätthålla ett virtuellt filsystem i RAM, och arbeta med det på samma sätt som du skulle göra med en traditionell disk. Och vad kan vara enklare än InputStream och OutputStream ?

Naturligtvis är detta ett exempel för instruktion, inte produktionsklar kod. Det tar INTE hänsyn till (följande lista är inte heltäckande):

  • multitrådning
  • filstorleksgränser (mängden tillgängligt RAM-minne för en körande JVM)
  • verifiering av vägstrukturen
  • verifiering av metodargument

Intressant insiderinformation:
CodeGyms uppgiftsverifieringsserver använder ett något liknande tillvägagångssätt. Vi snurrar upp en virtuell FS, bestämmer vilka tester som måste köras för uppgiftsverifiering, kör testerna och läser resultaten.

Slutsats och den stora frågan

Den stora frågan efter att ha läst den här lektionen är "Varför kan jag inte bara använda byte[] , eftersom det är bekvämare och inte medför begränsningar?"

Fördelen med ByteArrayInputStream är att det starkt indikerar att du kommer att använda skrivskyddade bytes (eftersom strömmen inte tillhandahåller ett gränssnitt för att ändra dess innehåll). Som sagt, det är viktigt att notera att en programmerare fortfarande kan komma åt byten direkt.

Men om du ibland har en byte[] , ibland har du en fil, ibland har du en nätverksanslutning och så vidare, behöver du någon form av abstraktion för "en ström av bytes, och jag bryr mig inte om var de komma från". Och det är vad InputStream är. När källan råkar vara en byte-array är ByteArrayInputStream en bra InputStream att använda.

Detta är användbart i många situationer, men här är två specifika exempel:

  1. Du skriver ett bibliotek som tar emot bytes och bearbetar dem på något sätt (anta till exempel att det är ett bibliotek med bildbehandlingsverktyg). Användare av ditt bibliotek kan tillhandahålla bytes från en fil, från en byte i minnet[] eller från någon annan källa. Så du tillhandahåller ett gränssnitt som accepterar en InputStream , vilket betyder att om de har en byte[] , måste de linda in den i en ByteArrayInputStream .

  2. Du skriver kod som läser en nätverksanslutning. Men för att utföra enhetstester på den här koden vill du inte öppna en anslutning – du vill bara mata några byte till koden. Så koden tar en InputStream och ditt test godkänns i en ByteArrayInputStream .