前言
現在很多項目的開發都會用到SpringBoot,而SpringBoot的自動配置的底層原理實現就在於,Java提供的IO流將META-INF/spring-autoconfigure-metadata.properties文件中的數據讀取出來,所以適當瞭解Java的IO流是十分有必要的。本文的介紹內容如下:
文章目錄
(若文章有不正之處,或難以理解的地方,請多多諒解,歡迎指正)
1. 流的概念和作用
在《Java編程思想》中,流被定義爲代表任何有能力產出數據的數據源對象或者有能力接受數據的接收端對象。
也就是說,流的本質其實就是數據傳輸,是計算機各部件之間的數據流動。
流的作用其實也就是爲數據源和目的建立一個傳輸管道。
2. IO的分類和特性
按照不同的分類方式,可以將把流分爲不同的類型。常用的分類有三種:
- 按流的流向劃分
- 按操作單元劃分
- 按流的角色劃分
2.1 按流的流向劃分,可以分爲輸入流和輸出流
輸入流:將數據從外設或外村(如鍵盤、鼠標、文件等)傳遞到應用程序的稱爲輸入流(Input Stream);
輸出流:將數據從應用程序傳遞到外設或外存(如屏幕、打印機、文件等)的流(OutputStream)。
可以看到,輸入流和輸出流都涉及到一個方向的問題。舉個栗子,如果數據從內存到硬盤,那麼傳輸過程中的數據流是輸入流還是輸出流?
答案是輸出流。也就是說這裏所說的輸入、輸出都是從程序運行所在的內存的角度來進行劃分。
再看個例子,數據從服務器(Server)傳送到客戶端(Client),在這個過程中服務器和客戶端的程序分別使用什麼流向的流?
其實,在這個過程中因爲服務器是從數據庫取出數據,數據通過內存經由指定程序發送到流上,所以服務器端的程序使用的是輸出流;而客戶端是從流上獲取數據,內存接受到數據之後交給CPU進行處理,所以客戶端的程序使用的是輸入流。
在Java中,輸入流主要是InputStream和Reader作爲基類,而輸出流則主要是OutputStream和Writer作爲基類。它們都是一些抽象基類,並不能直接用於創建對象實例。
2.2 按操作單元劃分,可以劃分爲字節流和字符流
在介紹字節流和字符流之前,我們需要知道字節和字符之間的關係:
- 1字符 = 2字節
- 1字節 = 8位
- 一個漢字佔兩個字節長度(因爲漢字博大精深,所以有些漢字也會佔到三個字節的長度)
字節流:處理字節數據。每次讀取(寫出)一個字節,當傳輸的資源文件中有中文時,就會出現亂碼;
字符流:處理字符數據。每次讀取(寫出)兩個字節時,有中文時使用該留就可以正確傳輸顯示文字。
在Java中,字節流主要是由InputStream和OutputStream作爲基類,而字符流則主要有Reader和Writer作爲基類。
2.3 按流的角色,可劃分爲節點流和處理流
節點流,就是可以從/向一個特定的IO設備(如磁盤、網絡)讀/寫數據的流。
可以從上圖看出,當使用節點流進行輸入和輸出數據過程中,程序直接連接到實際的數據源,和實際的輸入/輸出節點連接。節點流也被稱爲低級流。
處理流:對一個已存在的流進行連接和封裝,通過所封裝的流的功能調用實現數據讀寫。
當使用處理流進行輸入/輸出操作時,程序並不會直接連接到實際的數據源,沒有與實際的輸入和輸出節點連接。只要使用相同的處理流,程序就可以採用完全相同的輸入/輸出代碼來訪問不同的數據源,隨着處理流所包裝的節點流的變化,程序實際訪問的數據源也會相應地發生變化。
2.4 IO流的特性
Java的IO流涉及到了40多個類,但其實都是從如下4個抽象基類中派生出來的:
- InputStream/Reader:所有的輸入流的基類,前者是字節輸入流,後者是字符輸入流;
- OutputStream/Writer:所有的輸出流的基類,前者是字節輸出流,後者是字符輸出流。
我們知道,流的作用就像是一個數據管道,而數據就像是管道中的一滴滴水。字符流和字節流的處理單位不同,但處理方式相似。
輸入流使用隱式的記錄指針來表示當前正準備從哪個“水滴”開始讀取,每當程序從InputStream或Reader中取出“水滴”之後,記錄指針自己向後移動,除此之外,InputStream和Reader裏面都提供了一些方法來控制記錄指針的移動。
而對於輸出流OutputStream和Writer而言,它們同樣是把輸出設備抽象成一個“水管”,當執行輸出的時候,程序相當於把水管中的“水滴”依次放出。輸出流同樣採用隱式指針來表示當前輸出水滴的位置,每當程序從OutputStream或Writer中取出水滴的時候,指針也會自動向後移動。
以上展示了JavaIO的基本概念模型,但Java的處理流模型則給我們另一種輸入/輸出流設計的角度:
處理流可以在任何已存在的流的基礎之上,這就允許Java應用程序採用相同的代碼,透明的方式來訪問不同的輸入和輸出設備的數據流。而在處理流中主要是以增加緩衝的方式來提供輸入和輸出的效率,且可能提供了一系列便捷的方法來一次性輸入和輸出大批量的內容,而不是輸入/輸出一個或多個“水滴”。
在處理流中有一個專門提供了一個內存區域用於輸入和輸出大批量內容的流——緩衝流(Buffered Stream)。
如果每次操作都是以一個字節/字符爲單位,顯然這樣的數據傳輸效率很低。爲了提高數據傳輸效率,通常使用緩衝流,即爲一個流配有一個緩衝區(Buffer),這個緩衝區就是專門用於傳送數據的一塊內存。
當向一個緩衝流寫入數據時,系統將數據發送到緩衝區,而不是直接發送到外部設備。緩衝區自動記錄數據,當緩衝區滿時,系統將數據全部發送到相應的外部設備。而且當從一個緩衝流中讀取數據時,系統實際是從緩衝區中讀取數據。當緩衝區空時,系統就會從相關外部設備自動讀取數據,並讀取儘可能多的數據填滿緩衝區。由此可見,緩衝流提供了內存與外部設備之間的數據傳輸效率。
從上述我們其實可以窺見JavaIO的特性:
- 先進先出,最先寫入輸出流的數據最先被輸入流讀取;
- 順序存取,數據的獲取和發送是沿着數據序列順序進行,不能隨機訪問中間的數據。(但RandomAccessFile可以從文件的任意位置進行存取(輸入輸出)操作);
- 只讀或只寫,每個流只能是輸入流或輸出流的一種,不能同時具備兩個功能。
2.5 小結
可能看了上面的分類,會有讀者不清楚什麼時候使用字節流,什麼時候用輸出流。筆者在此整理這幾個步驟:
- 首先要知道自己選擇輸入流還是輸出流(請站在內存的角度考慮);
- 在考慮傳輸數據時,通過判斷是否有中文來決定是使用字節流傳輸還是字符流;
- 通過前面兩步就可以選出一個合適的節點流了,如字節輸入流InputStream,如果要在此基礎上增強功能,那麼在處理流中選擇一個合適的即可。
而在JavaIO中常用的流的類型有以下這些(粗體所標出的類代表節點流,必須直接與指定的物理節點關聯;斜體標出的類代表抽象類,不能直接創建實例):
分類 | 字節輸入流 | 字節輸出流 | 字符輸入流 | 字符輸出流 |
---|---|---|---|---|
抽象基類 | InputStream | OutputStream | Reader | Writer |
訪問文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
訪問數組 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
訪問管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
訪問字符串 | StringReader | StringWriter | ||
緩衝流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
轉換流 | InputStreamReader | OutputStreamWriter | ||
對象流 | ObjectInputStream | ObjectOutputStream | ||
抽象基類 | FilterInputStream | FileOutputStream | FilterReader | FilterWriter |
打印流 | PrintStream | PrintWriter | ||
推回輸入流 | PushbackInputStream | PushbackReader | ||
特殊流 | DataInputStream | DataOutputStream |
3. 常用的IO流的用法
3.1 IO體系的基類(InputStream/Reader、OutputStream/Writer)
因字節流和字符流的處理方式相似,只是操作的單位不同,所以就整理在一起進行介紹。
InputStream和Reader是所有輸入流的抽象基類,本身並不能創建實例來執行輸入,但是它們的方法是所有輸入流都可以使用的方法。
在InputStream裏包含如下三種方法:
方法 | 功能說明 |
---|---|
int read() | 從輸入流中讀取單個字節,返回所讀取的字節數據(字節數據可直接轉換爲int類型) |
int read(byte[] b) | 從輸入流中最多讀取b.length個字節的數據,並將其存儲入字節數組b中,返回實際讀取的字節數 |
int read(byte[] b, int off, int len) | 從輸入流中最多讀取len個字節的數據,並將其存儲入字節數組b中,放入數組b中時,從off位置開始進行讀取,返回實際讀取的字節數 |
在Reader中也包含了如下三個方法:
方法 | 功能說明 |
---|---|
int read() | 從輸入流中讀取單個字符 |
int read(char[] c) | 從輸入流中讀最多c.length個字符,存入字符數組c中,返回實際讀取的字符數 |
int read(char[] c, int off, int len) | 從輸入流中讀最多len個字符,存入字符數組c中從off開始的位置,返回實際讀取的字符數 |
而OutputStream和Writer是所有輸出流的抽象基類,本身並不能創建實例來執行輸出,但是它們的方法是所有輸出流都可以使用的方法。
在OutputStream和Writer裏包含了三種方法:
方法 | 功能說明 |
---|---|
void write(int c) | 將指定的字節/字符輸出到輸出流中,其中c即可以代表字節,也可以代表字符 |
void write(byte[]/char[] buf) | 將字節數組/字符數組中從off位置開始,長度爲len的字節/字符輸出到輸出流 |
void write(byte[]/char[] buf, int off, int len) | 將字節數組/字符數組中從off位置開始,長度爲len的字節/字符輸出到輸出流中 |
因爲字符流是直接以字符作爲操作單位,所以中Writer中可以使用字符串來代替字符數組,即String對象可作爲參數。在Writer中還包含如下兩個將String作爲操作對象的方法:
方法 | 功能說明 |
---|---|
void write(String str) | 將str字符串裏面包含的字符串輸出到指定輸出流中 |
void write(String str, int off, int len) | 將str字符串裏面從off位置開始,將長度爲len的字符輸出到指定的輸出流中 |
3.2 IO體系中基類文件流的使用(FileInputStream/FileReader、FileOutputStream/FileWriter)
上文所介紹的InputStream和Reader是抽象類,並不能用於創建實例,但它們分別有用於讀取文件的子類輸入流:FileInputStream和FileReader,它們都是節點流,會直接和指定文件關聯。舉個栗子:
public class FileTest {
public static void main(String[] args) throws IOException {
FileInputStream fis = null;
try{
//1. 創建字節輸入流
fis = new FileInputStream("D:\\output.txt");
//2. 創建一個長度爲1024的儲水池
byte[] b = new byte[1024];
//用於保存的實際字節數
int hasRead = 0;
//3. 使用循環來重複取水滴的過程
while((hasRead = fis.read(b))>0){
//取出儲水池中的水滴(字節),將字節數組轉換成字符串輸出
System.out.println(new String(b, 0, hasRead));
}
}catch (IOException e){
e.printStackTrace();
}finally {
fis.close();
}
}
}
因爲以上程序中打開的文件IO資源並不屬於內存的資源,垃圾回收機制無法回收該資源,所以需要使用fis.close()來顯示地關閉打開的IO資源。在JDK1.7時,IO資源類都被改寫成實現了AutoCloseable接口,因此都可以通過自動關閉資源的try語句來關閉這些IO流。下圖是FileInputStream類的繼承關係圖:
而使用FileReader讀取文件的處理方法與FileInputStream相似,只是操作單元不一樣:
public class FileTest {
public static void main(String[] args) throws IOException {
FileReader fr = null;
try {
//1. 創建字節輸入流
fr = new FileReader("D:\\output.txt");
//2. 創建一個長度爲1024的儲水池
char[] b = new char[1024];
//用於保存的實際字節數
int hasRead = 0;
//3. 使用循環來重複取水滴的過程
while ((hasRead = fr.read(b)) > 0) {
//取出儲水池中的水滴(字節),將字節數組轉換成字符串輸出
System.out.println(new String(b, 0, hasRead));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
fr.close();
}
}
}
FileOutputStream/FileWriter是IO中的文件輸出流,接下來我們來看個栗子:
public class FileTest {
public static void main(String[] args) throws IOException {
FileInputStream fis = null;
FileOutputStream fos = null;
try{
// 創建字節輸入流
fis = new FileInputStream("D:\\output.txt");
// 創建字節輸出流
fos = new FileOutputStream("D:\\newOutput.txt");
// 創建一個長度爲1024的儲水池
byte[] b = new byte[1024];
// 用於保存的實際字節數
int hasRead = 0;
// 使用循環來重複取水滴的過程
while((hasRead = fis.read(b))>0){
// 當輸入流FileInputStream讀取多少關於output.txt的內容
// FileOutputStream就寫入多少在newOutput.txt
fos.write(b, 0, hasRead);
}
}catch (IOException e){
e.printStackTrace();
}finally {
fis.close();
fos.close();
}
}
}
在運行程序之後,可以輸出流指定的目錄下看到多了一個newOutput.txt文件,該文件中的內容跟output.txt文件的內容完全相同。FileWriter的使用方式和FileOutputStream基本類似,在此就不做贅述了。
需要注意的是,使用JavaIO進行輸出操作的時候,一定要關閉字符輸出流。字節流沒有緩衝區,是直接輸出的,而字符流是具有緩衝區的。關閉輸出流除了可以保證流的物理資源被及時回收之外,可能還可以將輸出流緩衝區中的數據flush到物理節點中(因爲在執行close()方法之前,會自動執行輸出流的flush()方法)。
3.3 緩衝流的使用(BufferedInputStream/BufferedReader、BufferedOutputStream/BufferedWriter)
在上文有對處理流和緩衝流的原理進行介紹,接下來來介紹一下字節緩存流的用法(字符緩存流與字節緩存流的用法相似,在此就不做贅述了):
public class BufferedTest {
public static void main(String[] args) throws IOException {
FileInputStream fis = null;
FileOutputStream fos = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try{
// 創建字節輸入流
fis = new FileInputStream("D:\\output.txt");
// 創建字節輸出流
fos = new FileOutputStream("D:\\newOutput.txt");
// 創建字節緩存輸入流
bis = new BufferedInputStream(fis);
// 創建字節緩存輸出流
bos = new BufferedOutputStream(fos);
// 創建一個長度爲1024的儲水池
byte[] b = new byte[1024];
// 用於保存的實際字節數
int hasRead = 0;
// 使用循環來重複取水滴的過程
while((hasRead = bis.read(b))>0){
// 當輸入流FileInputStream讀取多少關於output.txt的內容
// FileOutputStream就寫入多少在newOutput.txt
bos.write(b, 0, hasRead);
}
}catch (IOException e){
e.printStackTrace();
}finally {
bis.close();
bos.close();
}
}
}
可以看到,使用字節緩存流讀取和寫入數據的方式和文件流(FileInputStream,FileOutputStream)並沒有什麼不同,但是因爲緩存流的加入,使得數據可以在緩存區存儲到一定的量的時候再寫入磁盤,提高了工作效率,還減少對硬盤的讀取次數,降低對磁盤的損耗。從這個栗子我們也可以看到,緩存流,也就是處理流,是一種可以增強流功能的流,可以對已經存在的流進行修飾。
在上面的代碼中,我們使用了緩存流和文件流,但我們只關閉了緩存流。因爲我們在使用處理流套接到節點流的時候,只需要關閉最上層的處理就可以了,Java會幫我們關閉下層的節點流。
對設計模式有所瞭解的讀者應該也發現了,這裏處理流的操作,按功能劃分流,並通過動態裝配這些流,以獲得需要的功能,不就是使用到了設計模式中的裝飾模式嘛!能想到這一步的讀者,可以適當獎勵一下自己哦~
3.4 轉換流的使用(InputStreamReader/OutputStreamWriter)
當文本文件在硬盤中以字節流的形式進行存儲時,通過InputStreamReader讀取後可轉爲字符流給程序處理,程序處理的字符流通過OutputStreamWriter轉換爲字節流保存。所以轉換流主要是在這兩種場景中使用:
- 當字節和字符之間有轉換動作時;
- 流操作的數據需要編碼或解碼時。
下面以獲取鍵盤輸入爲例,來介紹轉換流的用法。System.in可以喚起鍵盤輸入功能,但這個標準輸入流是InputStream類的實例,使用有所不便,而鍵盤輸入內容都是文本內容,所以可以使用InputStreamReader將其InputStream包裝成BufferedReader,利用BufferedReader.readLine()方法可以一次讀取一行內容:
public class InputStreamReaderTest {
public static void main(String[] args){
try{
//將System.in對象轉換爲Reader對象
InputStreamReader reader = new InputStreamReader(System.in);
//將普通的Reader包裝爲BufferedReader
BufferedReader bufferedReader = new BufferedReader(reader);
String buffer = null;
while((buffer = bufferedReader.readLine())!=null){
//如果讀取到字符串"quit"則退出程序
if(buffer.equals("quit")){
System.exit(1);
}
System.out.println("輸入內容:"+buffer);
}
}catch (IOException e){
e.printStackTrace();
}finally {
}
}
}
結語
通過上文我們可以對JavaIO的整體類結構和類的特性有一個初步的瞭解,我們可以在開發過程中根據需要,靈活地使用不同的IO流進行開發。如果是在操作二進制文件如圖片,就是用字節流;如果操作的是文本文件,就是用字符流。而且儘可能地多用處理流,這樣會使我們的代碼更加靈活,複用性更好。
如果本文對你有幫助,請給一個贊吧,這會是我最大的動力~
參考資料: