文件流的基本類有四種:
- 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類提供了很多對於文件或目錄的操作。
- 獲取文件的信息。文件名稱,路徑,文件大小,修改時間等等。
- 文件的創建和刪除,目錄的創建
- 文件設置權限(讀,寫,執行)
- ...
二、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這兩個字節流上。