天天看點

【Java基礎】吃透Java IO:位元組流、字元流、緩沖流

Java IO流

  • 前言
  • 1 初識Java IO
    • 1.1 IO流分類
    • 1.2 案例實操
  • 2 IO流對象
    • 2.1 File類
    • 2.2 位元組流
    • 2.3 字元流
    • 2.4 序列化
  • 3 IO流方法
    • 3.1 位元組流方法
    • 3.2 字元流方法
  • 4 附加内容
    • 4.1 位、位元組、字元
    • 4.2 IO流效率對比
    • 4.3 NIO

有人曾問fastjson的作者(阿裡技術專家高鐵):“你開發fastjson,沒得到什麼好處,反而挨了罵背了鍋,這種事情你為什麼要做呢?”

高鐵答道:“因為熱愛本身,就是獎勵啊!”

這個回答頓時觸動了我。想想自己,又何嘗不是如此。寫作是個痛苦的過程,用心寫作就更加煎熬,需字字斟酌,反複删改才有所成。然而,當一篇篇精良文章出自己手而呈現眼前時,那些痛苦煎熬就都那麼值得。如果這些博文能有幸得大家閱讀和認可,就更加是莫大的鼓舞了。技術人的快樂就是可以這麼純粹和簡單。

點波關注不迷路,一鍵三連好運連連!

IO流是Java中的一個重要構成部分,也是我們經常打交道的。這篇關于Java IO的博文幹貨滿滿,堪稱全網前三(請輕噴!)

下面幾個問題(問題還會繼續補充),如果你能對答如流,那麼恭喜你,IO知識掌握得很好,可以立即關閉文章。反之,你可以在後面得文章中尋找答案。

  1. Java IO流有什麼特點?
  2. Java IO流分為幾種類型?
  3. 位元組流和字元流的關系與差別?
  4. 字元流是否使用了緩沖?
  5. 緩沖流的效率一定高嗎?為什麼?
  6. 緩沖流展現了Java中的哪種設計模式思想?
  7. 為什麼要實作序列化?如何實作序列化?
  8. 序列化資料後,再次修改類檔案,讀取資料會出問題,如何解決呢?

IO,即in和out,也就是輸入和輸出,指應用程式和外部裝置之間的資料傳遞,常見的外部裝置包括檔案、管道、網絡連接配接。

Java 中是通過流處理IO 的,那麼什麼是流?

流(Stream),是一個抽象的概念,是指一連串的資料(字元或位元組),是以先進先出的方式發送資訊的通道。

當程式需要讀取資料的時候,就會開啟一個通向資料源的流,這個資料源可以是檔案,記憶體,或是網絡連接配接。類似的,當程式需要寫入資料的時候,就會開啟一個通向目的地的流。這時候你就可以想象資料好像在這其中“流”動一樣。

一般來說關于流的特性有下面幾點:

  1. 先進先出:最先寫入輸出流的資料最先被輸入流讀取到。
  2. 順序存取:可以一個接一個地往流中寫入一串位元組,讀出時也将按寫入順序讀取一串位元組,不能随機通路中間的資料。(RandomAccessFile除外)
  3. 隻讀或隻寫:每個流隻能是輸入流或輸出流的一種,不能同時具備兩個功能,輸入流隻能進行讀操作,對輸出流隻能進行寫操作。在一個資料傳輸通道中,如果既要寫入資料,又要讀取資料,則要分别提供兩個流。

IO流主要的分類方式有以下3種:

  1. 按資料流的方向:輸入流、輸出流
  2. 按處理資料機關:位元組流、字元流
  3. 按功能:節點流、處理流
【Java基礎】吃透Java IO:位元組流、字元流、緩沖流

1、輸入流與輸出流

輸入與輸出是相對于應用程式而言的,比如檔案讀寫,讀取檔案是輸入流,寫檔案是輸出流,這點很容易搞反。

【Java基礎】吃透Java IO:位元組流、字元流、緩沖流

2、位元組流與字元流

位元組流和字元流的用法幾乎完成全一樣,差別在于位元組流和字元流所操作的資料單元不同,位元組流操作的單元是資料單元是8位的位元組,字元流操作的是資料單元為16位的字元。

為什麼要有字元流?

