La classe ByteArrayOutputStream implémente un flux de sortie qui écrit des données dans un tableau d'octets. La mémoire tampon s'agrandit automatiquement au fur et à mesure que des données y sont écrites.

La classe ByteArrayOutputStream crée un tampon en mémoire et toutes les données envoyées au flux sont stockées dans le tampon.

Constructeurs ByteArrayOutputStream

La classe ByteArrayOutputStream a les constructeurs suivants :

Constructeur
ByteArrayOutputStream() Ce constructeur crée un tampon en mémoire d'une longueur de 32 octets.
ByteArrayOutputStream(int a) Ce constructeur crée un tampon en mémoire avec une taille spécifique.

Et voici à quoi ressemble la classe à l'intérieur :


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

Méthodes de la classe ByteArrayOutputStream

Parlons des méthodes que nous pouvons utiliser dans notre classe.

Essayons de mettre quelque chose dans notre flux. Pour ce faire, nous utiliserons la méthode write() — elle peut accepter un octet ou un ensemble d'octets pour l'écriture.

Méthode
annuler l'écriture (int b) Écrit un octet.
void write(byte b[], int off, int len) Écrit un tableau d'octets d'une taille spécifique.
void writeBytes(octet b[]) Écrit un tableau d'octets.
void writeTo(OutputStream out) Écrit toutes les données du flux de sortie actuel dans le flux de sortie transmis.

Mise en œuvre de la méthode :


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

Le résultat est un nouveau fichier output.txt qui ressemble à ceci :

La méthode toByteArray() renvoie le contenu actuel de ce flux de sortie sous la forme d'un tableau d'octets. Et vous pouvez utiliser la méthode toString() pour obtenir le tableau d'octets buf sous forme de texte :


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

Notre tampon contient le tableau d'octets que nous lui avons transmis.

La méthode reset() remet à zéro le nombre d'octets valides dans le flux de sortie du tableau d'octets (ainsi, tout ce qui est accumulé dans la sortie est réinitialisé).


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

Lorsque nous affichons notre tampon après avoir appelé la méthode reset() , nous n'obtenons rien.

Spécificités de la méthode close()

Cette méthode mérite une attention particulière. Pour comprendre ce qu'il fait, jetons un coup d'œil à l'intérieur:


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

Notez que la méthode close() de la classe ByteArrayOutputStream ne fait rien.

Pourquoi donc? Un ByteArrayOutputStream est un flux basé sur la mémoire (c'est-à-dire qu'il est géré et rempli par l'utilisateur dans le code), donc l'appel de close() n'a aucun effet.

Pratique

Essayons maintenant d'implémenter un système de fichiers en utilisant nos ByteArrayOutputStream et ByteArrayInputStream .

Écrivons une classe FileSystem en utilisant le modèle de conception singleton et utilisons un HashMap<String, byte[]> statique , où :

  • String est le chemin d'accès à un fichier
  • byte[] est les données dans le fichier enregistré

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

Dans cette classe, nous avons créé les méthodes publiques suivantes :

  • les méthodes CRUD standards (create, read, update, delete),
  • une méthode pour vérifier si un fichier existe,
  • une méthode pour obtenir une instance du système de fichiers.

Pour lire à partir d'un fichier, nous renvoyons un InputStream . Sous le capot se trouve l' implémentation de ByteArrayInputStream . Le tampon est un tableau d'octets stocké dans la carte des fichiers .

Une autre méthode intéressante est newOutputStream . Lorsque cette méthode est appelée, nous renvoyons un nouvel objet ByteArrayOutputStream qui remplace deux méthodes : flush et close . L'appel de l'une de ces méthodes devrait provoquer l'écriture.

Et c'est exactement ce que nous faisons : nous récupérons le tableau d'octets dans lequel l'utilisateur a écrit et stockons une copie en tant quevaleurdans la carte des fichiers avec une clé appropriée.

Nous utilisons le code suivant pour tester notre système de fichiers (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);
   }
}
    

Lors du test, nous vérifions les actions suivantes :

  1. Nous créons un nouveau fichier.
  2. Nous vérifions que le fichier créé est vide.
  3. Nous écrivons des données dans le fichier.
  4. Nous relisons les données et vérifions qu'elles correspondent à ce que nous avons écrit.
  5. Nous supprimons le fichier.
  6. Nous vérifions que le fichier a été supprimé.

L'exécution de ce code nous donne cette sortie :

Fichier créé avec succès
Contenu du fichier :
données écrites dans le fichier
Contenu du fichier : CodeGym
Le fichier existe : faux

Pourquoi cet exemple était-il nécessaire ?

En termes simples, les données sont toujours un ensemble d'octets. Si vous avez besoin de lire/écrire beaucoup de données depuis/vers le disque, votre code s'exécutera lentement en raison de problèmes d'E/S. Dans ce cas, il est logique de conserver un système de fichiers virtuel dans la RAM, en travaillant avec lui de la même manière que vous le feriez avec un disque traditionnel. Et quoi de plus simple que InputStream et OutputStream ?

Bien sûr, il s'agit d'un exemple d'instruction, pas de code prêt pour la production. Il ne tient PAS compte (la liste suivante n'est pas exhaustive):

  • multithreading
  • limites de taille de fichier (la quantité de RAM disponible pour une JVM en cours d'exécution)
  • vérification de la structure du chemin
  • vérification des arguments de la méthode

Informations privilégiées intéressantes :
le serveur de vérification des tâches CodeGym utilise une approche quelque peu similaire. Nous faisons tourner un FS virtuel, déterminons quels tests doivent être exécutés pour la vérification des tâches, exécutons les tests et lisons les résultats.

Conclusion et la grande question

La grande question après avoir lu cette leçon est "Pourquoi ne puis-je pas simplement utiliser byte[] , puisque c'est plus pratique et n'impose pas de restrictions?"

L'avantage de ByteArrayInputStream est qu'il indique fortement que vous allez utiliser des octets en lecture seule (car le flux ne fournit pas d'interface pour modifier son contenu). Cela dit, il est important de noter qu'un programmeur peut toujours accéder directement aux octets.

Mais si parfois vous avez un byte[] , parfois vous avez un fichier, parfois vous avez une connexion réseau, etc., vous aurez besoin d'une sorte d'abstraction pour "un flux d'octets, et je me fiche de savoir où ils viens de". Et c'est ce qu'est InputStream . Lorsque la source se trouve être un tableau d'octets, ByteArrayInputStream est un bon InputStream à utiliser.

Ceci est utile dans de nombreuses situations, mais voici deux exemples spécifiques :

  1. Vous écrivez une bibliothèque qui reçoit des octets et les traite d'une manière ou d'une autre (par exemple, supposons qu'il s'agisse d'une bibliothèque d'utilitaires de traitement d'image). Les utilisateurs de votre bibliothèque peuvent fournir des octets à partir d'un fichier, d'un byte[] en mémoire ou d'une autre source. Vous fournissez donc une interface qui accepte un InputStream , ce qui signifie que s'ils ont un byte[] , ils doivent l'envelopper dans un ByteArrayInputStream .

  2. Vous écrivez du code qui lit une connexion réseau. Mais pour effectuer des tests unitaires sur ce code, vous ne souhaitez pas ouvrir de connexion, vous souhaitez simplement alimenter le code en quelques octets. Ainsi, le code prend un InputStream et votre test passe dans un ByteArrayInputStream .