IO 流,掌控一切
上一篇文章我們認識了文件操作的源頭 File 類,這篇文章就來聊聊文件操作的核心 IO 流。
我們經??梢月牭剑狠斎肓?、輸出流、字節(jié)流、字符流、節(jié)點流、處理流等詞語,咋一聽,忍不住“哇~~~!”的一聲,心里在想:“感覺好復雜的樣子,學習 IO 流需要知道這么多東西啊!”,從而有了畏難的情緒。大家千萬不要被這些詞語嚇到,望而卻步,它們只不過是從三個維度對 IO 流的總結。
學習 IO 流是有套路的,通過這篇文章的學習,你一定能掌握 IO 流的使用技巧,從而掌控一切文件操作問題。
一、認識 IO 流
1、IO 流的分類
從 IO 流的流向來劃分,IO 流分為:輸入流、輸出流。
從 IO 流要處理的數(shù)據(jù)來劃分,IO 流分為:字節(jié)流、字符流。其中,字節(jié)流可以處理一切文件數(shù)據(jù),包括純文本,word文檔,pdf文檔,圖片,音頻和視頻等二進制數(shù)據(jù);字符流只能處理純文本文件。
從 IO 流的功能來劃分,IO 流分為:節(jié)點流和處理流。其中,節(jié)點流是用來包裝數(shù)據(jù)源(File)的,它直接和數(shù)據(jù)源連接,表示從一個節(jié)點讀取數(shù)據(jù)或者把數(shù)據(jù)寫入到一個節(jié)點;處理流是用來包裝節(jié)點流的,它是對一個已經存在的節(jié)點流進行連接,處理流通過增加緩存的方式來提高輸入輸出操作的性能。
總的來說,java.io 包中流的操作主要分為字節(jié)流和字符流兩類,他倆都有對應的節(jié)點流與數(shù)據(jù)源進行連接,為了提高文件操作的性能,在節(jié)點流的基礎上提供了處理流,以便增強節(jié)點流的功能,同時他倆都有輸入和輸出操作。
通過上面的分類,大家先對 IO 流先有一個初步的了解,后面結合代碼給大家進一步講解。
2、區(qū)分流的輸入與輸出
在程序中所有的數(shù)據(jù)都是以流的方式進行傳輸?shù)?,程序需要?shù)據(jù)的時候就用輸入流讀取數(shù)據(jù),當程序需要將計算好的數(shù)據(jù)進行保存到文件或者輸出到其他系統(tǒng)時,就用輸出流寫出數(shù)據(jù)。
簡單來說的話,就是以我們的程序為中心,如果是外部的數(shù)據(jù)流向程序,那么就是輸入流,輸入流一定是讀取操作;如果是程序里的數(shù)據(jù)流出到外部,那么就是輸出流,輸出流一定是寫出操作。