Java中字元是采用Unicode标準,Unicode 編碼中,一個英文為一個位元組,一個中文為兩個位元組。

【Java基礎】吃透Java IO:位元組流、字元流、緩沖流

而在UTF-8編碼中,一個中文字元是3個位元組。例如下面圖中,“雲深不知處”5個中文對應的是15個位元組:-28-70-111-26-73-79-28-72-115-25-97-91-27-92-124

【Java基礎】吃透Java IO:位元組流、字元流、緩沖流

那麼問題來了,如果使用位元組流進行中文,如果一次讀寫一個字元對應的位元組數就不會有問題,一旦将一個字元對應的位元組分裂開來,就會出現亂碼了。為了更友善地進行中文這些字元,Java就推出了字元流。

位元組流和字元流的其他差別:

  1. 位元組流一般用來處理圖像、視訊、音頻、PPT、Word等類型的檔案。字元流一般用于處理純文字類型的檔案,如TXT檔案等,但不能處理圖像視訊等非文本檔案。用一句話說就是:位元組流可以處理一切檔案,而字元流隻能處理純文字檔案。
  2. 位元組流本身沒有緩沖區,緩沖位元組流相對于位元組流,效率提升非常高。而字元流本身就帶有緩沖區,緩沖字元流相對于字元流效率提升就不是那麼大了。詳見文末效率對比。

以寫檔案為例,我們檢視字元流的源碼,發現确實有利用到緩沖區:

【Java基礎】吃透Java IO:位元組流、字元流、緩沖流
【Java基礎】吃透Java IO:位元組流、字元流、緩沖流

3、節點流和處理流

節點流:直接操作資料讀寫的流類,比如FileInputStream

處理流:對一個已存在的流的連結和封裝,通過對資料進行處理為程式提供功能強大、靈活的讀寫功能,例如BufferedInputStream(緩沖位元組流)

處理流和節點流應用了Java的裝飾者設計模式。

下圖就很形象地描繪了節點流和處理流,處理流是對節點流的封裝,最終的資料處理還是由節點流完成的。

【Java基礎】吃透Java IO:位元組流、字元流、緩沖流

在諸多處理流中,有一個非常重要,那就是緩沖流。

我們知道,程式與磁盤的互動相對于記憶體運算是很慢的,容易成為程式的性能瓶頸。減少程式與磁盤的互動,是提升程式效率一種有效手段。緩沖流,就應用這種思路:普通流每次讀寫一個位元組,而緩沖流在記憶體中設定一個緩存區,緩沖區先存儲足夠的待操作資料後,再與記憶體或磁盤進行互動。這樣,在總資料量不變的情況下,通過提高每次互動的資料量,減少了互動次數。

【Java基礎】吃透Java IO:位元組流、字元流、緩沖流

聯想一下生活中的例子,我們搬磚的時候,一塊一塊地往車上裝肯定是很低效的。我們可以使用一個小推車,先把磚裝到小推車上,再把這小推車推到車前,把磚裝到車上。這個例子中,小推車可以視為緩沖區,小推車的存在,減少了我們裝車次數,進而提高了效率。

【Java基礎】吃透Java IO:位元組流、字元流、緩沖流

需要注意的是,緩沖流效率一定高嗎?不一定,某些情形下,緩沖流效率反而更低,具體請見IO流效率對比。

完整的IO分類圖如下:

【Java基礎】吃透Java IO:位元組流、字元流、緩沖流

接下來,我們看看如何使用Java IO。

文本讀寫的例子,也就是文章開頭所說的,将“松下問童子,言師采藥去。隻在此山中,雲深不知處。”寫入本地文本,然後再從檔案讀取内容并輸出到控制台。

1、FileInputStream、FileOutputStream(位元組流)

位元組流的方式效率較低,不建議使用
public class IOTest {
	public static void main(String[] args) throws IOException {
		File file = new File("D:/test.txt");

		write(file);
		System.out.println(read(file));
	}

	public static void write(File file) throws IOException {
		OutputStream os = new FileOutputStream(file, true);

		// 要寫入的字元串
		String string = "松下問童子,言師采藥去。隻在此山中,雲深不知處。";
		// 寫入檔案
		os.write(string.getBytes());
		// 關閉流
		os.close();
	}

