De klasse ByteArrayOutputStream implementeert een uitvoerstroom die gegevens naar een byte-array schrijft. De buffer groeit automatisch naarmate er gegevens naar worden geschreven.

De klasse ByteArrayOutputStream maakt een buffer in het geheugen en alle gegevens die naar de stream worden verzonden, worden in de buffer opgeslagen.

ByteArrayOutputStream-constructors

De klasse ByteArrayOutputStream heeft de volgende constructors:

Constructeur
ByteArrayOutputStream() Deze constructor maakt een in-memory buffer van 32 bytes lang.
ByteArrayOutputStream(int a) Deze constructor maakt een buffer in het geheugen met een specifieke grootte.

En zo ziet de klas er van binnen uit:


// 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 van de klasse ByteArrayOutputStream

Laten we het hebben over de methoden die we in onze klas kunnen gebruiken.

Laten we proberen iets in onze stream te plaatsen. Om dit te doen, gebruiken we de methode write() — deze kan één byte of een reeks bytes accepteren om te schrijven.

Methode
ongeldig schrijven (int b) Schrijft één byte.
ongeldig schrijven (byte b[], int off, int len) Schrijft een reeks bytes van een bepaalde grootte.
ongeldig schrijvenBytes(byte b[]) Schrijft een reeks bytes.
void writeTo (OutputStream out) Schrijft alle gegevens van de huidige uitvoerstroom naar de doorgegeven uitvoerstroom.

Methode implementatie:


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

Het resultaat is een nieuw bestand output.txt dat er als volgt uitziet:

De methode toByteArray() retourneert de huidige inhoud van deze uitvoerstroom als een reeks bytes. En je kunt de toString() methode gebruiken om de buf byte array als tekst te krijgen:


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

Onze buffer bevat de byte-array die we eraan hebben doorgegeven.

De methode reset() zet het aantal geldige bytes in de uitvoerstroom van de byte-array terug naar nul (dus alles wat in de uitvoer is verzameld, wordt gereset).


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

Wanneer we onze buffer weergeven nadat we de methode reset() hebben aangeroepen , krijgen we niets.

Specifieke kenmerken van de close() methode

Deze methode verdient speciale aandacht. Laten we een kijkje nemen om te begrijpen wat het doet:


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

Merk op dat de methode close() van de klasse ByteArrayOutputStream eigenlijk niets doet.

Waarom is dat? Een ByteArrayOutputStream is een op het geheugen gebaseerde stroom (dat wil zeggen, deze wordt beheerd en gevuld door de gebruiker in code), dus het aanroepen van close() heeft geen effect.

Oefening

Laten we nu proberen een bestandssysteem te implementeren met behulp van onze ByteArrayOutputStream en ByteArrayInputStream .

Laten we een FileSystem- klasse schrijven met behulp van het singleton- ontwerppatroon en een statische HashMap<String, byte[]> gebruiken , waarbij:

  • String is het pad naar een bestand
  • byte[] zijn de gegevens in het opgeslagen bestand

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 deze klasse hebben we de volgende openbare methoden gemaakt:

  • standaard CRUD-methoden (maken, lezen, bijwerken, verwijderen),
  • een methode om te controleren of een bestand bestaat,
  • een methode om een ​​instantie van het bestandssysteem te krijgen.

Om uit een bestand te lezen, retourneren we een InputStream . Onder de motorkap bevindt zich de ByteArrayInputStream- implementatie. De buffer is een byte-array die is opgeslagen in de bestandsmap .

Een andere interessante methode is newOutputStream . Wanneer deze methode wordt aangeroepen, retourneren we een nieuw ByteArrayOutputStream- object dat twee methoden overschrijft: flush en close . Het aanroepen van een van deze methoden zou ervoor moeten zorgen dat het schrijven plaatsvindt.

En dat is precies wat we doen: we krijgen de byte-array waarnaar de gebruiker heeft geschreven en slaan een kopie op als dewaardein de bestandsmap met een geschikte sleutel.

We gebruiken de volgende code om ons bestandssysteem (FS) te 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);
   }
}
    

Tijdens de test verifiëren we de volgende acties:

  1. We maken een nieuw bestand aan.
  2. We controleren of het aangemaakte bestand leeg is.
  3. We schrijven wat gegevens naar het bestand.
  4. We lezen de gegevens terug en controleren of deze overeenkomen met wat we hebben geschreven.
  5. Wij verwijderen het bestand.
  6. We verifiëren dat het bestand is verwijderd.

Het uitvoeren van deze code geeft ons deze uitvoer:

Bestand succesvol aangemaakt
Bestandsinhoud:
Gegevens naar bestand geschreven
Bestandsinhoud: CodeGym
Bestand bestaat: onwaar

Waarom was dit voorbeeld nodig?

Simpel gezegd, gegevens zijn altijd een set bytes. Als u veel gegevens van/naar schijf moet lezen/schrijven, zal uw code traag werken vanwege I/O-problemen. In dit geval is het logisch om een ​​virtueel bestandssysteem in RAM te onderhouden en ermee te werken op dezelfde manier als met een traditionele schijf. En wat is er eenvoudiger dan InputStream en OutputStream ?

Dit is natuurlijk een voorbeeld voor instructie, geen productieklare code. Het houdt GEEN rekening met (de volgende lijst is niet volledig):

  • multithreading
  • limieten voor bestandsgrootte (de hoeveelheid beschikbaar RAM voor een draaiende JVM)
  • verificatie van de padstructuur
  • verificatie van methodeargumenten

Interessante insiderinformatie:
de CodeGym-taakverificatieserver gebruikt een enigszins vergelijkbare aanpak. We draaien een virtuele FS, bepalen welke tests moeten worden uitgevoerd voor taakverificatie, voeren de tests uit en lezen de resultaten.

Conclusie en de grote vraag

De grote vraag na het lezen van deze les is "Waarom kan ik niet gewoon byte[] gebruiken , omdat het handiger is en geen beperkingen oplegt?"

Het voordeel van ByteArrayInputStream is dat het sterk aangeeft dat u alleen-lezen bytes gaat gebruiken (omdat de stream geen interface biedt voor het wijzigen van de inhoud). Dat gezegd hebbende, is het belangrijk op te merken dat een programmeur nog steeds rechtstreeks toegang heeft tot de bytes.

Maar als je soms een byte[] hebt, soms een bestand, soms een netwerkverbinding, enzovoort, heb je een soort abstractie nodig voor "een stroom bytes, en het kan me niet schelen waar ze Komt van". En dat is wat InputStream is. Wanneer de bron toevallig een byte-array is, is ByteArrayInputStream een ​​goede InputStream om te gebruiken.

Dit is in veel situaties nuttig, maar hier zijn twee specifieke voorbeelden:

  1. U schrijft een bibliotheek die bytes ontvangt en deze op de een of andere manier verwerkt (stel bijvoorbeeld dat het een bibliotheek met hulpprogramma's voor beeldverwerking is). Gebruikers van uw bibliotheek kunnen bytes leveren van een bestand, van een in-memory byte[] of van een andere bron. U biedt dus een interface die een InputStream accepteert , wat betekent dat als ze een byte[] hebben, ze deze in een ByteArrayInputStream moeten verpakken .

  2. Je schrijft code die een netwerkverbinding leest. Maar om unit-tests op deze code uit te voeren, wilt u geen verbinding openen - u wilt alleen een paar bytes aan de code toevoegen. Dus de code neemt een InputStream en uw test slaagt in een ByteArrayInputStream .