3、IO 操作的套路
Java 中 IO 操作也是有套路的,有標準的操作步驟,主要的操作步驟如下:
1、使用 File 類與文件建立聯(lián)系
2、選擇對應的輸入流或者輸出流
3、進行讀或寫操作
4、關閉資源
先對這個套路進行一個了解,后面結合代碼一下就明白了,原來套路如此簡單。
二、萬能鑰匙字節(jié)流
1、認識字節(jié)流
字節(jié)流主要操作 byte 類型數(shù)據(jù),說它是萬能鑰匙,是因為它可以處理一切文件,包括文本、word文檔、Excel文檔、pdf文檔、圖片、語音、視頻等,統(tǒng)統(tǒng)都可以處理。
字節(jié)流分為字節(jié)輸入流和字節(jié)輸出流,在 Java 中 字節(jié)輸入流用 InputStream 表示,字節(jié)輸出流用 OutputStream 表示。
字節(jié)輸入流:InputStream 是一個抽象類,必須依靠其子類 FileInputStream 來讀取文件內容,輸入到程序中。我們常用的方法是:
int read(byte b[]) //讀取byte數(shù)組中的內容,返回讀入的長度
close() //關閉資源
字節(jié)輸出流:OutputStream 是一個抽象類,必須依靠其子類 FileOutputStream 來讀取文件內容,輸入到程序中。我們常用的方法是:
//將一個制定范圍的byte數(shù)組輸出
void write(byte b[], int off, int len)
close() //關閉資源
flush() // 在關閉資源的時候默認會調用刷新方法
2、字節(jié)輸出流 FileOutputStream 的使用
我們來看一個例子,把“演示字節(jié)輸出流的使用\r\n用 FileOutputStream 類操作!”的文本輸出到 D:/file/txt/output.txt 文件中。
因為文件操作有可能發(fā)生 FileNotFoundException 和 IOException,為了精簡代碼,便于閱讀主要代碼,除了本例子以外,后續(xù)的例子我會直接使用 throws 關鍵字拋出異常,并且關閉資源也不放在finally里,這樣可以減少 try...catch...finally的代碼。
@Test
public void testOutput() {
// 1、建立聯(lián)系, File對象, 輸出文件的地址
// 如果文件不存在則可以創(chuàng)建文件并寫入,
// 但是如果加了文件夾,那么文件夾不存在則會產生FileNotFoundException,系統(tǒng)找不到指定的路徑
String path = "D:/txt/output.txt";
File file = new File(path);
// 2、選擇流
// 由于os要在finally中用到,放到try的外部,以提升os的變量作用范圍
OutputStream os = null;
try {
// 用FileOutputStream子類實例化父類OutputStream
// 以追加的方式輸出到文件,必須是true,否則就會覆蓋原有的文件
os = new FileOutputStream(file, true);
// 3、操作
String info = "演示字節(jié)輸出流的使用\r\n用 FileOutputStream 類操作!\r\n";
byte[] b = info.getBytes();// 字符串轉字節(jié)數(shù)組
os.write(b, 0, b.length);// 寫出
// 要養(yǎng)成這個習慣,為了避免緩存沒有寫出去,需要顯示地flush一下
os.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
System.out.println("文件不存在");
} catch (IOException e) {
e.printStackTrace();
System.out.println("文件寫出失敗");
} finally {
try {
// 4、釋放資源
if (os != null) {
os.close();
}
} catch (Exception e2) {
System.out.println("關閉文件輸出流資源失敗");
}
}
}
運行結果:

3、字節(jié)輸入流 FileInputStream 的使用
上面的例子我們學會了字節(jié)輸出流的使用,下面用字節(jié)輸入流 FileInputStream 來讀取上面的文件內容。
@Test
public void testInput() throws IOException {
// 1、建立聯(lián)系
File file = new File("D:/output.txt");
// 2、選擇流
InputStream is = new FileInputStream(file);
// 3、讀操作:即不斷地讀取
byte[] b = new byte[1024]; // 緩存數(shù)組
int len = 0; // 接收實際讀取的大小
while ((len = is.read(b)) != -1) {
// 能讀取到數(shù)據(jù)則輸出,字節(jié)數(shù)組轉成字符串
String info = new String(b, 0, len);
System.out.println(info);
}
is.close();
}
運行結果:
演示字節(jié)輸出流的使用
用 FileOutputStream 類操作!
演示字節(jié)輸出流的使用
用 FileOutputStream 類操作!
4、使用字節(jié)流,完成圖片文件的拷貝
下面的例子演示如何通過字節(jié)流對圖片文件進行拷貝操作,假設把 tomcat.png 拷貝成 tomcat1.jpg。
文件的拷貝操作的思路就是,用字節(jié)輸入流讀取圖片 tomcat.png 的內容,用字節(jié)輸出流寫出到 tomcat1.jpg 文件中,根據(jù)文件操作的套路,很容易就能寫出以下的代碼:
@Test
public void testCopy() throws IOException {
// 1、使用File類與文件建立聯(lián)系
File srcFile = new File("D:/file/image/tomcat.png");
File destFile = new File("D:/file/image/tomcat1.jpg");
// 2、選擇對應的輸入流或者輸出流
InputStream is = new FileInputStream(srcFile);
OutputStream os = new FileOutputStream(destFile);
// 3、進行讀或寫操作
byte[] b = new byte[1024];
int len = 0;
while ((len = is.read(b)) != -1) {
// 判斷每次讀取的內容長度,如果不等于-1,表示文件沒有讀完
// 選擇帶參數(shù)的write方法,就是為了避免byte緩存比實際內容多的時候,輸出多余的空內容
os.write(b, 0, len);
}
os.flush();
// 4、關閉資源,先創(chuàng)建的后關閉
os.close();
is.close();
}
運行結果:

