[Java IO - Từ tổng quan tới chi tiết] Bài 03: Các lớp dẫn xuất của InputStream: FileInputStream

Chào các bạn! Chúng ta lại gặp nhau trong series Java IO - Từ tổng quan tới chi tiết. Trong bài này, chúng ta sẽ đi tìm hiểu về cả FileInputStreamFileOutputStream luôn nhé!

Để giúp các bạn dễ hình dung 2 lớp này nằm ở đâu trong cây phân cấp, mình có đánh dấu ở hình dưới đây. (Hơi mờ chút, các bạn thông cảm nhé)

Ngoài ra, bạn có thể tìm hiểu thêm tại Document của Oracle tại FileInputStreamFileOutputStream

1. FileInputStream

Lớp java.io.FileInputStream được dùng với mục đích thu nhận các byte đầu vào từ một file. Lớp java.io.FileInputStream có 3 hàm khởi tạo, các bạn có thể xem chi tiết tại đây

FileInputStream(File file)
FileInputStream(FileDescriptor fdObj)
FileInputStream(String name)

Để hiểu xem cách đọc file, các bạn xem qua ví dụ sau:

import java.io.*;

/**
 * Created by nhs3108 on 05/07/2017.
 */
public class FileInputStreamExample {
    public static void main(String[] args) {
        String absoluteFilePath = "/home/nhs3108/Desktop/test.txt";
        try {
            String content = FileInputStreamExample.getContentFile(absoluteFilePath);
            System.out.println("-------------------------------------------------");
            System.out.println(content);
            System.out.println("-------------------------------------------------");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static String getContentFile(String absoluteFilePath) throws IOException {
        StringBuilder result = new StringBuilder();
        FileInputStream fileInputStream = new FileInputStream(absoluteFilePath);
        int b;
        while ((b = fileInputStream.read()) != -1) {
            result.append((char) b);
        }
        fileInputStream.close();
        return result.toString();
    }
}

GIẢI THÍCH CHÚT NHÉ: Khi khởi tạo fileInputStream, bạn có thể hình dung ra có 1 con trỏ đang trỏ tới byte đầu tiên của dãy bytes của file. Hàm read() được gọi sẽ làm 2 việc

  • Một là di chuyển con trỏ tới byte tiếp theo trong dãy bytes
  • Hai là thực hiện trả về byte code tại vị trí con trỏ đang trỏ tới

Quá trình diễn ra có thể được hình dung như sau


Khi khởi tạo đối tượng fileInputStream tạo kết nối tới file cần thao tác đọc. Với * thể hiện cho một con trỏ đánh dấu vị trí byte mà nó đang trỏ tới. Ta có thể hình dung các bytes (mình ví dụ 3 bytes thôi nhé) được sắp xếp như sau.

HEAD byte-1 byte-2 byte-3 TAIL
*

Khi gọi hàm read lần đầu tiên, con trỏ di chuyển tới byte-1 và trả về byte code của byte-1

HEAD byte-1 byte-2 byte-3 TAIL
*

Tiếp theo, là vị trí byte-2

HEAD byte-1 byte-2 byte-3 TAIL
*

Tiếp theo, là vị trí byte-3

HEAD byte-1 byte-2 byte-3 TAIL
*

Tại thời điểm này, ta gọi hàm read(), nó sẽ trả về giá trị -1, đồng nghĩa với việc nó đã di chuyển tới cuối file. Lúc này ta sẽ dừng việc đọc byte cho dãy bytes của file lại.

HEAD byte-1 byte-2 byte-3 TAIL
*

Bằng lý thuyết đó, ta sử dụng vòng lặp để đọc từng byte trong dãy bytes cho đến khi nào đọc tới cuối file, tức là tới khi read() trả về giá trị -1 Lệnh (char) b được thực hiện để ký tự tuơng ứng của byte code đó trong bảng mã ASCII

VÍ DỤ

Với file /home/nhs3108/Desktop/test.txt có nội dung sau

Name : Nguyen Hong Son Gender : Male Marial Status : Single

Và kết quả nhận được tương ứng là

-------------------------------------------------
Name          : Nguyen Hong Son
Gender        : Male
Marial Status : Single
-------------------------------------------------

Tuy nhiên, việc đọc từng byte như ví dụ trên sẽ rất chậm. Bạn sẽ thấy rõ ràng độ chậm của nó khi bạn đọc 1 file có dữ liệu kha khá (vài MB). Chính vì thế, InputStream cung cấp thên phương thức read(byte[] bytes, int offset, int length) để hỗ trợ cho việc đọc file, với

  • mảng bytes là bộ nhớ đệm được sử dụng để lưu lại các bytes đọc được
  • Giá trịoffset là vị trí đầu tiên của mảng bytes được ghi dữ liệu.
  • Giá trịlength là số bytes tối đa được đọc được từ file rồi ghi lên mảng bytes. Giá trị của length làm sao nằm trong khoảng [0, bytes.length - offset]. Ví dụ bộ nhớ đệm có size = 4, offet = 2, thì bạn chỉ có thể set giá trị của length trong khoảng [0-2].

Mình lấy ví dụ, mảng bytes của mình có size là 4 bytes = new bytes[4]. offset = 1, length = 2. Khi đọc được 2 bytes b1b2 từ file và thực hiện ghi lên bộ nhớ đệm, mảng bytes sẽ có dạng

  • Số bytes đối đa được ghi lên bộ nhớ đệm có bằng giá trị của length hay không phụ thuộc vào số bytes còn lại từ vị trí con trỏ tới cuối file. Số bytes đọc được = remainingBytes.length > length ? length : remainingBytes.length.
  • Giá trị trả về là tổng số bytes đọc được vào bộ nhớ đệm. Trả về -1 nếu không còn dữ liệu để đọc do đã đọc tới cuối file.

Phương thức read(byte[] bytes) thực hiện lệnh gọi phương thức read(byte[] bytes, int offset, int length) với offset = 0 và length = bytes.length

Giờ thì hãy tự mình trải nghiệm và so sánh tốc độ giữa việc đọc từng byte với sử dụng buffer (size tùy bạn chọn) nhé.

Câu hỏi đặt ra là: Khi nào nên đọc từng byte? Khi nào sử dụng bộ nhớ đệm? Nếu sử dụng bộ nhớ đệm thì size của bộ nhớ đệm bao nhiêu là hợp lý?. Các bạn hãy tự mình tìm câu trả lời nhé. Keyword mà bạn có thể sử dụng là How do I decide how many bytes to read from an inputstream?

2. FileOutputStream

Cũng giống như lớp FileInputStream, lớp FileOutputStream được sử dụng để làm việc với file. Khác là ở chỗ, trong khi lớp FileInputStream được sử dụng để thu nhận các dữ liệu thì FileOutputStream được sử dụng để ghi dữ liệu lên file.

Lớp FileOutputStream cung cấp 5 hàm khởi tạo, chi tiết tại đây

FileOutputStream(File file)
FileOutputStream(File file, boolean append)
FileOutputStream(FileDescriptor fdObj)
FileOutputStream(String name)
FileOutputStream(String name, boolean append)

CHÚ Ý

  • Một chú ý nho nhỏ ở đây, giá trị boolean của append. Làtrue thì các bytes sẽ được ghi vào cuối của file (hiểu nôm na là dán thêm vào file). Làfalse thì ngược lại, khi thực hiện ghi, dữ liệu tồn tại trước đó của file sẽ bị xóa sạch để ghi mới. false cũng là giá trị mặc định của append
  • Lớp có 3 phuơng thức ghi bao gồm: --write(byte[] bytes): Ghi toàn bộ các bytes, tuơng đuơng với việc bạn gọi write(byte[] bytes, int offset, int length) với offset = 0 và length = bytes.length --write(byte[] bytes, int offset, int length): Ghi length các bytes nằm trong mảng bytes, bắt đầu từ vị trí offset (của mảng bytes). --write(int byte) : Ghi một byte duy nhất.

Hãy cùng xem qua ví dụ nho nhỏ dưới đây của mình để xem cách sử dụng FileOutputStream để ghi dữ liệu ra file nhé.

import java.io.FileOutputStream;
import java.io.IOException;

/**
 * Created by nhs3108 on 11/07/2017.
 */
public class FileOutputStreamExample {
    public static void main(String[] args) {
        String absoluteFilePath = "/home/nhs3108/Desktop/test_output.txt";
        String content = "Welcome to Nguyen Hong Son's tutorials!";
        boolean append = false;

        try {
            System.out.println("----------------Writing...---------------");
            FileOutputStreamExample.writeContent(absoluteFilePath, content, append);
            System.out.println("-------------------DONE!-------------------");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void writeContent(String absoluteFilePath, String content, boolean append) throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream(absoluteFilePath, append);
        fileOutputStream.write(content.getBytes());
        fileOutputStream.close();
    }
}