Lớp ByteArrayOutputStream triển khai luồng đầu ra ghi dữ liệu vào một mảng byte. Bộ đệm tự động phát triển khi dữ liệu được ghi vào nó.

Lớp ByteArrayOutputStream tạo bộ đệm trong bộ nhớ và tất cả dữ liệu được gửi tới luồng được lưu trữ trong bộ đệm.

Các hàm tạo ByteArrayOutputStream

Lớp ByteArrayOutputStream có các hàm tạo sau:

Người xây dựng
ByteArrayOutputStream() Hàm tạo này tạo một bộ đệm trong bộ nhớ dài 32 byte.
ByteArrayOutputStream(int a) Hàm tạo này tạo bộ đệm trong bộ nhớ với kích thước cụ thể.

Và đây là giao diện của lớp bên trong:


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

Các phương thức của lớp ByteArrayOutputStream

Hãy nói về các phương pháp chúng ta có thể sử dụng trong lớp của mình.

Hãy thử đưa một cái gì đó vào luồng của chúng tôi. Để làm điều này, chúng ta sẽ sử dụng phương thức write() — nó có thể chấp nhận một byte hoặc một tập hợp các byte để ghi.

Phương pháp
ghi vô hiệu (int b) Viết một byte.
void write(byte b[], int off, int len) Viết một mảng byte có kích thước cụ thể.
void writeBytes(byte b[]) Viết một mảng byte.
void writeTo(OutputStream ra) Ghi tất cả dữ liệu từ luồng đầu ra hiện tại sang luồng đầu ra đã qua.

Phương pháp thực hiện:


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

Kết quả là một tệp output.txt mới trông như thế này:

Phương thức toByteArray() trả về nội dung hiện tại của luồng đầu ra này dưới dạng một mảng byte. Và bạn có thể sử dụng phương thức toString() để lấy mảng byte buf dưới dạng văn bản:


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

Bộ đệm của chúng tôi chứa mảng byte mà chúng tôi đã chuyển cho nó.

Phương thức reset() đặt lại số byte hợp lệ trong luồng đầu ra của mảng byte về 0 (vì vậy mọi thứ được tích lũy trong đầu ra đều được đặt lại).


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

Khi chúng tôi hiển thị bộ đệm của mình sau khi gọi phương thức reset() , chúng tôi không nhận được gì.

Các tính năng cụ thể của phương thức close()

Phương pháp này đáng được quan tâm đặc biệt. Để hiểu những gì nó làm, chúng ta hãy xem qua bên trong:


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

Lưu ý rằng phương thức close() của lớp ByteArrayOutputStream không thực sự làm gì cả.

Tại sao vậy? ByteArrayOutputStream là một luồng dựa trên bộ nhớ (nghĩa là nó được quản lý và điền bởi người dùng trong mã), vì vậy việc gọi close () không có hiệu lực.

Luyện tập

Bây giờ, hãy thử triển khai một hệ thống tệp bằng cách sử dụng ByteArrayOutputStreamByteArrayInputStream của chúng tôi .

Hãy viết một lớp FileSystem bằng cách sử dụng mẫu thiết kế singleton và sử dụng HashMap<String, byte[]> tĩnh , trong đó:

  • Chuỗi là đường dẫn đến tệp
  • byte[] là dữ liệu trong tệp đã lưu

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

Trong lớp này, chúng tôi đã tạo các phương thức công khai sau:

  • các phương thức CRUD tiêu chuẩn (tạo, đọc, cập nhật, xóa),
  • một phương pháp để kiểm tra nếu một tập tin tồn tại,
  • một phương thức để lấy một thể hiện của hệ thống tệp.

Để đọc từ một tệp, chúng tôi trả về một InputStream . Dưới mui xe là triển khai ByteArrayInputStream . Bộ đệm là một mảng byte được lưu trữ trong bản đồ tệp .

Một phương thức thú vị khác là newOutputStream . Khi phương thức này được gọi, chúng tôi trả về một đối tượng ByteArrayOutputStream mới sẽ ghi đè hai phương thức: flushclose . Việc gọi một trong hai phương thức này sẽ khiến quá trình ghi diễn ra.

