Java IO機制分析

Java 的 I/O 類庫的基本架構

    Java 的 I/O 操做類大部分在包 java.io 下,這些類大概能夠分紅四組,分別是:java

  1. 基於字節操做的 I/O 接口:InputStream 和 OutputStream
  2. 基於字符操做的 I/O 接口:Writer 和 Reader
  3. 基於磁盤操做的 I/O 接口:File
  4. 基於網絡操做的 I/O 接口:Socket

前兩組主要是根據傳輸數據的數據格式,後兩組主要是根據傳輸數據的方式,雖然 Socket 類並不在 java.io 包下,可是我仍然把它們劃分在一塊兒,由於我我的認爲 I/O 的核心問題要麼是數據格式影響 I/O 操做,要麼是傳輸方式影響 I/O 操做,也就是將什麼樣的數據寫到什麼地方的問題,I/O 只是人與機器或者機器與機器交互的手段,除了在它們可以完成這個交互功能外,咱們關注的就是如何提升它的運行效率了,而數據格式和傳輸方式是影響效率最關鍵的因素了。咱們後面的分析也是基於這兩個因素來展開的。緩存

基於字節的 I/O 操做接口

基於字節的 I/O 操做接口輸入和輸出分別是:InputStream 和 OutputStream,InputStream 輸入流的類繼承層次以下圖所示:網絡