	public static String read(File file) throws IOException {
		InputStream in = new FileInputStream(file);

		// 一次性取多少個位元組
		byte[] bytes = new byte[1024];
		// 用來接收讀取的位元組數組
		StringBuilder sb = new StringBuilder();
		// 讀取到的位元組數組長度,為-1時表示沒有資料
		int length = 0;
		// 循環取資料
		while ((length = in.read(bytes)) != -1) {
			// 将讀取的内容轉換成字元串
			sb.append(new String(bytes, 0, length));
		}
		// 關閉流
		in.close();

		return sb.toString();
	}
}
123456789101112131415161718192021222324252627282930313233343536373839複制代碼      

2、BufferedInputStream、BufferedOutputStream(緩沖位元組流)

緩沖位元組流是為高效率而設計的,真正的讀寫操作還是靠FileOutputStream和FileInputStream,是以其構造方法入參是這兩個類的對象也就不奇怪了。
public class IOTest {

	public static void write(File file) throws IOException {
		// 緩沖位元組流,提高了效率
		BufferedOutputStream bis = new BufferedOutputStream(new FileOutputStream(file, true));

		// 要寫入的字元串
		String string = "松下問童子,言師采藥去。隻在此山中,雲深不知處。";
		// 寫入檔案
		bis.write(string.getBytes());
		// 關閉流
		bis.close();
	}

	public static String read(File file) throws IOException {
		BufferedInputStream fis = new BufferedInputStream(new FileInputStream(file));

		// 一次性取多少個位元組
		byte[] bytes = new byte[1024];
		// 用來接收讀取的位元組數組
		StringBuilder sb = new StringBuilder();
		// 讀取到的位元組數組長度,為-1時表示沒有資料
		int length = 0;
		// 循環取資料
		while ((length = fis.read(bytes)) != -1) {
			// 将讀取的内容轉換成字元串
			sb.append(new String(bytes, 0, length));
		}
		// 關閉流
		fis.close();

		return sb.toString();
	}
}
12345678910111213141516171819202122232425262728293031323334複制代碼      

3、InputStreamReader、OutputStreamWriter(字元流)

字元流适用于文本檔案的讀寫,OutputStreamWriter類其實也是借助FileOutputStream類實作的,故其構造方法是FileOutputStream的對象
public class IOTest {
	
	public static void write(File file) throws IOException {
		// OutputStreamWriter可以顯示指定字元集,否則使用預設字元集
		OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(file, true), "UTF-8");

		// 要寫入的字元串
		String string = "松下問童子,言師采藥去。隻在此山中,雲深不知處。";
		osw.write(string);
		osw.close();
	}

	public static String read(File file) throws IOException {
		InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8");
		// 字元數組:一次讀取多少個字元
		char[] chars = new char[1024];
		// 每次讀取的字元數組先append到StringBuilder中
		StringBuilder sb = new StringBuilder();
		// 讀取到的字元數組長度,為-1時表示沒有資料
		int length;
		// 循環取資料
		while ((length = isr.read(chars)) != -1) {
			// 将讀取的内容轉換成字元串
			sb.append(chars, 0, length);
		}
		// 關閉流
		isr.close();

		return sb.toString()
	}
}
12345678910111213141516171819202122232425262728293031複制代碼      

4、字元流便捷類

Java提供了FileWriter和FileReader台灣字元流的讀寫,new FileWriter等同于new OutputStreamWriter(new FileOutputStream(file, true))
public class IOTest {
	
	public static void write(File file) throws IOException {
		FileWriter fw = new FileWriter(file, true);

		// 要寫入的字元串
		String string = "松下問童子,言師采藥去。隻在此山中,雲深不知處。";
		fw.write(string);
		fw.close();
	}

	public static String read(File file) throws IOException {
		FileReader fr = new FileReader(file);
		// 一次性取多少個位元組
		char[] chars = new char[1024];
		// 用來接收讀取的位元組數組
		StringBuilder sb = new StringBuilder();
		// 讀取到的位元組數組長度,為-1時表示沒有資料
		int length;
		// 循環取資料
		while ((length = fr.read(chars)) != -1) {
			// 将讀取的内容轉換成字元串
			sb.append(chars, 0, length);
		}
		// 關閉流
		fr.close();

		return sb.toString();
	}
}
123456789101112131415161718192021222324252627282930複制代碼      

