La classe ByteArrayOutputStream implementa un flusso di output che scrive i dati in una matrice di byte. Il buffer cresce automaticamente man mano che i dati vengono scritti su di esso.

La classe ByteArrayOutputStream crea un buffer in memoria e tutti i dati inviati al flusso vengono archiviati nel buffer.

Costruttori ByteArrayOutputStream

La classe ByteArrayOutputStream ha i seguenti costruttori:

Costruttore
ByteArrayOutputStream() Questo costruttore crea un buffer in memoria lungo 32 byte.
ByteArrayOutputStream(int a) Questo costruttore crea un buffer in memoria con una dimensione specifica.

E questo è l'aspetto della classe all'interno:


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

Metodi della classe ByteArrayOutputStream

Parliamo dei metodi che possiamo usare nella nostra classe.

Proviamo a inserire qualcosa nel nostro stream. Per fare questo, useremo il metodo write() — può accettare un byte o un insieme di byte per la scrittura.

Metodo
void scrivere(int b) Scrive un byte.
void write(byte b[], int off, int len) Scrive una matrice di byte di una dimensione specifica.
void writeBytes(byte b[]) Scrive una matrice di byte.
void writeTo(OutputStream out) Scrive tutti i dati dal flusso di output corrente al flusso di output passato.

Implementazione del metodo:


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

Il risultato è un nuovo file output.txt simile a questo:

Il metodo toByteArray() restituisce il contenuto corrente di questo flusso di output come un array di byte. E puoi usare il metodo toString() per ottenere l' array di byte buf come testo:


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

Il nostro buffer contiene l'array di byte che gli abbiamo passato.

Il metodo reset() azzera il numero di byte validi nel flusso di output dell'array di byte (quindi tutto ciò che è stato accumulato nell'output viene ripristinato).


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

Quando mostriamo il nostro buffer dopo aver chiamato il metodo reset() , non otteniamo nulla.

Caratteristiche specifiche del metodo close()

Questo metodo merita un'attenzione speciale. Per capire cosa fa, diamo uno sguardo all'interno:


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

Si noti che il metodo close() della classe ByteArrayOutputStream in realtà non fa nulla.

Perché? Un ByteArrayOutputStream è un flusso basato sulla memoria (ovvero, è gestito e popolato dall'utente nel codice), quindi la chiamata close() non ha alcun effetto.

Pratica

Ora proviamo ad implementare un file system utilizzando i nostri ByteArrayOutputStream e ByteArrayInputStream .

Scriviamo una classe FileSystem usando il modello di progettazione singleton e usiamo un HashMap<String, byte[]> statico , dove:

  • String è il percorso di un file
  • byte[] sono i dati nel file salvato

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 questa classe, abbiamo creato i seguenti metodi pubblici:

  • metodi CRUD standard (crea, leggi, aggiorna, elimina),
  • un metodo per verificare se un file esiste,
  • un metodo per ottenere un'istanza del file system.

Per leggere da un file, restituiamo un InputStream . Sotto il cofano c'è l' implementazione ByteArrayInputStream . Il buffer è un array di byte memorizzato nella mappa dei file .

Un altro metodo interessante è newOutputStream . Quando viene chiamato questo metodo, viene restituito un nuovo oggetto ByteArrayOutputStream che sovrascrive due metodi: flush e close . La chiamata di uno di questi metodi dovrebbe causare l'esecuzione della scrittura.

Ed è esattamente ciò che facciamo: otteniamo l'array di byte su cui l'utente ha scritto e memorizziamo una copia come thevalorenella mappa dei file con una chiave appropriata.

Utilizziamo il seguente codice per testare il nostro file system (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);
   }
}
    

Durante il test, verifichiamo le seguenti azioni:

  1. Creiamo un nuovo file.
  2. Controlliamo che il file creato sia vuoto.
  3. Scriviamo alcuni dati nel file.
  4. Rileggiamo i dati e verifichiamo che corrispondano a quanto scritto.
  5. Cancelliamo il file.
  6. Verifichiamo che il file è stato eliminato.

L'esecuzione di questo codice ci dà questo output:

File creato correttamente
Contenuto del file:
dati scritti nel file
Contenuto del file: CodeGym
Il file esiste: falso

Perché era necessario questo esempio?

In parole povere, i dati sono sempre un insieme di byte. Se hai bisogno di leggere/scrivere molti dati da/su disco, il tuo codice verrà eseguito lentamente a causa di problemi di I/O. In questo caso, ha senso mantenere un file system virtuale nella RAM, lavorando con esso nello stesso modo in cui si farebbe con un disco tradizionale. E cosa potrebbe esserci di più semplice di InputStream e OutputStream ?

Naturalmente, questo è un esempio di istruzione, non di codice pronto per la produzione. NON tiene conto (l'elenco seguente non è esaustivo):

  • multithreading
  • limiti di dimensione del file (la quantità di RAM disponibile per una JVM in esecuzione)
  • verifica della struttura del percorso
  • verifica degli argomenti del metodo

Interessanti informazioni privilegiate:
il server di verifica delle attività CodeGym utilizza un approccio in qualche modo simile. Avviamo un FS virtuale, determiniamo quali test devono essere eseguiti per la verifica dell'attività, eseguiamo i test e leggiamo i risultati.

Conclusione e la grande domanda

La grande domanda dopo aver letto questa lezione è "Perché non posso semplicemente usare byte[] , dato che è più conveniente e non impone restrizioni?"

Il vantaggio di ByteArrayInputStream è che indica fortemente che utilizzerai byte di sola lettura (poiché il flusso non fornisce un'interfaccia per modificarne il contenuto). Detto questo, è importante notare che un programmatore può ancora accedere direttamente ai byte.

Ma se a volte hai un byte[] , a volte hai un file, a volte hai una connessione di rete e così via, avrai bisogno di una sorta di astrazione per "un flusso di byte, e non mi interessa dove sono venire da". Ed è quello che è InputStream . Quando la sorgente sembra essere un array di byte, ByteArrayInputStream è un buon InputStream da usare.

Questo è utile in molte situazioni, ma ecco due esempi specifici:

  1. Stai scrivendo una libreria che riceve byte e li elabora in qualche modo (ad esempio, supponiamo che sia una libreria di utilità di elaborazione delle immagini). Gli utenti della tua libreria possono fornire byte da un file, da un byte[] in memoria o da qualche altra fonte. Quindi fornisci un'interfaccia che accetta un InputStream , il che significa che se hanno un byte[] , devono avvolgerlo in un ByteArrayInputStream .

  2. Stai scrivendo codice che legge una connessione di rete. Ma per eseguire unit test su questo codice, non vuoi aprire una connessione, vuoi solo alimentare alcuni byte al codice. Quindi il codice accetta un InputStream e il tuo test passa in un ByteArrayInputStream .