前面介紹了利用文件寫入器和文件讀取器來讀寫文件,因為FileWriter與FileReader讀寫的數據以字元為單位,所以這種讀寫文件的方式被稱作“字元流I/O”,其中字母I代表輸入Input,字母O代表輸出Output。可是FileWriter的讀操作並不高效,緣由在於FileWriter每次調用 ...
前面介紹了利用文件寫入器和文件讀取器來讀寫文件,因為FileWriter與FileReader讀寫的數據以字元為單位,所以這種讀寫文件的方式被稱作“字元流I/O”,其中字母I代表輸入Input,字母O代表輸出Output。可是FileWriter的讀操作並不高效,緣由在於FileWriter每次調用write方法都會直接寫入文件,假如某項業務需要多次調用write方法,那麼程式就會寫入文件同樣次數。因為寫文件本質是寫磁碟,磁碟的速度遠不如記憶體,所以頻繁地寫文件必然嚴重降低程式的運行效率。為此Java又設計了緩存寫入器BufferedWriter,它的write方法並不直接寫入文件,而是先寫入一塊緩存,等到緩存寫滿了再將緩存上的數據寫入文件。由於緩存空間位於記憶體之中,寫入緩存等同訪問記憶體,這樣相當於把寫磁碟動作替換成寫記憶體動作,因此BufferedWriter的整體寫文件性能要大大優於FileWriter。除此之外,BufferedWriter還新增了下列幾個方法:
newLine:往文件末尾添加換行標記(Window系統是回車加換行)。當然實際上是先往緩存添加換行標記,並非直接往磁碟寫入換行標記。
flush:立即將緩衝區中的數據寫入磁碟。預設情況要等緩衝區滿了才會寫入磁碟,或者調用close方法關閉文件之時也會寫入磁碟,但是有時程式猴急,一定要立即寫入磁碟,此時就需調用flush方法強行寫磁碟。
使用緩存寫入器之前要先創建文件讀取器對象,並獲得父類Writer的實例,然後再據此創建緩存寫入器對象。下麵是通過緩存寫入器把多行字元串寫入文件的代碼例子:
private static String mSrcName = "D:/test/aad.txt"; // 使用緩存字元流寫入文件 private static void writeBuffer() { String str1 = "白日依山盡,黃河入海流。"; String str2 = "欲窮千里目,更上一層樓。"; File file = new File(mSrcName); // 創建一個指定路徑的文件對象 // try(...)允許在圓括弧內部擁有多個資源創建語句,語句之間以冒號分隔 // 先創建文件寫入器,再根據文件讀取器創建緩存寫入器 try (Writer writer = new FileWriter(file); BufferedWriter bwriter = new BufferedWriter(writer);) { // FileWriter的每次write調用都會直接寫入磁碟,不但效率低,性能也差。 // BufferedWriter的每次write調用會先寫入緩衝區,直到緩衝區滿了才寫入磁碟, // 緩衝區大小預設是8K,查看源碼defaultCharBufferSize = 8192; // 資源釋放的close方法再把緩衝區的剩餘數據寫入磁碟, // 或者中途調用flush方法也可提前將緩衝區的數據寫入磁碟。 bwriter.write(str1); // 往文件寫入字元串 bwriter.newLine(); // 另起一行,也就是在文件末尾添加換行標記(Window系統是回車加換行) bwriter.write(str2); // 往文件寫入字元串 //bwriter.flush(); // 把緩衝區中的數據寫入磁碟 } catch (Exception e) { e.printStackTrace(); } }
既然文件寫入器有對應的緩存寫入器,那麼文件讀取器也有對應的緩存讀取器BufferedReader。BufferedReader的實現原理與它的兄弟BufferedWriter類似,另外BufferedReader比起文件讀取器新增瞭如下方法:
readLine:從文件中讀取一行數據。
mark:在當前位置做個標記。
reset:重置文件指針,令其回到上次標記的位置。也就是回到上次mark方法標記的文件位置。
lines:讀取文件內容的所有行,返回的是Stream<String>流對象,之後便可按照流式處理來加工該字元串流。
若想使用緩存讀取器,依然要先創建文件讀取器,再根據其父類的讀取器實例創建緩存讀取器。下麵是通過緩存讀取器從文件中讀取多行字元串的代碼例子:
// 使用緩存字元流讀取文件 private static void readBuffer() { File file = new File(mSrcName); // 創建一個指定路徑的文件對象 // try(...)允許在圓括弧內部擁有多個資源創建語句,語句之間以冒號分隔 // 先創建文件讀取器,再根據文件讀取器創建緩存讀取器 try (Reader reader = new FileReader(file); BufferedReader breader = new BufferedReader(reader);) { breader.mark((int) file.length()); // 做個標記 for (int i=1; ; i++) { // FileReader只能一個字元一個字元地讀,或者一次性讀進字元數組。 // BufferedReader還支持一行一行地讀。 String line = breader.readLine(); // 從文件中讀出一行文字 if (line == null) { // 讀到了空指針,表示已經到了文件末尾 break; } System.out.println("第"+i+"行的文字為:"+line); } breader.reset(); // 重置文件指針,令其回到上次標記的位置 for (int i=1; ; i++) { String line = breader.readLine(); // 從文件中讀出一行文字 if (line == null) { // 讀到了空指針,表示已經到了文件末尾 break; } System.out.println("又讀了一遍 第"+i+"行的文字為:"+line); } //breader.lines(); // 返回Stream<String>對象,之後可按照流式處理來加工該字元串流 } catch (Exception e) { e.printStackTrace(); } }
註意到以上代碼BufferedWriter和BufferedReader的創建語句都位於try後面的圓括弧之中,這是因為Writer與Reader兩大家族統統實現了AutoCloseable介面,所以由它們繁衍而來的所有子類都具備自動釋放資源的功能。另外,try語句支持同時管理多個資源類,只要它們的對象創建語句以冒號隔開,程式在運行時即可自動回收相關的資源。
結合運用讀操作和寫操作,可以實現文件複製的功能,無非是一邊從源文件中讀出數據,另一邊緊接著往目標文件寫入數據。採用緩存讀取器和緩存寫入器逐行複製的話,具體的文件複製代碼示例如下:
private static String mSrcName = "D:/test/aad.txt"; private static String mDestName = "D:/test/aad_copy.txt"; // 通過緩存字元流逐行複製文件 private static void copyFile() { File src = new File(mSrcName); // 創建一個指定路徑的源文件對象 File dest = new File(mDestName); // 創建一個指定路徑的目標文件對象 // try(...)允許在圓括弧內部擁有多個資源創建語句,語句之間以冒號分隔 // 分別創建源文件的緩存讀取器,以及目標文件的緩存寫入器 try (BufferedReader breader = new BufferedReader(new FileReader(src)); BufferedWriter bwriter = new BufferedWriter(new FileWriter(dest));) { for (int i=0; ; i++) { String line = breader.readLine(); // 從文件中讀出一行文字 if (line == null) { // 讀到了空指針,表示已經到了文件末尾 break; } if (i != 0) { // 第一行開頭不用換行 bwriter.newLine(); // 另起一行,也就是在文件末尾添加換行標記 } bwriter.write(line); // 往文件寫入字元串 } } catch (Exception e) { e.printStackTrace(); } System.out.println("文件複製完成,源文件大小="+src.length()+",新文件大小="+dest.length()); }
或者也可逐個字元來複制文件,此時BufferedReader每次調用的read方法只返回整型數表示一個字元,並且BufferedWriter每次調用的write方法也只寫入該字元對應的整型數。通過依次遍歷源文件的所有字元,同時往目標文件依次寫入這些字元,從而完成逐個字元複製文件的操作流程。下麵是採取逐字元複製文件的代碼例子:
// 通過緩存字元流逐個字元複製文件 private static void copyFileByInt() { File src = new File(mSrcName); // 創建一個指定路徑的源文件對象 File dest = new File(mDestName); // 創建一個指定路徑的目標文件對象 // try(...)允許在圓括弧內部擁有多個資源創建語句,語句之間以冒號分隔 // 分別創建源文件的緩存讀取器,以及目標文件的緩存寫入器 try (BufferedReader breader = new BufferedReader(new FileReader(src)); BufferedWriter bwriter = new BufferedWriter(new FileWriter(dest));) { while (true) { // 開始遍歷文件中的所有字元 int temp = breader.read(); // 從源文件中讀出一個字元 if (temp == -1) { // read方法返回-1表示已經讀到了文件末尾 break; } bwriter.write(temp); // 往目標文件寫入一個字元 } } catch (Exception e) { e.printStackTrace(); } System.out.println("文件複製完成,源文件大小="+src.length()+",新文件大小="+dest.length()); }
需要註意的是,使用字元流複製文件只有逐行複製和逐字元複製兩種方式,不可採取整個讀到字元數組再整個寫入字元數組的方式。之所以不能通過字元數組複製文件,是因為中文跟英文不一樣,一個漢字會占用多個位元組(GBK編碼的每個漢字占用兩個位元組,UTF8編碼的每個漢字占用三個位元組)。若要把文件內容讀到字元數組,勢必先得知曉該數組的長度,可是調用文件對象的length方法只能得到該文件的位元組長度,並非字元長度。譬如“白日依山盡”這個字元串在記憶體中的字元數組長度為5,寫到UTF8編碼的文件之後,文件大小是5*3=15位元組;接著想把文件內容讀到字元數組,然而15位元組的文件天曉得它有幾個字元,可能有5個UTF8編碼的中文字元,也可能有15個英文字元,也可能有5個GBK編碼的中文字元加5個英文字元共10個字元,總之你根本想不到該分配多大的字元數組。既然確定不了待讀取的字元數組長度,就無法一字不差地複製文件內容了。
更多Java技術文章參見《Java開發筆記(序)章節目錄》