前面介紹了利用文件寫入器和文件讀取器來讀寫文件,由於FileWriter與FileReader讀寫的數據以字符爲單位,因此這種讀寫文件的方式被稱做「字符流I/O」,其中字母I表明輸入Input,字母O表明輸出Output。但是FileWriter的讀操做並不高效,原因在於FileWriter每次調用write方法都會直接寫入文件,假如某項業務須要屢次調用write方法,那麼程序就會寫入文件一樣次數。由於寫文件本質是寫磁盤,磁盤的速度遠不如內存,因此頻繁地寫文件必然嚴重下降程序的運行效率。爲此Java又設計了緩存寫入器BufferedWriter,它的write方法並不直接寫入文件,而是先寫入一塊緩存,等到緩存寫滿了再將緩存上的數據寫入文件。因爲緩存空間位於內存之中,寫入緩存等同訪問內存,這樣至關於把寫磁盤動做替換成寫內存動做,所以BufferedWriter的總體寫文件性能要大大優於FileWriter。除此以外,BufferedWriter還新增了下列幾個方法:
newLine:往文件末尾添加換行標記(Window系統是回車加換行)。固然其實是先往緩存添加換行標記,並不是直接往磁盤寫入換行標記。
flush:當即將緩衝區中的數據寫入磁盤。默認狀況要等緩衝區滿了纔會寫入磁盤,或者調用close方法關閉文件之時也會寫入磁盤,可是有時程序猴急,必定要當即寫入磁盤,此時就需調用flush方法強行寫磁盤。
使用緩存寫入器以前要先建立文件讀取器對象,並得到父類Writer的實例,而後再據此建立緩存寫入器對象。下面是經過緩存寫入器把多行字符串寫入文件的代碼例子:html
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>流對象,以後即可按照流式處理來加工該字符串流。
若想使用緩存讀取器,依然要先建立文件讀取器,再根據其父類的讀取器實例建立緩存讀取器。下面是經過緩存讀取器從文件中讀取多行字符串的代碼例子:java
// 使用緩存字符流讀取文件 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開發筆記(序)章節目錄》編碼