ByteArrayOutputStream類實現將數據寫入字節數組的輸出流緩衝區會隨著數據寫入而自動增長。

ByteArrayOutputStream類在內存中創建一個緩衝區,所有發送到流數據都存儲在緩衝區中。

ByteArrayOutputStream 構造函數

ByteArrayOutputStream類具有以下構造函數

構造器
字節數組輸出流() 此構造函數創建一個 32 字節長的內存緩衝區。
字節數組輸出流(int a) 此構造函數創建一個具有特定大小的內存緩衝區。

這就是班級內部的樣子:


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

ByteArrayOutputStream 類的方法

讓我們談談我們可以在課堂上使用的方法。

讓我們試著在我們的流中放一些東西。為此,我們將使用write()方法——它可以接受一個字節或一組字節進行寫入。

方法
無效寫入(int b) 寫入一個字節。
void write(byte b[], int off, int len) 寫入特定大小的字節數組。
void writeBytes(字節 b[]) 寫入一個字節數組。
void writeTo(OutputStream 輸出) 將當前輸出流中的所有數據寫入傳遞的輸出流。

方法實現:


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

結果是一個新的output.txt文件,如下所示:

toByteArray ()方法將此輸出流的當前內容作為字節數組返回。您可以使用toString()方法將buf字節數組作為文本獲取:


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

我們的緩衝區包含我們傳遞給它的字節數組。

reset ()方法將字節數組輸出流中的有效字節數重置為零(因此輸出中累積的所有內容都將重置)。


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

當我們在調用reset()方法後顯示我們的緩衝區時,我們什麼也得不到。

close() 方法的具體特徵

這種方法值得特別注意。要了解它的作用,讓我們看一下內部結構:


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

請注意,ByteArrayOutputStream類的close()方法實際上不執行任何操作。

這是為什麼?ByteArrayOutputStream是基於內存的流(即,它由用戶在代碼中管理和填充),因此調用close()無效

實踐

現在讓我們嘗試使用我們的ByteArrayOutputStreamByteArrayInputStream來實現一個文件系統。

讓我們使用單例設計模式編寫一個FileSystem類,並使用靜態HashMap<String, byte[]>,其中:

  • 字符串是文件的路徑
  • byte[]是保存文件中的數據

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

在這個類中,我們創建了以下公共方法:

  • 標準的 CRUD 方法(創建、讀取、更新、刪除),
  • 一種檢查文件是否存在的方法,
  • 獲取文件系統實例的方法。

要從文件中讀取,我們返回一個InputStream。底層是ByteArrayInputStream實現。緩衝區是存儲在文件映射中的字節數組。

另一個有趣的方法是newOutputStream。調用此方法時,我們返回一個新的ByteArrayOutputStream對象,該對象覆蓋兩個方法:flushclose。調用這些方法中的任何一個都應該導致寫入發生。

這正是我們所做的:我們獲取用戶寫入的字節數組,並將副本存儲為價值文件映射中使用適當的鍵。

我們使用以下代碼來測試我們的文件系統(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);
   }
}
    

在測試期間,我們驗證以下操作:

  1. 我們創建一個新文件。
  2. 我們檢查創建的文件是否為空。
  3. 我們將一些數據寫入文件。
  4. 我們回讀數據並驗證它是否與我們寫入的內容相匹配。
  5. 我們刪除文件。
  6. 我們確認該文件已被刪除。

運行這段代碼給我們這個輸出:

文件創建成功
文件內容:
寫入文件的數據
文件內容:CodeGym
文件存在:false

為什麼這個例子是必要的?

簡而言之,數據始終是一組字節。如果您需要從磁盤讀取/寫入大量數據,由於 I/O 問題,您的代碼將運行緩慢。在這種情況下,在 RAM 中維護一個虛擬文件系統是有意義的,就像使用傳統磁盤一樣使用它。還有什麼比InputStreamOutputStream更簡單的呢?

當然,這是一個指令示例,而不是生產就緒代碼。它不考慮(以下列表不完整):

  • 多線程
  • 文件大小限制(運行中的 JVM 的可用 RAM 量)
  • 路徑結構的驗證
  • 方法參數的驗證

有趣的內幕信息:
CodeGym 任務驗證服務器使用了某種類似的方法。我們啟動一個虛擬 FS,確定需要運行哪些測試以進行任務驗證,運行測試並讀取結果。

結論和大問題

讀完這節課後最大的問題是“為什麼我不能只使用byte[],因為它更方便而且不施加限制?”

ByteArrayInputStream的優點是它強烈指示您將使用只讀字節(因為流不提供更改其內容的接口)。也就是說,重要的是要注意程序員仍然可以直接訪問字節。

但是如果有時你有一個byte[],有時你有一個文件,有時你有一個網絡連接,等等,你將需要某種抽象“字節流,我不在乎它們在哪裡來自”。這就是InputStream。當源恰好是字節數組時,ByteArrayInputStream是一個很好用的InputStream 。

這在許多情況下都很有用,但這裡有兩個具體示例:

  1. 您正在編寫一個接收字節並以某種方式處理它們的庫(例如,假設它是一個圖像處理實用程序庫)。您的庫的用戶可以提供來自文件、內存中byte[]或其他來源的字節。所以你提供了一個接受InputStream 的接口,這意味著如果他們有一個byte[],他們需要將它包裝在一個ByteArrayInputStream中。

  2. 您正在編寫讀取網絡連接的代碼。但是要對此代碼執行單元測試,您不想打開連接——您只想向代碼提供幾個字節。因此代碼採用InputStream並且您的測試通過ByteArrayInputStream