Clasa ByteArrayOutputStream implementează un flux de ieșire care scrie date într-o matrice de octeți. Buffer-ul crește automat pe măsură ce datele sunt scrise în el.

Clasa ByteArrayOutputStream creează un buffer în memorie, iar toate datele trimise către flux sunt stocate în buffer.

Constructori ByteArrayOutputStream

Clasa ByteArrayOutputStream are următorii constructori :

Constructor
ByteArrayOutputStream() Acest constructor creează un buffer în memorie care are o lungime de 32 de octeți.
ByteArrayOutputStream(int a) Acest constructor creează un buffer în memorie cu o anumită dimensiune.

Și așa arată clasa în interior:


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

Metode ale clasei ByteArrayOutputStream

Să vorbim despre metodele pe care le putem folosi în clasa noastră.

Să încercăm să punem ceva în fluxul nostru. Pentru a face acest lucru, vom folosi metoda write() - poate accepta un octet sau un set de octeți pentru scriere.

Metodă
void write(int b) Scrie un octet.
void write (byte b[], int off, int len) Scrie o matrice de octeți de o anumită dimensiune.
void writeBytes(octetul b[]) Scrie o matrice de octeți.
void writeTo(OutputStream out) Scrie toate datele din fluxul de ieșire curent în fluxul de ieșire transmis.

Implementarea metodei:


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

Rezultatul este un nou fișier output.txt care arată astfel:

Metoda toByteArray() returnează conținutul curent al acestui flux de ieșire ca o matrice de octeți. Și puteți folosi metoda toString() pentru a obține matricea de octeți buf ca 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]);
    }
}
    

Buffer-ul nostru conține matricea de octeți pe care i-am transmis-o.

Metoda reset() resetează numărul de octeți validi din fluxul de ieșire al matricei de octeți la zero (deci tot ce se acumulează în ieșire este resetat).


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

Când ne afișăm tamponul după apelarea metodei reset() , nu obținem nimic.

Caracteristici specifice ale metodei close().

Această metodă merită o atenție specială. Pentru a înțelege ce face, să aruncăm o privire în interior:


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

Rețineți că metoda close() a clasei ByteArrayOutputStream nu face nimic.

De ce este asta? Un ByteArrayOutputStream este un flux bazat pe memorie (adică este gestionat și populat de utilizator în cod), așa că apelarea close() nu are niciun efect.

Practică

Acum să încercăm să implementăm un sistem de fișiere folosind ByteArrayOutputStream și ByteArrayInputStream .

Să scriem o clasă FileSystem folosind modelul de design singleton și să folosim un HashMap<String, byte[]> static , unde:

  • String este calea către un fișier
  • byte[] sunt datele din fișierul salvat

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

În această clasă, am creat următoarele metode publice:

  • metode standard CRUD (creare, citire, actualizare, ștergere),
  • o metodă de a verifica dacă un fișier există,
  • o metodă pentru a obține o instanță a sistemului de fișiere.

Pentru a citi dintr-un fișier, returnăm un InputStream . Sub capotă se află implementarea ByteArrayInputStream . Buffer-ul este o matrice de octeți stocați în harta fișierelor .

O altă metodă interesantă este newOutputStream . Când această metodă este apelată, returnăm un nou obiect ByteArrayOutputStream care suprascrie două metode: flush și close . Apelarea oricăreia dintre aceste metode ar trebui să determine scrierea.

Și exact asta facem: obținem matricea de octeți în care a scris utilizatorul și stocăm o copie cavaloareîn harta fișierelor cu o cheie adecvată.

Folosim următorul cod pentru a testa sistemul nostru de fișiere (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);
   }
}
    

În timpul testului, verificăm următoarele acțiuni:

  1. Creăm un nou fișier.
  2. Verificăm dacă fișierul creat este gol.
  3. Scriem câteva date în fișier.
  4. Citim înapoi datele și verificăm că se potrivesc cu ceea ce am scris.
  5. Ștergem fișierul.
  6. Verificăm că fișierul a fost șters.

Rularea acestui cod ne oferă această ieșire:

Fișier creat cu succes
Conținutul fișierului:
Datele scrise în fișier
Conținutul fișierului:
Fișierul CodeGym există: fals

De ce a fost necesar acest exemplu?

Mai simplu, datele sunt întotdeauna un set de octeți. Dacă trebuie să citiți/scrieți o mulțime de date de pe/pe disc, codul dumneavoastră va rula lent din cauza problemelor I/O. În acest caz, este logic să mențineți un sistem de fișiere virtual în RAM, lucrând cu el în același mod în care ați face-o cu un disc tradițional. Și ce ar putea fi mai simplu decât InputStream și OutputStream ?

Desigur, acesta este un exemplu pentru instrucțiuni, nu cod gata de producție. NU ține cont de (următoarea listă nu este completă):

  • multithreading
  • limitele de dimensiune a fișierului (cantitatea de RAM disponibilă pentru un JVM care rulează)
  • verificarea structurii traseului
  • verificarea argumentelor metodei

Informații privilegiate interesante:
serverul de verificare a sarcinilor CodeGym folosește o abordare oarecum similară. Începem un FS virtual, determinăm ce teste trebuie să fie rulate pentru verificarea sarcinilor, rulăm testele și citim rezultatele.

Concluzia și marea întrebare

Marea întrebare după citirea acestei lecții este „De ce nu pot folosi byte[] , deoarece este mai convenabil și nu impune restricții?”

Avantajul ByteArrayInputStream este că indică puternic că veți folosi octeți doar pentru citire (deoarece fluxul nu oferă o interfață pentru modificarea conținutului). Acestea fiind spuse, este important de reținut că un programator poate accesa în continuare octeții direct.

Dar dacă uneori aveți un octet[] , uneori aveți un fișier, alteori aveți o conexiune la rețea și așa mai departe, veți avea nevoie de un fel de abstractizare pentru „un flux de octeți și nu-mi pasă unde sunt vine din". Și asta este InputStream . Când sursa se întâmplă să fie o matrice de octeți, ByteArrayInputStream este un InputStream bun de utilizat.

Acest lucru este util în multe situații, dar iată două exemple specifice:

  1. Scrieți o bibliotecă care primește octeți și îi procesează cumva (de exemplu, să presupunem că este o bibliotecă de utilitare de procesare a imaginilor). Utilizatorii bibliotecii dvs. pot furniza octeți dintr-un fișier, dintr-un octet din memorie [] sau dintr-o altă sursă. Deci oferiți o interfață care acceptă un InputStream , ceea ce înseamnă că, dacă au un octet[] , trebuie să-l împacheteze într-un ByteArrayInputStream .

  2. Scrieți un cod care citește o conexiune de rețea. Dar pentru a efectua teste unitare pe acest cod, nu doriți să deschideți o conexiune - doriți doar să introduceți câțiva octeți codului. Deci, codul ia un InputStream și testul dvs. trece într-un ByteArrayInputStream .