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