IO流(1)--文件流及其原理


文件流的基本類有四種:

  • FileInputStream/FileOutputStream
  • FileReader/FileWriter

一、File對象

文件流是一種節點流,它溝通程序與文件之間的數據傳輸。在Java中,文件被抽象為File。

我們通過File的構造器創建File對象,最常用的是通過文件路徑字符串進行創建。

public class Main{
    public static void main(String[] args){
        // 將一個已經存在的,或者不存在的文件或者目錄封裝成file對象
        File f = new File("/home/ubuntu/test/a.txt");
        File dir = new File("/home/ubuntu/test");
    }
}        

File類提供了很多對於文件或目錄的操作。

  1. 獲取文件的信息。文件名稱,路徑,文件大小,修改時間等等。
  2. 文件的創建和刪除,目錄的創建
  3. 文件設置權限(讀,寫,執行)
  4. ...

二、FileInputStream/FileOutputStream

FileInputStream和FileOutputStream是作用於文件的字節流。其實例連接了程序內存與文件對象,在構造流對象的時候需要指定文件對象。

// FileInputStream.java
public class FileInputStream extends InputStream{
    // 傳入文件名作為參數
    public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
    }

    // 傳入文件作為參數
    public FileInputStream(File file) throws FileNotFoundException {
        String name = (file != null ? file.getPath() : null);
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkRead(name);
        }
        if (name == null) {
            // 文件對象為空指針
            throw new NullPointerException();
        }
        // 文件路徑無效
        if (file.isInvalid()) {
            throw new FileNotFoundException("Invalid file path");
        }
        // 文件描述符
        fd = new FileDescriptor();
        fd.attach(this);
        // 設置path
        path = name;
        // 打開文件
        open(name);
    }
    // 傳入文件描述符
    public FileInputStream(FileDescriptor fdObj) {
        SecurityManager security = System.getSecurityManager();
        if (fdObj == null) {
            throw new NullPointerException();
        }
        if (security != null) {
            security.checkRead(fdObj);
        }
        fd = fdObj;
        path = null;

        /*
         * FileDescriptor is being shared by streams.
         * Register this stream with FileDescriptor tracker.
         */
        fd.attach(this);
    }

}

從源碼中我們可以看到,FileInputStream在構造的時候需要傳入一個文件對象,同時你可能還注意到,在構造器中我們還實例化了一個FileDescriptor,甚至在重載的構造器里有直接傳入一個FileDescriptor對象,這個對象有什么作用暫且不說,我們接着看一下FileInputStream的讀數據操作。

//FileInputStream.java
// 打開文件
private void open(String name) throws FileNotFoundException {
    open0(name);
}
private native void open0(String name) throws FileNotFoundException;

// 讀字節
public int read() throws IOException {
    return read0();
}
private native int read0() throws IOException;

// 讀字節數組
public int read(byte b[]) throws IOException {
    return readBytes(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
    return readBytes(b, off, len);
}
private native int readBytes(byte b[], int off, int len) throws IOException;

// 忽略部分字節
public long skip(long n) throws IOException {
    return skip0(n);
}
private native long skip0(long n) throws IOException;

可以看到,讀數據等操作都由本地方法實現。我們可以分析一下,FileInputStream在文件與程序之間建立了連接,實現了文件的讀字節操作。在構造FileInputStream的實例對象時候,我們傳入了文件,那么具體的操作怎么去執行呢,FileDescriptor就派上了用場。

根據定義,FileDescriptor也稱作文件描述符,內核通過文件描述符來訪問文件,文件描述符通常為非負整數的索引,它指向內核為每個進程所維護的該進程打開文件的記錄表。通俗來說,文件描述符就是文件的索引,有了這個索引,內核才能找到文件,也就才能把“流”連接起來,對於文件的操作也是基於這個索引展開的。

還有一點比較有意思,POSIX定義了3個符號常量:

  • 標准輸入的文件描述符 0
  • 標准輸出的文件描述符 1
  • 標准錯誤的文件描述符 2

而在FileDescriptor類中,也定義了三個常量in、out、err。根據注釋,這三個變量就是System.out、System.in、System.err所對應的三個文件描述符。

public static final FileDescriptor in = new FileDescriptor(0);
public static final FileDescriptor out = new FileDescriptor(1);
public static final FileDescriptor err = new FileDescriptor(2);

總的來說,為了構建基本流,我們需要:

  • 程序內存端。
  • 節點端,如文件對象。
  • 文件描述符對象,用於開放節點(如開放文件、開放套接字、或者某個字節的源/目的地)
  • 節點流對象,用於連接程序內存與文件對象,連接內存自不用說,連接文件對象則是通過文件描述符來完成。

FIleOutputStream與FileInputStream也是類似的,只是將讀操作變為寫操作。

三、FileReader/FileWriter

FileReader/FileWriter是作用於文件的字符流。它們分別繼承自轉換流InputStreamReader/OutputStreamWriter。在構造流對象時同樣需要傳入文件對象。此處就以FileWriter為例。

FileWriter繼承自OutputStreamWriter,只是定義了幾個構造器。在構造器中,FileWriter調用了父類構造器,並傳入FileOutputStream對象作為參數。由此可見FileWriter的寫文件操作底層還是通過FileOutputStream完成。

public class FileWriter extends OutputStreamWriter {

    // 傳入文件名
    public FileWriter(String fileName) throws IOException {
        super(new FileOutputStream(fileName));
    }

    // 傳入文件名,並指定追加模式
    public FileWriter(String fileName, boolean append) throws IOException {
        super(new FileOutputStream(fileName, append));
    }

    // 傳入文件對象
    public FileWriter(File file) throws IOException {
        super(new FileOutputStream(file));
    }

    // 傳入文件對象,指定是否追加
    public FileWriter(File file, boolean append) throws IOException {
        super(new FileOutputStream(file, append));
    }

    // 傳入文件描述符
    public FileWriter(FileDescriptor fd) {
        super(new FileOutputStream(fd));
    }

}

那么寫操作的功能都在OutputStreamWriter中實現

// OutputStreamWriter.java
// 在構造器中會初始化一個StreamEncoder的實例對象se,對文件的操作就是通過se的方法來完成。
// 構造se對象時將FileWriter傳入的FileOutputStream對象作為參數,因此我猜想寫文件的操作過程在se中,首先對字符進行編碼,然后調用FileOutputStream進行寫入操作。
// 寫字符 public void write(int c) throws IOException { se.write(c); } // 寫字符數組 public void write(char cbuf[], int off, int len) throws IOException { se.write(cbuf, off, len); } // 寫字符串 public void write(String str, int off, int len) throws IOException { se.write(str, off, len); } // 刷寫 public void flush() throws IOException { se.flush(); } // 關閉流 public void close() throws IOException { se.close(); }

FileReader與FileWriter的原理也是類似的。這里就不一一贅述了。

四、總結

文件的操作流主要就是這四個,我們可以通過源碼窺見出,FileInputStream/FileOutputStream是對文件進行字節的讀寫。FileReader/FileWriter是字符流,它們通過中間的編碼解碼器操作,將字符轉換成字節或者將字節轉換成字符,最終對文件的操作還是落在FileInputStream/FileOutputStream這兩個字節流上。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM