前言
Java的I/O系統(tǒng)使用“流”來處理各種類型的輸入、輸出數(shù)據(jù)的任務(wù)。 在傳輸數(shù)據(jù)的過程中,我們需要判斷流中傳輸?shù)臄?shù)據(jù)何時(shí)結(jié)束這樣的問題。這對于我們正確地發(fā)送和接收數(shù)據(jù)是非常關(guān)鍵的。 如何判斷流的末尾和批數(shù)據(jù)的末尾,是解決這個(gè)問題的關(guān)鍵。本文就是要深入地分析Java I/O輸入輸出的工作原理,保證我們能夠正確地執(zhí)行數(shù)據(jù)的發(fā)送和接收! Java I/O任務(wù) 一個(gè)Java的I/O任務(wù),創(chuàng)建了一個(gè)連接兩個(gè)系統(tǒng)的數(shù)據(jù)傳輸管道。它分為兩個(gè)部分:輸入流和輸出流。 輸入流,指的是通過流向本系統(tǒng)的內(nèi)存?zhèn)鬏敂?shù)據(jù)的單向數(shù)據(jù)傳輸通道。 輸出流,指的是通過流向外部系統(tǒng)傳輸數(shù)據(jù)的單向數(shù)據(jù)傳輸通道。 輸入流 InputStream類是表示字節(jié)輸入流的所有類的超類。這是一個(gè)抽象類。 我們看它提供的讀入數(shù)據(jù)的方法: read public abstract int read() throws IOException 從輸入流讀取下一個(gè)數(shù)據(jù)字節(jié)。返回 0 到 255 范圍內(nèi)的 int 字節(jié)值。如果因已到達(dá)流末尾而沒有可用的字節(jié),則返回值 -1。在輸入數(shù)據(jù)可用、檢測到流的末尾或者拋出異常前,此方法一直阻塞。 子類必須提供此方法的一個(gè)實(shí)現(xiàn)。 返回: 下一個(gè)數(shù)據(jù)字節(jié),如果到達(dá)流的末尾,則返回 -1。 拋出: IOException - 如果發(fā)生 I/O 錯(cuò)誤。 -------------------------------------------------------------------------------- read public int read(byte[] b) throws IOException 從輸入流中讀取一定數(shù)量的字節(jié)并將其存儲(chǔ)在緩沖區(qū)數(shù)組 b 中。以整數(shù)形式返回實(shí)際讀取的字節(jié)數(shù)。在輸入數(shù)據(jù)可用、檢測到文件末尾或者拋出異常前,此方法一直阻塞。 如果 b 為 null,將拋出 NullPointerException。如果 b 的長度為 0,則無字節(jié)可讀且返回 0;否則,要嘗試讀取至少一個(gè)字節(jié)。如果因?yàn)榱魑挥谖募┪捕鴽]有可用的字節(jié),則返回值 -1;否則,至少可以讀取一個(gè)字節(jié)并將其存儲(chǔ)在 b 中。 將讀取的第一個(gè)字節(jié)存儲(chǔ)在元素 b[0] 中,下一個(gè)存儲(chǔ)在 b[1] 中,依次類推。讀取的字節(jié)數(shù)最多等于 b 的長度。讓 k 為實(shí)際讀取的字節(jié)數(shù);這些字節(jié)將存儲(chǔ)在元素 b[0] 至 b[k-1] 之間,不影響元素 b[k] 至 b[b.length-1]。 如果不是因?yàn)榱魑挥谖募┪捕鵁o法讀取讀取第一個(gè)字節(jié),則拋出 IOException。特別是,如果輸入流已關(guān)閉,則拋出 IOException。 類 InputStream 的 read(b) 方法的效果等同于: read(b, 0, b.length) 參數(shù): b - 讀入數(shù)據(jù)的緩沖區(qū)。 返回: 讀入緩沖區(qū)的總字節(jié)數(shù),如果由于流末尾已到達(dá)而不再有數(shù)據(jù),則返回 -1。 拋出: IOException - 如果發(fā)生 I/O 錯(cuò)誤。 NullPointerException - 如果 b 為 null。 另請參見: read(byte[], int, int) -------------------------------------------------------------------------------- read public int read(byte[] b, int off, int len) throws IOException 將輸入流中最多 len 個(gè)數(shù)據(jù)字節(jié)讀入字節(jié)數(shù)組。嘗試讀取多達(dá) len 字節(jié),但可能讀取較少數(shù)量。以整數(shù)形式返回實(shí)際讀取的字節(jié)數(shù)。 在輸入數(shù)據(jù)可用、檢測到流的末尾或者拋出異常前,此方法一直阻塞。 如果 b 為 null,則拋出 NullPointerException。 如果 off 為負(fù),或 len 為負(fù),或 off+len 大于數(shù)組 b 的長度,則拋出 IndexOutOfBoundsException。 如果 len 為 0,則沒有字節(jié)可讀且返回 0;否則,要嘗試讀取至少一個(gè)字節(jié)。如果因?yàn)榱魑挥谖募┪捕鴽]有可用的字節(jié),則返回值 -1;否則,至少可以讀取一個(gè)字節(jié)并將其存儲(chǔ)在 b 中。 將讀取的第一個(gè)字節(jié)存儲(chǔ)在元素 b[off] 中,下一個(gè)存儲(chǔ)在 b[off+1] 中,依次類推。讀取的字節(jié)數(shù)最多等于 len。讓 k 為實(shí)際讀取的字節(jié)數(shù);這些字節(jié)將存儲(chǔ)在元素 b[off] 至 b[off+k-1] 之間,其余元素 b[off+k] 至 b[off+len-1] 不受影響。 在任何情況下,元素 b[0] 至 b[off] 和元素 b[off+len] 至 b[b.length-1] 都不會(huì)受到影響。 如果不是因?yàn)榱魑挥谖募┪捕鵁o法讀取第一個(gè)字節(jié),則拋出 IOException。特別是,如果輸入流已關(guān)閉,則拋出 IOException。 類 InputStream 的 read(b, off, len) 方法只重復(fù)調(diào)用方法 read()。如果第一個(gè)這樣的調(diào)用導(dǎo)致 IOException,則從對 read(b, off, len) 方法的調(diào)用中返回該異常。如果對 read() 的任何后續(xù)調(diào)用導(dǎo)致 IOException,則該異常會(huì)被捕獲并將發(fā)生異常時(shí)的位置視為文件的末尾;到達(dá)該點(diǎn)時(shí)讀取的字節(jié)存儲(chǔ)在 b 中并返回發(fā)生異常之前讀取的字節(jié)數(shù)。建議讓子類提供此方法的更有效的實(shí)現(xiàn)。 參數(shù): b - 讀入數(shù)據(jù)的緩沖區(qū)。 off - 在其處寫入數(shù)據(jù)的數(shù)組 b 的初始偏移量。 len - 要讀取的最大字節(jié)數(shù)。 返回: 讀入緩沖區(qū)的總字節(jié)數(shù),如果由于已到達(dá)流末尾而不再有數(shù)據(jù),則返回 -1。 拋出: IOException - 如果發(fā)生 I/O 錯(cuò)誤。 NullPointerException - 如果 b 為 null。 另請參見: read() read方法解讀 這些方法的核心。實(shí)際上就是read()方法。 這個(gè)方法的JavaDoc中指出: “如果因已到達(dá)流末尾而沒有可用的字節(jié),則返回值 -1。如果因已到達(dá)流末尾而沒有可用的字節(jié),則返回值 -1。在輸入數(shù)據(jù)可用、檢測到流的末尾或者拋出異常前,此方法一直阻塞。” 1,read方法會(huì)在能夠返回流中的字節(jié)之前,一直阻塞線程。這就是說,read方法是一個(gè)低消耗的監(jiān)聽和讀取I/O傳輸?shù)暮梅椒ā? 這個(gè)方法的實(shí)現(xiàn),具有非常高的性能。 2,如果輸入流的I/O系統(tǒng)在執(zhí)行這個(gè)read方法時(shí)拋出異常,那么顯然這個(gè)方法會(huì)非正常結(jié)束,這個(gè)也是毫無疑問的。 3,“如果因已到達(dá)流末尾而沒有可用的字節(jié),則返回值 -1。” 這句話看似沒有問題,但實(shí)際上有非常大的歧義!什么是流的末尾?只有流的末尾才能返回-1嗎? InputStream類和SocketInputStream類的源碼解讀 通過查看InputStream類的源碼,我發(fā)現(xiàn)實(shí)際上,流,就好比是雙向2車道的高速公路。它傳輸數(shù)據(jù)是一批一批的。我把它叫做“批數(shù)據(jù)”。 假設(shè)A=======B兩個(gè)系統(tǒng)通過一個(gè)I/O流來連接。 那么,從B端通向A端的車道,就叫作A的“輸入流”。同一條車道,在B這邊,叫作B的“輸出流”。 同理,從A端通向B端的車道,就叫作A的“輸出流”。同一條車道,在B這邊,就叫作B的“輸入流”。 數(shù)據(jù)在這條高速公路上,不是一條一條跑的,而是一批一批跑。 OutputStream類,此抽象類是表示輸出字節(jié)流的所有類的超類。輸出流接受輸出字節(jié)并將這些字節(jié)發(fā)送到某個(gè)接收器。 OutputStream類的write方法,每執(zhí)行一次,就向這條高速公路上發(fā)送了一批數(shù)據(jù)。OutputStream類的一些子類,它們并不是在每次write()方法執(zhí)行之后立刻把這批數(shù)據(jù)發(fā)送到數(shù)據(jù)高速公路上的。而是只有在執(zhí)行flush()方法之后,才把之前write的多批數(shù)據(jù)真正地發(fā)送到數(shù)據(jù)通道中。 這樣,多個(gè)write()方法發(fā)送的數(shù)據(jù)就變?yōu)榱艘慌鷶?shù)據(jù)了! 通過read()方法讀入時(shí),當(dāng)讀完該批數(shù)據(jù)之后,如果再一次執(zhí)行read()方法,就會(huì)立刻返回-1。 實(shí)際上,這是并沒有到達(dá)流的末尾!僅僅是讀完了一批發(fā)送的數(shù)據(jù)而已! 如果我們又一次執(zhí)行read()方法,那么,如果: 1,流沒有結(jié)束。也就是說,對面的發(fā)送端可能還會(huì)發(fā)送下一批數(shù)據(jù)時(shí),就會(huì)進(jìn)入阻塞狀態(tài)。當(dāng)前線程暫停,直到讀取到輸入流中下一批數(shù)據(jù)的第一個(gè)字節(jié)。 2,流結(jié)束了。也就是說,對面的發(fā)送端不再發(fā)送任何數(shù)據(jù),也即:這條數(shù)據(jù)通道已經(jīng)沒有用了,這時(shí),可以說“到達(dá)流的末尾”了!返回-1。 所以,InputStream及其子類的read()方法的注釋是不完整的! Read()方法的注釋,應(yīng)該這么說: read public abstract int read() throws IOException 從輸入流讀取下一個(gè)數(shù)據(jù)字節(jié)。返回 0 到 255 范圍內(nèi)的 int 字節(jié)值。 如果在讀完一批數(shù)據(jù)后首次調(diào)用read()方法,那么返回-1。表示這批數(shù)據(jù)已經(jīng)讀完了! 如果因已到達(dá)流末尾而沒有可用的字節(jié),則返回值 -1。在輸入數(shù)據(jù)可用、檢測到流的末尾或者拋出異常前,此方法一直阻塞。 子類必須提供此方法的一個(gè)實(shí)現(xiàn)。 返回: 下一個(gè)數(shù)據(jù)字節(jié); 如果剛讀完一批數(shù)據(jù),則返回-1; 如果到達(dá)流的末尾,則返回 -1。 拋出: IOException - 如果發(fā)生 I/O 錯(cuò)誤。 如何正確使用Java I/O輸出和讀入數(shù)據(jù) 明白了Java的I/O流的工作機(jī)理和read方法的執(zhí)行結(jié)果,我們就能夠正確地使用Java I/O系統(tǒng)輸出和讀入數(shù)據(jù)了。 如何分批輸出數(shù)據(jù) 由于read(…)方法是分批讀取數(shù)據(jù)的,所以,我們應(yīng)該在輸出端正確地分批輸出數(shù)據(jù)。 Write(…)方法,然后執(zhí)行flush()方法能夠?qū)⒍嗯鷶?shù)據(jù)合并成一批數(shù)據(jù)輸出。 盡管OutputStream這個(gè)基類的flush()方法是無用的,但是由于我們得到的OutputStream類型的輸出對象都是這個(gè)類的子類的對象,所以,我們還是應(yīng)該盡量使用flush()方法強(qiáng)制向輸出流中物理輸出數(shù)據(jù),以避免錯(cuò)誤。 如何分批讀取數(shù)據(jù) 我們常常使用public int read(byte[] b, int off, int len) throws IOException 這個(gè)方法來讀取一批數(shù)據(jù)。 查看這個(gè)方法的源代碼,我們可以發(fā)現(xiàn),它在讀取完一批數(shù)據(jù)時(shí),又執(zhí)行了一次read()方法,由于前面論述的原因,這個(gè)方法立刻返回-1,然后這個(gè)方法退出,返回這次讀取的字節(jié)數(shù)。 因此,如果我們要讀取一批數(shù)據(jù),可以采用如下幾種方法: 使用read()判斷-1來讀完一批數(shù)據(jù): 代碼示例: Int byte; While((byte=inputStream.read())!=-1 ){ Byte就是返回的字節(jié)。 } 如果讀完一批數(shù)據(jù)后再一次執(zhí)行read方法,將會(huì)立刻返回-1,表示這批數(shù)據(jù)已經(jīng)讀完。 使用read(byte[] buffer)判斷是否返回小于buffer.length或者-1來判斷是否讀完一批數(shù)據(jù) 上面那樣一個(gè)一個(gè)字節(jié)讀取數(shù)據(jù)比較麻煩,我們通常使用一個(gè)字節(jié)數(shù)組來讀取數(shù)據(jù)。 read(byte[] buffer)方法返回: 1,buffer.length,表示從輸入流中讀取到的數(shù)據(jù)塞滿了這個(gè)字節(jié)數(shù)組。此時(shí),可能已經(jīng)讀完了這批數(shù)據(jù),也可能沒有讀完這批數(shù)據(jù)。 如果剛好讀完,那么再執(zhí)行一次read()方法,就會(huì)返回-1,表示這批數(shù)據(jù)已經(jīng)讀完,而不是表示流已經(jīng)結(jié)束。 2,小于buffer.length。表示讀完了該批數(shù)據(jù)。并且執(zhí)行了一次read()方法,返回-1,表示這批數(shù)據(jù)已經(jīng)讀完。 3,-1。這批數(shù)據(jù)已經(jīng)讀完了。 不可能是表示流已經(jīng)結(jié)束。因?yàn)橹熬蜁?huì)退出while循環(huán)。 代碼示例: Byte[] buffer=new byte[1024]; int size=buffer.length; While(size!=-1 || size>=buffer.length){ Size=inputStream.read(buffer)); 將數(shù)據(jù)讀到數(shù)組buffer中。 如果讀到了-1,或者 讀出的數(shù)據(jù)少于buffer的尺寸,表示已經(jīng)讀完該批數(shù)據(jù),不再循環(huán)讀取數(shù)據(jù)了! } 讀入一批數(shù)據(jù)的操作必須對應(yīng)輸出一批數(shù)據(jù)的操作 讀入一批數(shù)據(jù)的操作必須對應(yīng)輸出一批數(shù)據(jù)的操作。否則,讀入數(shù)據(jù)的線程會(huì)一直阻塞,等待輸出端輸出下一批數(shù)據(jù)。 如果對方也需要我們提供輸出數(shù)據(jù),那么就可能會(huì)使整個(gè)流的兩端的線程互相等待,死鎖住。并且這個(gè)I/O流也會(huì)永遠(yuǎn)不釋放,這樣就會(huì)使系統(tǒng)的資源耗盡。 后記: 在前兩天的工作中,我使用Socket在服務(wù)器和客戶端執(zhí)行操作時(shí),出現(xiàn)了死鎖的現(xiàn)象。為了找出問題的根源,我仔細(xì)查看了InputStream類和SocketInputStream類的源代碼。找出了造成輸入、輸出流誤用的原因。 希望這篇文章能夠幫助飽受I/O輸入、輸出流問題困擾的Java程序員! |
|