圖 1. InputStream 相關類層次結構(查看大圖數據結構

圖 1. InputStream 相關類層次結構

輸入流根據數據類型和操做方式又被劃分紅若干個子類,每一個子類分別處理不一樣操做類型,OutputStream 輸出流的類層次結構也是相似,以下圖所示:架構

圖 2. OutputStream 相關類層次結構(查看大圖app

圖 2. OutputStream 相關類層次結構

這裏就不詳細解釋每一個子類如何使用了,若是不清楚的話能夠參考一下 JDK 的 API 說明文檔,這裏只想說明兩點,一個是操做數據的方式是能夠組合使用的,如這樣組合使用編碼

OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream("fileName"))spa

還有一點是流最終寫到什麼地方必需要指定,要麼是寫到磁盤要麼是寫到網絡中,其實從上面的類圖中咱們發現,寫網絡實際上也是寫文件,只不過寫網絡還有一步須要處理就是底層操做系統再將數據傳送到其它地方而不是本地磁盤。關於網絡 I/O 和磁盤 I/O 咱們將在後面詳細介紹。操作系統

基於字符的 I/O 操做接口

不論是磁盤仍是網絡傳輸,最小的存儲單元都是字節,而不是字符,因此 I/O 操做的都是字節而不是字符,可是爲啥有操做字符的 I/O 接口呢?這是由於咱們的程序中一般操做的數據都是以字符形式,爲了操做方便固然要提供一個直接寫字符的 I/O 接口,如此而已。咱們知道字符到字節必需要通過編碼轉換,而這個編碼又很是耗時,並且還會常常出現亂碼問題,因此 I/O 的編碼問題常常是讓人頭疼的問題。關於 I/O 編碼問題請參考另外一篇文章 《深刻分析Java中的中文編碼問題》設計

下圖是寫字符的 I/O 操做接口涉及到的類,Writer 類提供了一個抽象方法 write(char cbuf[], int off, int len) 由子類去實現。

圖 3. Writer 相關類層次結構(查看大圖

圖 3. Writer 相關類層次結構

讀字符的操做接口也有相似的類結構,以下圖所示:

圖 4.Reader 類層次結構(查看大圖

圖 4.Reader 類層次結構

讀字符的操做接口中也是 int read(char cbuf[], int off, int len),返回讀到的 n 個字節數,不論是 Writer 仍是 Reader 類它們都只定義了讀取或寫入的數據字符的方式,也就是怎麼寫或讀,可是並無規定數據要寫到哪去,寫到哪去就是咱們後面要討論的基於磁盤和網絡的工做機制。

字節與字符的轉化接口

另外數據持久化或網絡傳輸都是以字節進行的,因此必需要有字符到字節或字節到字符的轉化。字符到字節須要轉化,其中讀的轉化過程以下圖所示:

圖 5. 字符解碼相關類結構

圖 5. 字符解碼相關類結構

InputStreamReader 類是字節到字符的轉化橋樑,InputStream 到 Reader 的過程要指定編碼字符集,不然將採用操做系統默認字符集,極可能會出現亂碼問題。StreamDecoder 正是完成字節到字符的解碼的實現類。也就是當你用以下方式讀取一個文件時:

清單 1.讀取文件

1

2

3

4

5

6

7

8

9

try {

           StringBuffer str = new StringBuffer();

           char[] buf = new char[1024];

           FileReader f = new FileReader("file");

           while(f.read(buf)>0){

               str.append(buf);

           }

           str.toString();

} catch (IOException e) {}

FileReader 類就是按照上面的工做方式讀取文件的,FileReader 是繼承了 InputStreamReader 類,其實是讀取文件流,而後經過 StreamDecoder 解碼成 char,只不過這裏的解碼字符集是默認字符集。

寫入也是相似的過程以下圖所示:

圖 6. 字符編碼相關類結構

圖 6. 字符編碼相關類結構

經過 OutputStreamWriter 類完成,字符到字節的編碼過程,由 StreamEncoder 完成編碼過程。

磁盤 I/O 工做機制

前面介紹了基本的 Java I/O 的操做接口,這些接口主要定義瞭如何操做數據,以及介紹了操做兩種數據結構:字節和字符的方式。還有一個關鍵問題就是數據寫到何處,其中一個主要方式就是將數據持久化到物理磁盤,下面將介紹如何將數據持久化到物理磁盤的過程。

咱們知道數據在磁盤的惟一最小描述就是文件,也就是說上層應用程序只能經過文件來操做磁盤上的數據,文件也是操做系統和磁盤驅動器交互的一個最小單元。值得注意的是 Java 中一般的 File 並不表明一個真實存在的文件對象,當你經過指定一個路徑描述符時,它就會返回一個表明這個路徑相關聯的一個虛擬對象,這個多是一個真實存在的文件或者是一個包含多個文件的目錄。爲什麼要這樣設計?由於大部分狀況下,咱們並不關心這個文件是否真的存在,而是關心這個文件到底如何操做。例如咱們手機裏一般存了幾百個朋友的電話號碼,可是咱們一般關心的是我有沒有這個朋友的電話號碼,或者這個電話號碼是什麼,可是這個電話號碼到底能不能打通,咱們並非時時刻刻都去檢查,而只有在真正要給他打電話時纔會看這個電話能不能用。也就是使用這個電話記錄要比打這個電話的次數多不少。

什麼時候真正會要檢查一個文件存不存?就是在真正要讀取這個文件時,例如 FileInputStream 類都是操做一個文件的接口,注意到在建立一個 FileInputStream 對象時,會建立一個 FileDescriptor 對象,其實這個對象就是真正表明一個存在的文件對象的描述,當咱們在操做一個文件對象時能夠經過 getFD() 方法獲取真正操做的與底層操做系統關聯的文件描述。例如能夠調用 FileDescriptor.sync() 方法將操做系統緩存中的數據強制刷新到物理磁盤中。

下面以清單 1 的程序爲例,介紹下如何從磁盤讀取一段文本字符。以下圖所示:

圖 7. 從磁盤讀取文件

圖 7. 從磁盤讀取文件

當傳入一個文件路徑,將會根據這個路徑建立一個 File 對象來標識這個文件,而後將會根據這個 File 對象建立真正讀取文件的操做對象,這時將會真正建立一個關聯真實存在的磁盤文件的文件描述符 FileDescriptor,經過這個對象能夠直接控制這個磁盤文件。因爲咱們須要讀取的是字符格式,因此須要 StreamDecoder 類將 byte 解碼爲 char 格式,至於如何從磁盤驅動器上讀取一段數據,由操做系統幫咱們完成。至於操做系統是如何將數據持久化到磁盤以及如何創建數據結構須要根據當前操做系統使用何種文件系統來回答,至於文件系統的相關細節能夠參考另外的文章。

參考文檔:

https://www.ibm.com/developerworks/cn/java/j-lo-javaio/ 

相關文章
相關標籤/搜索