5、BufferedReader、BufferedWriter(字元緩沖流)

public class IOTest {
	
	public static void write(File file) throws IOException {
		// BufferedWriter fw = new BufferedWriter(new OutputStreamWriter(new
		// FileOutputStream(file, true), "UTF-8"));
		// FileWriter可以大幅度簡化代碼
		BufferedWriter bw = new BufferedWriter(new FileWriter(file, true));

		// 要寫入的字元串
		String string = "松下問童子,言師采藥去。隻在此山中,雲深不知處。";
		bw.write(string);
		bw.close();
	}

	public static String read(File file) throws IOException {
		BufferedReader br = new BufferedReader(new FileReader(file));
		// 用來接收讀取的位元組數組
		StringBuilder sb = new StringBuilder();

		// 按行讀資料
		String line;
		// 循環取資料
		while ((line = br.readLine()) != null) {
			// 将讀取的内容轉換成字元串
			sb.append(line);
		}
		// 關閉流
		br.close();

		return sb.toString();
	}
}
1234567891011121314151617181920212223242526272829303132複制代碼      

第一節中,我們大緻了解了IO,并完成了幾個案例,但對IO還缺乏更詳細的認知,那麼接下來我們就對Java IO細細分解,梳理出完整的知識體系來。

Java種提供了40多個類,我們隻需要詳細了解一下其中比較重要的就可以滿足日常應用了。

File類是用來操作檔案的類,但它不能操作檔案中的資料。

public class File extends Object implements Serializable, Comparable<File>
1複制代碼      

File類實作了Serializable、 Comparable<File>,說明它是支援序列化和排序的。

File類的構造方法

方法名

說明

File(File parent, String child)

根據 parent 抽象路徑名和 child 路徑名字元串建立一個新 File 執行個體。

File(String pathname)

通過将給定路徑名字元串轉換為抽象路徑名來建立一個新 File 執行個體。

File(String parent, String child)

根據 parent 路徑名字元串和 child 路徑名字元串建立一個新 File 執行個體。

File(URI uri)

通過将給定的 file: URI 轉換為一個抽象路徑名來建立一個新的 File 執行個體。

File類的常用方法

方法

createNewFile()

當且僅當不存在具有此抽象路徑名指定名稱的檔案時,不可分地建立一個新的空檔案。

delete()

删除此抽象路徑名表示的檔案或目錄。

exists()

測試此抽象路徑名表示的檔案或目錄是否存在。

getAbsoluteFile()

傳回此抽象路徑名的絕對路徑名形式。

getAbsolutePath()

傳回此抽象路徑名的絕對路徑名字元串。

length()

傳回由此抽象路徑名表示的檔案的長度。

mkdir()

建立此抽象路徑名指定的目錄。

File類使用執行個體

public class FileTest {
	public static void main(String[] args) throws IOException {
		File file = new File("C:/Mu/fileTest.txt");

		// 判斷檔案是否存在
		if (!file.exists()) {
			// 不存在則建立
			file.createNewFile();
		}
		System.out.println("檔案的絕對路徑:" + file.getAbsolutePath());
		System.out.println("檔案的大小:" + file.length());

		// 刪除檔案
		file.delete();
	}
}
12345678910111213141516複制代碼      

InputStream與OutputStream是兩個抽象類,是位元組流的基類,所有具體的位元組流實作類都是分别繼承了這兩個類。

以InputStream為例,它繼承了Object,實作了Closeable

public abstract class InputStream
extends Object
implements Closeable
123複制代碼      

InputStream類有很多的實作子類,下面列舉了一些比較常用的:

【Java基礎】吃透Java IO:位元組流、字元流、緩沖流