Và đó chính xác là những gì chúng tôi làm: chúng tôi lấy mảng byte mà người dùng đã ghi vào và lưu trữ một bản sao dưới dạnggiá trịtrong bản đồ tệp bằng một khóa thích hợp.

Chúng tôi sử dụng đoạn mã sau để kiểm tra hệ thống tệp của mình (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);
   }
}
    

Trong quá trình thử nghiệm, chúng tôi xác minh các hành động sau:

  1. Chúng tôi tạo một tệp mới.
  2. Chúng tôi kiểm tra xem tệp đã tạo có trống không.
  3. Chúng tôi ghi một số dữ liệu vào tập tin.
  4. Chúng tôi đọc lại dữ liệu và xác minh rằng nó khớp với những gì chúng tôi đã viết.
  5. Chúng tôi xóa tệp .
  6. Chúng tôi xác minh rằng tệp đã bị xóa.

Chạy mã này cho chúng ta đầu ra này:

Tệp được tạo thành công
Nội dung tệp:
Dữ liệu được ghi vào tệp
Nội dung tệp: CodeGym
Tệp tồn tại: sai

Tại sao ví dụ này là cần thiết?

Nói một cách đơn giản, dữ liệu luôn là một tập hợp các byte. Nếu bạn cần đọc/ghi nhiều dữ liệu từ/vào đĩa, mã của bạn sẽ chạy chậm do sự cố I/O. Trong trường hợp này, bạn nên duy trì một hệ thống tệp ảo trong RAM, làm việc với nó giống như cách bạn làm với đĩa truyền thống. Và điều gì có thể đơn giản hơn InputStreamOutputStream ?

Tất nhiên, đây là một ví dụ để hướng dẫn, không phải mã sẵn sàng sản xuất. Nó KHÔNG chiếm (danh sách sau đây không đầy đủ):

  • đa luồng
  • giới hạn kích thước tệp (số lượng RAM khả dụng cho một JVM đang chạy)
  • xác minh cấu trúc đường dẫn
  • xác minh các đối số phương thức

Thông tin nội bộ thú vị:
Máy chủ xác minh tác vụ CodeGym sử dụng cách tiếp cận tương tự. Chúng tôi tạo ra một FS ảo, xác định những thử nghiệm nào cần được chạy để xác minh tác vụ, chạy thử nghiệm và đọc kết quả.

Kết luận và câu hỏi lớn

Câu hỏi lớn sau khi đọc bài học này là "Tại sao tôi không thể sử dụng byte[] , vì nó thuận tiện hơn và không áp đặt các hạn chế?"

Ưu điểm của ByteArrayInputStream là nó mạnh mẽ cho biết rằng bạn sẽ sử dụng các byte chỉ đọc (vì luồng không cung cấp giao diện để thay đổi nội dung của nó). Điều đó nói rằng, điều quan trọng cần lưu ý là một lập trình viên vẫn có thể truy cập trực tiếp vào các byte.

Nhưng nếu đôi khi bạn có byte[] , đôi khi bạn có một tệp, đôi khi bạn có kết nối mạng, v.v., bạn sẽ cần một số loại trừu tượng cho "một luồng byte và tôi không quan tâm chúng ở đâu đến từ". Và đó chính là InputStream . Khi nguồn xảy ra là một mảng byte, thì ByteArrayInputStream là một InputStream tốt để sử dụng.

Điều này hữu ích trong nhiều tình huống, nhưng đây là hai ví dụ cụ thể:

  1. Bạn đang viết một thư viện nhận byte và xử lý chúng bằng cách nào đó (ví dụ: giả sử đó là thư viện tiện ích xử lý ảnh). Người dùng thư viện của bạn có thể cung cấp byte từ tệp, từ byte trong bộ nhớ[] hoặc từ một số nguồn khác. Vì vậy, bạn cung cấp một giao diện chấp nhận InputStream , có nghĩa là nếu họ có byte[] , thì họ cần bọc giao diện đó trong ByteArrayInputStream .

  2. Bạn đang viết mã đọc kết nối mạng. Nhưng để thực hiện kiểm tra đơn vị trên mã này, bạn không muốn mở kết nối — bạn chỉ muốn cung cấp một vài byte cho mã. Vì vậy, mã nhận một InputStream và bài kiểm tra của bạn vượt qua ByteArrayInputStream .