如何正確使用Java I/O輸出和讀入資料
前言Java的I/O系統使用“流”來處理各種類型的輸入、輸出資料的任務。在傳輸資料的過程中,我們需要判斷流中傳輸的資料何時結束這樣的問題。這對於我們正確地發送和接收資料是非常關鍵的。如何判斷流的末尾和批資料的末尾,是解決這個問題的關鍵。本文就是要深入地分析Java I/O輸入輸出的工作原理,保證我們能夠正確地執行資料的發送和接收!
Java I/O任務一個Java的I/O任務,建立了一個串連兩個系統的資料轉送管道。它分為兩個部分:輸入資料流和輸出資料流。輸入資料流,指的是通過流向本系統的記憶體傳輸資料的單向資料轉送通道。輸出資料流,指的是通過流向外部系統傳輸資料的單向資料轉送通道。
輸入資料流
InputStream
類是表示位元組輸入資料流的所有類的超類。這是一個抽象類別。我們看它提供的讀入資料的方法:
read
public abstract int read()
throws IOException
從輸入資料流讀取下一個資料位元組。返回
0 到
255 範圍內的
int 位元組值。如果因已到達流末尾而沒有可用的位元組,則傳回值
-1。在輸入資料可用、檢測到流的末尾或者拋出異常前,此方法一直阻塞。 子類必須提供此方法的一個實現。
返回: 下一個資料位元組,如果到達流的末尾,則返回
-1。
拋出:
IOException - 如果發生 I/O 錯誤。
read
public int read(byte[] b)
throws IOException
從輸入資料流中讀取一定數量的位元組並將其儲存在緩衝區數組
b 中。以整數形式返回實際讀取的位元組數。在輸入資料可用、檢測到檔案末尾或者拋出異常前,此方法一直阻塞。 如果
b 為
null,將拋出
NullPointerException。如果
b 的長度為 0,則無位元組可讀且返回
0;否則,要嘗試讀取至少一個位元組。如果因為流位於檔案末尾而沒有可用的位元組,則傳回值
-1;否則,至少可以讀取一個位元組並將其儲存在
b 中。 將讀取的第一個位元組儲存在元素
b[0] 中,下一個儲存在
b[1] 中,依次類推。讀取的位元組數最多等於
b 的長度。讓
k 為實際讀取的位元組數;這些位元組將儲存在元素
b[0] 至
b[
k
-1] 之間,不影響元素
b[
k
] 至
b[b.length-1]。 如果不是因為流位於檔案末尾而無法讀取讀取第一個位元組,則拋出
IOException。特別是,如果輸入資料流已關閉,則拋出
IOException。 類
InputStream 的
read(b) 方法的效果等同於:
read(b, 0, b.length)
參數:
b - 讀入資料的緩衝區。
返回: 讀入緩衝區的總位元組數,如果由於流末尾已到達而不再有資料,則返回
-1。
拋出:
IOException - 如果發生 I/O 錯誤。
NullPointerException - 如果
b 為
null。
另請參見:
read(byte[], int, int)
read
public int read(byte[] b,
int off,
int len)
throws IOException
將輸入資料流中最多
len 個資料位元組讀入位元組數組。嘗試讀取多達
len 位元組,但可能讀取較少數量。以整數形式返回實際讀取的位元組數。 在輸入資料可用、檢測到流的末尾或者拋出異常前,此方法一直阻塞。 如果
b 為
null,則拋出
NullPointerException。 如果
off 為負,或
len 為負,或
off+len 大於數組
b 的長度,則拋出
IndexOutOfBoundsException。 如果
len 為 0,則沒有位元組可讀且返回
0;否則,要嘗試讀取至少一個位元組。如果因為流位於檔案末尾而沒有可用的位元組,則傳回值
-1;否則,至少可以讀取一個位元組並將其儲存在
b 中。 將讀取的第一個位元組儲存在元素
b[off] 中,下一個儲存在
b[off+1] 中,依次類推。讀取的位元組數最多等於
len。讓
k 為實際讀取的位元組數;這些位元組將儲存在元素
b[off] 至
b[off+
k
-1] 之間,其餘元素
b[off+
k
] 至
b[off+len-1] 不受影響。 在任何情況下,元素
b[0] 至
b[off] 和元素
b[off+len] 至
b[b.length-1] 都不會受到影響。 如果不是因為流位於檔案末尾而無法讀取第一個位元組,則拋出
IOException。特別是,如果輸入資料流已關閉,則拋出
IOException。 類
InputStream 的
read(b,
off,
len) 方法只重複調用方法
read()。如果第一個這樣的調用導致
IOException,則從對
read(b,
off,
len) 方法的調用中返回該異常。如果對
read() 的任何後續調用導致
IOException,則該異常會被捕獲並將發生異常時的位置視為檔案的末尾;到達該點時讀取的位元組儲存在
b 中並返回傳生異常之前讀取的位元組數。建議讓子類提供此方法的更有效實現。
參數:
b - 讀入資料的緩衝區。
off - 在其處寫入資料的數組
b 的初始位移量。
len - 要讀取的最大位元組數。
返回: 讀入緩衝區的總位元組數,如果由於已到達流末尾而不再有資料,則返回
-1。
拋出:
IOException - 如果發生 I/O 錯誤。
NullPointerException - 如果
b 為
null。
另請參見:
read()
read方法解讀這些方法的核心。實際上就是read()方法。這個方法的JavaDoc中指出:“如果因已到達流末尾而沒有可用的位元組,則傳回值
-1。如果因已到達流末尾而沒有可用的位元組,則傳回值
-1。在輸入資料可用、檢測到流的末尾或者拋出異常前,此方法一直阻塞。” 1,read方法會在能夠返迴流中的位元組之前,一直阻塞線程。這就是說,read方法是一個低消耗的監聽和讀取I/O傳輸的好方法。這個方法的實現,具有非常高的效能。 2,如果輸入資料流的I/O系統在執行這個read方法時拋出異常,那麼顯然這個方法會非正常結束,這個也是毫無疑問的。 3,“如果因已到達流末尾而沒有可用的位元組,則傳回值
-1。”這句話看似沒有問題,但實際上有非常大的歧義!什麼是流的末尾?只有流的末尾才能返回-1嗎?
InputStream類和SocketInputStream類的源碼解讀通過查看InputStream類的源碼,我發現實際上,流,就好比是雙向2車道的高速公路。它傳輸資料是一批一批的。我把它叫做“批資料”。假設A=======B兩個系統通過一個I/O流來串連。那麼,從B端通向A端的車道,就叫作A的“輸入資料流”。同一條車道,在B這邊,叫作B的“輸出資料流”。同理,從A端通向B端的車道,就叫作A的“輸出資料流”。同一條車道,在B這邊,就叫作B的“輸入資料流”。 資料在這條高速公路上,不是一條一條跑的,而是一批一批跑。
OutputStream類,此抽象類別是表示輸出位元組流的所有類的超類。輸出資料流接受輸出位元組並將這些位元組發送到某個接收器。
OutputStream類的write方法,每執行一次,就向這條高速公路上發送了一批資料。OutputStream類的一些子類,它們並不是在每次write()方法執行之後立刻把這批資料發送到資料高速公路上的。而是只有在執行flush()方法之後,才把之前write的多批資料真正地發送到資料通道中。
這樣,多個write()方法發送的資料就變為了一批資料了!
通過read()方法讀入時,當讀完該批資料之後,如果再一次執行read()方法,就會立刻返回-1。
實際上,這是並沒有到達流的末尾!僅僅是讀完了一批發送的資料而已!
如果我們又一次執行read()方法,那麼,如果:
1,流沒有結束。也就是說,對面的發送端可能還會發送下一批資料時,就會進入阻塞狀態。當前線程暫停,直到讀取到輸入資料流中下一批資料的第一個位元組。
2,流結束了。也就是說,對面的發送端不再發送任何資料,也即:這條資料通道已經沒有用了,這時,可以說“到達流的末尾”了!返回-1。
所以,InputStream及其子類的read()方法的注釋是不完整的!
Read()方法的注釋,應該這麼說:
read
public abstract int read()
throws IOException
從輸入資料流讀取下一個資料位元組。返回
0 到
255 範圍內的
int 位元組值。如果在讀完一批資料後首次調用read()方法,那麼返回-1。表示這批資料已經讀完了!如果因已到達流末尾而沒有可用的位元組,則傳回值
-1。在輸入資料可用、檢測到流的末尾或者拋出異常前,此方法一直阻塞。 子類必須提供此方法的一個實現。
返回: 下一個資料位元組;如果剛讀完一批資料,則返回-1;如果到達流的末尾,則返回
-1。
拋出:
IOException - 如果發生 I/O 錯誤。
如何正確使用Java I/O輸出和讀入資料明白了Java的I/O流的工作機理和read方法的執行結果,我們就能夠正確地使用Java I/O系統輸出和讀入資料了。
如何分批輸出資料由於read(…)方法是分批讀取資料的,所以,我們應該在輸出端正確地分批輸出資料。Write(…)方法,然後執行flush()方法能夠將多批資料合併成一批資料輸出。儘管OutputStream這個基類的flush()方法是無用的,但是由於我們得到的OutputStream類型的輸出對象都是這個類的子類的對象,所以,我們還是應該盡量使用flush()方法強制向輸出資料流中物理輸出資料,以避免錯誤。
如何分批讀取資料
我們常常使用public int read(byte[] b,
int off, int len) throws IOException這個方法來讀取一批資料。查看這個方法的原始碼,我們可以發現,它在讀取完一批資料時,又執行了一次read()方法,由於前面論述的原因,這個方法立刻返回-1,然後這個方法退出,返回這次讀取的位元組數。因此,如果我們要讀取一批資料,可以採用如下幾種方法:
使用read()判斷-1來讀完一批資料: 程式碼範例:Int byte;While((byte=inputStream.read())!=-1 ){ Byte就是返回的位元組。 }如果讀完一批資料後再一次執行read方法,將會立刻返回-1,表示這批資料已經讀完。
使用read(byte[] buffer)判斷是否返回小於buffer.length或者-1來判斷是否讀完一批資料上面那樣一個一個位元組讀取資料比較麻煩,我們通常使用一個位元組數組來讀取資料。read(byte[] buffer)方法返回:1,buffer.length,表示從輸入資料流中讀取到的資料塞滿了這個位元組數組。此時,可能已經讀完了這批資料,也可能沒有讀完這批資料。 如果剛好讀完,那麼再執行一次read()方法,就會返回-1,表示這批資料已經讀完,而不是表示流已經結束。2,小於buffer.length。表示讀完了該批資料。並且執行了一次read()方法,返回-1,表示這批資料已經讀完。3,-1。這批資料已經讀完了。 不可能是表示流已經結束。因為之前就會退出while迴圈。 程式碼範例:Byte[] buffer=new byte[1024];int size=buffer.length;While(size!=-1 || size>=buffer.length){Size=inputStream.read(buffer));將資料讀到數組buffer中。如果讀到了-1,或者 讀出的資料少於buffer的尺寸,表示已經讀完該批資料,不再迴圈讀取資料了! }
讀入一批資料的操作必須對應輸出一批資料的操作讀入一批資料的操作必須對應輸出一批資料的操作。否則,讀入資料的線程會一直阻塞,等待輸出端輸出下一批資料。如果對方也需要我們提供輸出資料,那麼就可能會使整個流的兩端的線程互相等待,死結住。並且這個I/O流也會永遠不釋放,這樣就會使系統的資源耗盡。
後記:在前兩天的工作中,我使用Socket在伺服器和用戶端執行操作時,出現了死結的現象。為了找出問題的根源,我仔細查看了InputStream類和SocketInputStream類的原始碼。找出了造成輸入、輸出資料流誤用的原因。希望這篇文章能夠協助飽受I/O輸入、輸出資料流問題困擾的Java程式員!