三、純文本操作字符流
1、認識字符流
字符流主要操作純文本類型數(shù)據(jù),只能處理 txt、html 等文本類型的數(shù)據(jù),在程序中一個字符等于兩個字節(jié),Java 提供了 Reader 類和 Writer 類用于專門操作字符流。
字符流也分為字符輸入流和字符輸出流,在 Java 中 字符輸入流用 Reader 表示,輸出流用 Writer 表示。
字符輸入流:Reader 是一個抽象類,必須依靠其子類 FileReader 來讀取純文本文件內容,輸入到程序中。我們常用的方法是:
int read(char cbuf[]) //讀取char數(shù)組中的內容,返回讀入的長度
close() //關閉資源
字符輸出流:Writer 是一個抽象類,必須依靠其子類 FileWriter 來讀取純文本文件內容,輸入到程序中。我們常用的方法是:
//將一個字符串輸出
void write(String str)
//將一個字符數(shù)組輸出
void write(char cbuf[], int off, int len)
close() //關閉資源
flush() // 在關閉資源的時候默認會調用刷新方法
2、字符輸出流 FileWriter 的使用
我們來看一個例子,把“演示字符輸出流的使用\r\n用 FileWriter 類操作!”的文本輸出到 D:/file/txt/output_char.txt 文件中。
@Test
public void testWriter() throws IOException {
// 1、使用File類與文件建立聯(lián)系
File file = new File("D:/file/txt/output_char.txt");
// 2、選擇對應的輸入流或者輸出流
Writer writer = new FileWriter(file, true);
String info = "演示字符輸出流的使用\r\n用 FileWriter 類操作!\r\n";
// 3、進行寫操作
writer.write(info); //將一個字符串組輸出
writer.flush();
// 4、關閉資源
writer.close();
}
運行結果:

3、字符輸入流 FileReader 的使用
上面的例子我們學會了字符輸出流的使用,下面用字符輸入流 FileReader 來讀取上面的文件內容。
@Test
public void testReader() throws IOException {
// 1、使用File類與文件建立聯(lián)系
File file = new File("D:/file/txt/output_char.txt");
// 2、選擇對應的輸入流或者輸出流
Reader reader = new FileReader(file);
char[] cbuf = new char[1024];
int len = 0;
// 3、進行寫操作
while ((len = reader.read(cbuf)) != -1) {
String info = new String(cbuf, 0, len); // 字符數(shù)組轉成字符串
System.out.println(info);
}
// 4、關閉資源
reader.close();
}
運行結果:
演示字符輸出流的使用
用 FileWriter 類操作!
演示字符輸出流的使用
用 FileWriter 類操作!
4、利用字符流,完成 txt文本文件的拷貝
下面的例子演示如何通過字符流對圖片文件進行拷貝操作,把 output_char.txt 拷貝成 output_char1.txt。
@Test
public void testTxtCopy() throws IOException {
// 1、使用File類與文件建立聯(lián)系
File srcFile = new File("D:/file/txt/output_char.txt");
File destFile = new File("D:/file/txt/output_char1.txt");
// 2、選擇對應的輸入流或者輸出流
Reader read = new FileReader(srcFile);
Writer write = new FileWriter(destFile);
// 3、進行讀寫操作
char[] cbuf = new char[1024];
int len = 0;
while ((len = read.read(cbuf)) != -1) {
write.write(cbuf, 0, len); //將一個字符數(shù)組輸出
}
write.flush();
// 4、關閉資源
write.close();
read.close();
}
運行結果:

四、字節(jié)流與字符流的區(qū)別
1、字符輸出流在寫出文件時用到了緩存區(qū)
除去剛才講過的,字節(jié)流可以處理一切文件,字符流只能處理純文本文件,兩者還有一個明顯的差異,那就是字符輸出流在操作文件時使用了緩沖區(qū),通過緩沖區(qū)再寫出到文件,而字節(jié)輸出流直接操作文件。
1、通過源碼可以證明字符輸出流用到了緩存區(qū)