詳細說明一下上圖中的類:

  1. InputStream:InputStream是所有位元組輸入流的抽象基類,前面說過抽象類不能被執行個體化,實際上是作為模闆而存在的,為所有實作類定義了處理輸入流的方法。
  2. FileInputSream:檔案輸入流,一個非常重要的位元組輸入流,用于對檔案進行讀取操作。
  3. PipedInputStream:管道位元組輸入流,能實作多線程間的管道通信。
  4. ByteArrayInputStream:位元組數組輸入流,從位元組數組(byte[])中進行以位元組為機關的讀取,也就是将資源檔案都以位元組的形式存入到該類中的位元組數組中去。
  5. FilterInputStream:裝飾者類,具體的裝飾者繼承該類,這些類都是處理類,作用是對節點類進行封裝,實作一些特殊功能。
  6. DataInputStream:資料輸入流,它是用來裝飾其它輸入流,作用是“允許應用程式以與機器無關方式從底層輸入流中讀取基本 Java 資料類型”。
  7. BufferedInputStream:緩沖流,對節點流進行裝飾,内部會有一個緩存區,用來存放位元組,每次都是将緩存區存滿然後發送,而不是一個位元組或兩個位元組這樣發送,效率更高。
  8. ObjectInputStream:對象輸入流,用來提供對基本資料或對象的持久存儲。通俗點說,也就是能直接傳輸對象,通常應用在反序列化中。它也是一種處理流,構造器的入參是一個InputStream的執行個體對象。

OutputStream類繼承關系圖:

【Java基礎】吃透Java IO:位元組流、字元流、緩沖流

OutputStream類繼承關系與InputStream類似,需要注意的是PrintStream.

與位元組流類似,字元流也有兩個抽象基類,分别是Reader和Writer。其他的字元流實作類都是繼承了這兩個類。

以Reader為例,它的主要實作子類如下圖:

【Java基礎】吃透Java IO:位元組流、字元流、緩沖流

各個類的詳細說明:

  1. InputStreamReader:從位元組流到字元流的橋梁(InputStreamReader構造器入參是FileInputStream的執行個體對象),它讀取位元組并使用指定的字元集将其解碼為字元。它使用的字元集可以通過名稱指定,也可以顯式給定,或者可以接受平台的預設字元集。
  2. BufferedReader:從字元輸入流中讀取文本,設定一個緩沖區來提高效率。BufferedReader是對InputStreamReader的封裝,前者構造器的入參就是後者的一個執行個體對象。
  3. FileReader:用于讀取字元檔案的便利類,new FileReader(File file)等同于new InputStreamReader(new FileInputStream(file, true),"UTF-8"),但FileReader不能指定字元編碼和預設位元組緩沖區大小。
  4. PipedReader :管道字元輸入流。實作多線程間的管道通信。
  5. CharArrayReader:從Char數組中讀取資料的媒體流。
  6. StringReader :從String中讀取資料的媒體流。

Writer與Reader結構類似,方向相反,不再贅述。唯一有差別的是,Writer的子類PrintWriter。

待續…

位元組輸入流InputStream主要方法:

  • read() :從此輸入流中讀取一個資料位元組。
  • read(byte[] b) :從此輸入流中将最多 b.length 個位元組的資料讀入一個 byte 數組中。
  • read(byte[] b, int off, int len) :從此輸入流中将最多 len 個位元組的資料讀入一個 byte 數組中。
  • close():關閉此輸入流并釋放與該流關聯的所有系統資源。

位元組輸出流OutputStream主要方法:

  • write(byte[] b) :将 b.length 個位元組從指定 byte 數組寫入此檔案輸出流中。
  • write(byte[] b, int off, int len) :将指定 byte 數組中從偏移量 off 開始的 len 個位元組寫入此檔案輸出流。
  • write(int b) :将指定位元組寫入此檔案輸出流。
  • close() :關閉此輸入流并釋放與該流關聯的所有系統資源。

字元輸入流Reader主要方法:

  • read():讀取單個字元。
  • read(char[] cbuf) :将字元讀入數組。
  • read(char[] cbuf, int off, int len) : 将字元讀入數組的某一部分。
  • read(CharBuffer target) :試圖将字元讀入指定的字元緩沖區。
  • flush() :重新整理該流的緩沖。
  • close() :關閉此流,但要先重新整理它。

字元輸出流Writer主要方法:

  • write(char[] cbuf) :寫入字元數組。
  • write(char[] cbuf, int off, int len) :寫入字元數組的某一部分。
  • write(int c) :寫入單個字元。
  • write(String str) :寫入字元串。
  • write(String str, int off, int len) :寫入字元串的某一部分。

