Java開發筆記(八十六)通過緩沖區讀寫文件


前面介紹了利用文件寫入器和文件讀取器來讀寫文件,因為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開發筆記(序)章節目錄


免責聲明!

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



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