2、通過兩段代碼的輸出結果證明字符輸出流用到了緩存區(qū)
- 驗證字符流:
/**
* 把flush方法和close方法去掉,觀察程序運行結果,用字符流輸出內容到文件是空的
*/
@Test
public void testWriter1() throws IOException {
// 1、使用File類與文件建立聯(lián)系
File file = new File("D:/file/txt/output_char_buffer.txt");
// 2、選擇對應的輸入流或者輸出流
Writer writer = new FileWriter(file, true);
String info = "把flush方法和close方法去掉,觀察程序運行結果,輸出的內容文件是空的!\r\n";
// 3、進行寫操作
writer.write(info);
}
運行結果:

- 驗證字節(jié)流:
/**
* 把flush方法和close方法去掉,觀察程序運行結果,用字節(jié)流可以輸出內容到文件
*/
@Test
public void testOutput1() throws IOException {
// 1、使用File類與文件建立聯(lián)系
File file = new File("D:/file/txt/output_char_output.txt");
// 2、選擇對應的輸入流或者輸出流
OutputStream os = new FileOutputStream(file, true);
// 3、進行寫操作
String info = "把flush方法和close方法去掉,觀察程序運行結果,輸出的內容文件是空的!\r\n";
byte[] b = info.getBytes();// 字符串轉字節(jié)數(shù)組
os.write(b, 0, b.length);// 寫出
}
運行結果:

通過以上的 2 段程序,可以看出,字符流是有緩存的,如果我們沒有調用 flush 方法,并且沒有調用 close 方法,是無法把內容寫到文件中的。但是同樣的沒有調用 flush 方法和 close 方法,字節(jié)流確可以把內容寫出到文件。
- 驗證字符流調用 flush方法,不調用 close 方法的結果
/**
* 調用flush方法,不調用close方法,觀察程序運行結果,用字符流輸出內容到文件是可以的,說明字符輸出流確實用到了緩沖區(qū)
*/
@Test
public void testWriter2() throws IOException {
// 1、使用File類與文件建立聯(lián)系
File file = new File("D:/file/txt/output_char_writer.txt");
// 2、選擇對應的輸入流或者輸出流
Writer writer = new FileWriter(file);
String info = "調用flush方法,不調用close方法,觀察程序運行結果,用字符流輸出內容到文件是可以的,說明字符輸出流確實用到了緩沖區(qū)!\r\n";
// 3、進行寫操作
writer.write(info);
// 4、強制刷出
writer.flush();
}
運行結果:

- 驗證字符流調用 close 方法,不調用 flush 方法的結果
/**
* 調用close方法,不調用flush方法,觀察程序運行結果,用字符流輸出內容到文件是可以的,說明字符輸出流確實用到了緩沖區(qū)
*/
@Test
public void testWriter3() throws IOException {
// 1、使用File類與文件建立聯(lián)系
File file = new File("D:/file/txt/output_char_writer.txt");
// 2、選擇對應的輸入流或者輸出流
Writer writer = new FileWriter(file);
String info = "調用close方法,不調用flush方法,觀察程序運行結果,用字符流輸出內容到文件是可以的,說明字符輸出流確實用到了緩沖區(qū)!\r\n";
// 3、進行寫操作
writer.write(info);
// 4、關閉資源
writer.close();
}
運行結果:

通過以上的 2 段程序,可以看出,字符流是有緩存的,通過顯示調用 flush 方法可以把緩存內容輸出到文件,如果沒有調用 flush 方法,在調用 close 方法時,默認也是會把緩存內容輸出到文件。
切記字符輸出流在flush方法和close方法都沒有調用的時候,是無法輸出內容到文件的。為了避免出現(xiàn)此類問題,我們在使用輸出流的時候,不管是字節(jié)流還是字符流最好都顯示的調用一下 flush 方法。
講了這么多,大家覺得我們在操作文件的時候是用字節(jié)流好呢還是用字符流好呢,答案是使用字節(jié)流更好,因為所有的文件在磁盤中以及網絡傳輸都是以二進制的字節(jié)傳輸?shù)模?strong>在實際開發(fā)中,字節(jié)流用的比較廣泛。
我們再來明確一下,文件操作的套路只有4步:
1、使用File類與文件建立聯(lián)系
2、選擇對應的輸入流或者輸出流
3、進行讀或寫操作
4、關閉資源
另外讀寫操作也是有固定套路的:
byte[] b = new byte[1024];
int len = 0;
while ((len = is.read(b)) != -1) {
os.write(b, 0, len);
}