另外,字元緩沖流還有兩個獨特的方法:

  • BufferedWriter類newLine() :寫入一個行分隔符。這個方法會自動适配所在系統的行分隔符。
  • BufferedReader類readLine() :讀取一個文本行。

位元組(Byte)是計量機關,表示資料量多少,是計算機資訊技術用于計量存儲容量的一種計量機關,通常情況下一位元組等于八位。

字元(Character)計算機中使用的字母、數字、字和符号,比如’A’、‘B’、’$’、’&'等。

一般在英文狀态下一個字母或字元占用一個位元組,一個漢字用兩個位元組表示。

位元組與字元:

  • ASCII 碼中,一個英文字母(不分大小寫)為一個位元組,一個中文漢字為兩個位元組。
  • UTF-8 編碼中,一個英文字為一個位元組,一個中文為三個位元組。
  • Unicode 編碼中,一個英文為一個位元組,一個中文為兩個位元組。
  • 符号:英文标點為一個位元組,中文标點為兩個位元組。例如:英文句号 . 占1個位元組的大小,中文句号 。占2個位元組的大小。
  • UTF-16 編碼中,一個英文字母字元或一個漢字字元存儲都需要 2 個位元組(Unicode 擴充區的一些漢字存儲需要 4 個位元組)。
  • UTF-32 編碼中,世界上任何字元的存儲都需要 4 個位元組。

首先,對比下普通位元組流和緩沖位元組流的效率:

public class MyTest {
	public static void main(String[] args) throws IOException {
		File file = new File("C:/Mu/test.txt");
		StringBuilder sb = new StringBuilder();

		for (int i = 0; i < 3000000; i++) {
			sb.append("abcdefghigklmnopqrstuvwsyz");
		}
		byte[] bytes = sb.toString().getBytes();

		long start = System.currentTimeMillis();
		write(file, bytes);
		long end = System.currentTimeMillis();

		long start2 = System.currentTimeMillis();
		bufferedWrite(file, bytes);
		long end2 = System.currentTimeMillis();

		System.out.println("普通位元組流耗時:" + (end - start) + " ms");
		System.out.println("緩沖位元組流耗時:" + (end2 - start2) + " ms");

	}

	// 普通位元組流
	public static void write(File file, byte[] bytes) throws IOException {
		OutputStream os = new FileOutputStream(file);
		os.write(bytes);
		os.close();
	}

	// 緩沖位元組流
	public static void bufferedWrite(File file, byte[] bytes) throws IOException {
		BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(file));
		bo.write(bytes);
		bo.close();
	}
}
12345678910111213141516171819202122232425262728293031323334353637複制代碼      

運作結果:

普通位元組流耗時:250 ms
緩沖位元組流耗時:268 ms
12複制代碼      

這個結果讓我大跌眼鏡,不是說好緩沖流效率很高麼?要知道為什麼,隻能去源碼裡找答案了。翻看位元組緩沖流的write方法:

public synchronized void write(byte b[], int off, int len) throws IOException {
    if (len >= buf.length) {
        /* If the request length exceeds the size of the output buffer,
           flush the output buffer and then write the data directly.
           In this way buffered streams will cascade harmlessly. */
        flushBuffer();
        out.write(b, off, len);
        return;
    }
    if (len > buf.length - count) {
        flushBuffer();
    }
    System.arraycopy(b, off, buf, count, len);
    count += len;
}
123456789101112131415複制代碼      

注釋裡說得很明白:如果請求長度超過輸出緩沖區的大小,重新整理輸出緩沖區,然後直接寫入資料。這樣,緩沖流将無害地級聯。

但是,至于為什麼這麼設計,我沒有想明白,有哪位明白的大佬可以留言指點一下。

基于上面的情形,要想對比普通位元組流和緩沖位元組流的效率差距,就要避免直接讀寫較長的字元串,于是,設計了下面這個對比案例:用位元組流和緩沖位元組流分别複制檔案。

