11.2 I/O類體系
在JDK API中,基礎的IO類都位於java.io包,而新實現的IO類則位於一系列以java.nio開頭的包名中,這裡首先介紹java.io包中類的體系結構。
按照前面的說明,流是有方向的,則整個流的結構按照流的方向可以劃分為兩類:
1、輸入流:
該類流將外部數據源的數據轉換為流,程序通過讀取該類流中的數據,完成對於外部數據源中數據的讀入。
2、輸出流:
該類流完成將流中的數據轉換到對應的數據源中,程序通過向該類流中寫入數據,完成將數據寫入到對應的外部數據源中。
而在實際實現時,由於JDK API歷史的原因,在java.io包中又實現了兩類流:字節流(byte stream)和字符流(char stream)。這兩種流實現的是流中數據序列的單位,在字節流中,數據序列以byte為單位,也就是流中的數據按照一個byte一個byte的順序實現成流,對於該類流操作的基本單位是一個byte,而對於字節流,數據序列以char為單位,也就是流中的數據按照一個char一個插入的順序實現成流,對於該類流操作的基本單位是一個char。
另外字節流是從JDK1.0開始加入到API中的,而字符流則是從JDK1.1開始才加入到API中的,對於現在使用的JDK版本來說,這兩類流都包含在API的內部。在實際使用時,字符流的效率要比字節流高一些。
在實際使用時,字符流中的類基本上和字節流中的類對應,所以在開始學習IO類時,可以從最基礎的字節流開始學習。
在SUN設計JDK的IO類時,按照以上的分類,為每個系列的類設計了一個父類,而實現具體操作的類都作為該系列類的子類,則IO類設計時的四個體系中每個體系中對應的父類分別是:
11.2.1 字節輸入流InputStream
該類是IO編程中所有字節輸入流的父類,熟悉該類的使用將對使用字節輸入流產生很大的幫助,下面做一下詳細的介紹。
按照前面介紹的流的概念,字節輸入流完成的是按照字節形式構造讀取數據的輸入流的結構,每個該類的對象就是一個實際的輸入流,在構造時由API完成將外部數據源轉換為流對象的操作,這種轉換對程序員來說是透明的。在程序使用時,程序員只需要讀取該流對象,就可以完成對於外部數據的讀取了。
InputStream是所有字節輸入流的父類,所以在InputStream類中包含的每個方法都會被所有字節輸入流類繼承,通過將讀取以及操作數據的基本方法都聲明在InputStream類內部,使每個子類根據需要覆蓋對應的方法,這樣的設計可以保證每個字節輸入流子類在進行實際使用時,開放給程序員使用的功能方法是一致的。這樣將簡化IO類學習的難度,方便程序員進行實際的編程。
默認情況下,對於輸入流內部數據的讀取都是單向的,也就是只能從輸入流從前向後讀,已經讀取的數據將從輸入流內部刪除掉。如果需要重復讀取流中同一段內容,則需要使用流類中的mark方法進行標記,然後才能重復讀取。這種設計在使用流類時,需要深刻進行體會。
在InputStream類中,常見的方法有:
a、available方法
public int available() throws IOException
該方法的作用是返回當前流對象中還沒有被讀取的字節數量。也就是獲得流中數據的長度。
假設初始情況下流內部包含100個字節的數據,程序調用對應的方法讀取了一個字節,則當前流中剩余的字節數量將變成99個。
另外,該方法不是在所有字節輸入流內部都得到正確的實現,所以使用該方法獲得流中數據的個數是不可靠的。
b、close方法
public void close() throws IOException
該方法的作用是關閉當前流對象,並釋放該流對象占用的資源。
在IO操作結束以後,關閉流是進行IO操作時都需要實現的功能,這樣既可以保證數據源的安全,也可以減少內存的占用。
c、markSupported方法
public boolean markSupported()
該方法的作用是判斷流是否支持標記(mark)。標記類似於讀書時的書簽,可以很方便的回到原來讀過的位置繼續向下讀取。
d、reset方法
public void reset() throws IOException
該方法的作用是使流讀取的位置回到設定標記的位置。可以從該位置開始繼續向後讀取。
e、mark方法
public void mark(int readlimit)
為流中當前的位置設置標志,使得以後可以從該位置繼續讀取。變量readlimit指設置該標志以後可以讀取的流中最大數據的個數。當設置標志以後,讀取的字節數量超過該限制,則標志會失效。
f、read方法
read方法是輸入流類使用時最核心的方法,能夠熟練使用該方法就代表IO基本使用已經入門。所以在學習以及後期的使用中都需要深刻理解該方法的使用。
在實際讀取流中的數據時,只能按照流中的數據存儲順序依次進行讀取,在使用字節輸入流時,讀取數據的最小單位是字節(byte)。
另外,需要注意的是,read方法是阻塞方法,也就是如果流對象中無數據可以讀取時,則read方法會阻止程序繼續向下運行,一直到有數據可以讀取為止。
read方法總計有三個,依次是:
public abstract int read() throws IOException
該方法的作用是讀取當前流對象中的第一個字節。當該字節被讀取出來以後,則該字節將被從流對象中刪除,原來流對象中的第二個字節將變成流中的第一個字節,而使用流對象的available方法獲得的數值也將減少1。如果需要讀取流中的所以數據,只要使用一個循環依次讀取每個數據即可。當讀取到流的末尾時,該方法返回-1。該返回值的int中只有最後一個字節是流中的有效數據,所以在獲得流中的數值時需要進行強制轉換。返回值作成int的目的主要是處理好-1的問題。
由於該方法是抽象的,所以會在子類中被覆蓋,從而實現最基礎的讀數據的功能。
public int read(byte[] b) throws IOException
該方法的作用是讀取當前流對象中的數據,並將讀取到的數據依次存儲到數組b(b需要提前初始化完成)中,也就是把當前流中的第一個字節的數據存儲到b[0],第二個字節的數據存儲到b[1],依次類推。流中已經讀取過的數據也會被刪除,後續的數據會變成流中的第一個字節。而實際讀取的字節數量則作為方法的返回值返回。
public int read(byte[] b, int off, int len) throws IOException
該方法的作用和上面的方法類似,也是將讀取的數據存儲到b中,只是將流中的第一個數據存儲到b中下標為off的位置,最多讀取len個數據,而實際讀取的字節數量則作為方法的返回值返回。
g、skip方法
public long skip(long n) throws IOException
該方法的作用是跳過當前流對象中的n個字節,而實際跳過的字節數量則以返回值的方式返回。
跳過n個字節以後,如果需要讀取則是從新的位置開始讀取了。使用該方法可以跳過流中指定的字節數,而不用依次進行讀取了。
從流中讀取出數據以後,獲得的是一個byte數組,還需要根據以前的數據格式,實現對於該byte數組的解析。
由於InputStream類是字節輸入流的父類,所以該體系中的每個子類都包含以上的方法,這些方法是實現IO流數據讀取的基礎。
11.2.2 字節輸出流OutputStream
該類是所有的字節輸出流的父類,在實際使用時,一般使用該類的子類進行編程,但是該類內部的方法是實現字節輸出流的基礎。
該體系中的類完成把對應的數據寫入到數據源中,在寫數據時,進行的操作分兩步實現:第一步,將需要輸出的數據寫入流對象中,數據的格式由程序員進行設定,該步驟需要編寫代碼實現;第二步,將流中的數據輸出到數據源中,該步驟由API實現,程序員不需要了解內部實現的細節,只需要構造對應的流對象即可。
在實際寫入流時,流內部會保留一個緩沖區,會將程序員寫入流對象的數據首先暫存起來,然後在緩沖區滿時將數據輸出到數據源。當然,當流關閉時,輸出流內部的數據會被強制輸出。
字節輸出流中數據的單位是字節,在將數據寫入流時,一般情況下需要將數據轉換為字節數組進行寫入。
在OutputStream中,常見的方法有:
a、close方法
public void close() throws IOException
該方法的作用是關閉流,釋放流占用的資源。
b、flush方法
public void flush() throws IOException
該方法的作用是將當前流對象中的緩沖數據強制輸出出去。使用該方法可以實現立即輸出。
c、write方法
write方法是輸出流中的核心方法,該方法實現將數據寫入流中。在實際寫入前,需要實現對應的格式,然後依次寫入到流中。寫入流的順序就是實際數據輸出的順序。
write方法總計有3個,依次是:
public abstract void write(int b) throws IOException
該方法的作用是向流的末尾寫入一個字節的數據。寫入的數據為參數b的最後一個字節。在實際向流中寫數據時需要按照邏輯的順序進行寫入。該方法在OutputStream的子類內部進行實現。
public void write(byte[] b) throws IOException
該方法的作用是將數組b中的數據依次寫入當前的流對象中。
public void write(byte[] b, int off, int len) throws IOException
該方法的作用是將數組b中從下標為off(包含)開始,後續長度為len個的數據依次寫入到流對象中。
在實際寫入時,還需要根據邏輯的需要設定byte數值的格式,這個根據不同的需要實現不同的格式。
11.2.3 字符輸入流Reader
字符輸入流體系是對字節輸入流體系的升級,在子類的功能上基本和字節輸入流體系中的子類一一對應,但是由於字符輸入流內部設計方式的不同,使得字符輸入流的執行效率要比字節輸入流體系高一些,在遇到類似功能的類時,可以優先選擇使用字符輸入流體系中的類,從而提高程序的執行效率。
Reader體系中的類和InputStream體系中的類,在功能上是一致的,最大的區別就是Reader體系中的類讀取數據的單位是字符(char),也就是每次最少讀入一個字符(兩個字節)的數據,在Reader體系中的讀數據的方法都以字符作為最基本的單位。
Reader類和InputStream類中的很多方法,無論聲明還是功能都是一樣的,但是也增加了兩個方法,依次介紹如下:
a、read方法
public int read(CharBuffer target) throws IOException
該方法的作用是將流內部的數據依次讀入CharBuffer對象中,實際讀入的char個數作為返回值返回。
b、ready方法
public boolean ready() throws IOException
該方法的作用是返回當前流對象是否准備完成,也就是流內部是否包含可以被讀取的數據。
其它和InputStream類一樣的方法可以參看上面的介紹。
11.2.4 字符輸出流Writer
字符輸出流體系是對字節輸出流體系的升級,在子類的功能實現上基本上和字節輸出流保持一一對應。但由於該體系中的類設計的比較晚,所以該體系中的類執行的效率要比字節輸出流中對應的類效率高一些。在遇到類似功能的類時,可以優先選擇使用該體系中的類進行使用,從而提高程序的執行效率。
Writer體系中的類和OutputStream體系中的類,在功能上是一致的,最大的區別就是Writer體系中的類寫入數據的單位是字符(char),也就是每次最少寫入一個字符(兩個字節)的數據,在Writer體系中的寫數據的方法都以字符作為最基本的操作單位。
Writer類和OutputStream類中的很多方法,無論聲明還是功能都是一樣的,但是還是增加了一些方法,依次介紹如下:
a、append方法
將數據寫入流的末尾。總計有3個方法,依次是:
public Writer append(char c) throws IOException
該方法的作用和write(int c)的作用完全一樣,既將字符c寫入流的末尾。
public Writer append(CharSequence csq) throws IOException
該方法的作用是將CharSequence對象csq寫入流的末尾,在寫入時會調用csq的toString方法將該對象轉換為字符串,然後再將該字符串寫入流的末尾。
public Writer append(CharSequence csq, int start, int end)
throws IOException
該方法的作用和上面的方法類似,只是將轉換後字符串從索引值為start(包含)到索引值為end(不包含)的部分寫入流中。
b、write方法
除了基本的write方法以外,在Writer類中又新增了兩個,依次是:
public void write(String str) throws IOException
該方法的作用是將字符串str寫入流中。寫入時首先將str使用getChars方法轉換成對應的char數組,然後實現依次寫入流的末尾。
public void write(String str, int off, int len)
throws IOException
該方法的作用是將字符串str中索引值為off(包含)開始,後續長度為len個字符寫入到流的末尾。
使用這兩個方法將更方便將字符串寫入流的末尾。
其它和OutputStream類一樣的方法可以參看上面的介紹。
11.2.5 小結
在實際使用IO類時,根據邏輯上的需要,挑選對應體系中的類進行實際的使用,從而實現程序中IO的相關功能。
熟悉了IO類的體系以後,就可以首先熟悉基本的IO類的使用,然後再按照IO類體系中相關類的使用方式逐步去了解相關的IO類的使用,從而逐步熟悉java.io包中類的使用,然後再掌握IO編程。
在實際使用時,一般都使用這4個類中對應的子類,每個子類完成相關的功能。對於這些子類,也可以根據這些類是否直接連接數據源,將這些IO類分類為:
1、實體流
指直接連接數據源的IO流類
2、裝飾流
指不直接連接數據源,而是建立在其它實體流對象的基礎之上。
下面IO類的使用中將分別介紹這些體系中的類。在實際使用時也應該根據流屬於實體流還是裝飾流進行不同的使用。