public class MyTest {
	public static void main(String[] args) throws IOException {
		File data = new File("C:/Mu/data.zip");
		File a = new File("C:/Mu/a.zip");
		File b = new File("C:/Mu/b.zip");

		StringBuilder sb = new StringBuilder();

		long start = System.currentTimeMillis();
		copy(data, a);
		long end = System.currentTimeMillis();

		long start2 = System.currentTimeMillis();
		bufferedCopy(data, b);
		long end2 = System.currentTimeMillis();

		System.out.println("普通位元組流耗時:" + (end - start) + " ms");
		System.out.println("緩沖位元組流耗時:" + (end2 - start2) + " ms");
	}

	// 普通位元組流
	public static void copy(File in, File out) throws IOException {
		// 封裝資料源
		InputStream is = new FileInputStream(in);
		// 封裝目的地
		OutputStream os = new FileOutputStream(out);
		
		int by = 0;
		while ((by = is.read()) != -1) {
			os.write(by);
		}
		is.close();
		os.close();
	}

	// 緩沖位元組流
	public static void bufferedCopy(File in, File out) throws IOException {
		// 封裝資料源
		BufferedInputStream bi = new BufferedInputStream(new FileInputStream(in));
		// 封裝目的地
		BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(out));
		
		int by = 0;
		while ((by = bi.read()) != -1) {
			bo.write(by);
		}
		bo.close();
		bi.close();
	}
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950複制代碼      
普通位元組流耗時:184867 ms
緩沖位元組流耗時:752 ms
12複制代碼      

這次,普通位元組流和緩沖位元組流的效率差異就很明顯了,達到了245倍。

public class IOTest {
	public static void main(String[] args) throws IOException {
		// 資料準備
		dataReady();

		File data = new File("C:/Mu/data.txt");
		File a = new File("C:/Mu/a.txt");
		File b = new File("C:/Mu/b.txt");
		File c = new File("C:/Mu/c.txt");

		long start = System.currentTimeMillis();
		copy(data, a);
		long end = System.currentTimeMillis();

		long start2 = System.currentTimeMillis();
		copyChars(data, b);
		long end2 = System.currentTimeMillis();

		long start3 = System.currentTimeMillis();
		bufferedCopy(data, c);
		long end3 = System.currentTimeMillis();

		System.out.println("普通位元組流1耗時:" + (end - start) + " ms,檔案大小:" + a.length() / 1024 + " kb");
		System.out.println("普通位元組流2耗時:" + (end2 - start2) + " ms,檔案大小:" + b.length() / 1024 + " kb");
		System.out.println("緩沖位元組流耗時:" + (end3 - start3) + " ms,檔案大小:" + c.length() / 1024 + " kb");
	}

	// 普通字元流不使用數組
	public static void copy(File in, File out) throws IOException {
		Reader reader = new FileReader(in);
		Writer writer = new FileWriter(out);

		int ch = 0;
		while ((ch = reader.read()) != -1) {
			writer.write((char) ch);
		}
		reader.close();
		writer.close();
	}

	// 普通字元流使用字元流
	public static void copyChars(File in, File out) throws IOException {
		Reader reader = new FileReader(in);
		Writer writer = new FileWriter(out);

		char[] chs = new char[1024];
		while ((reader.read(chs)) != -1) {
			writer.write(chs);
		}
		reader.close();
		writer.close();
	}

	// 緩沖字元流
	public static void bufferedCopy(File in, File out) throws IOException {
		BufferedReader br = new BufferedReader(new FileReader(in));
		BufferedWriter bw = new BufferedWriter(new FileWriter(out));

		String line = null;
		while ((line = br.readLine()) != null) {
			bw.write(line);
			bw.newLine();
			bw.flush();
		}

		// 釋放資源
		bw.close();
		br.close();
	}

	// 資料準備
	public static void dataReady() throws IOException {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < 600000; i++) {
			sb.append("abcdefghijklmnopqrstuvwxyz");
		}
		OutputStream os = new FileOutputStream(new File("C:/Mu/data.txt"));
		os.write(sb.toString().getBytes());

		os.close();
		System.out.println("完畢");
	}
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283複制代碼      
普通字元流1耗時:1337 ms,檔案大小:15234 kb
普通字元流2耗時:82 ms,檔案大小:15235 kb
緩沖字元流耗時:205 ms,檔案大小:15234 kb
123複制代碼