作者:星巴刻
? ? ? ? 作為 Java Nio 的一個(gè)基礎(chǔ)部分,其提供的 java.nio.ByteBuffer 不易被正確使用簡直讓人無語,無人愿意為它辯白。ByteBuffer 本質(zhì)只是 byte 數(shù)組的封裝,但是與 byte 數(shù)組相比起來,要理解好,需要耗費(fèi)點(diǎn)腦力。本文嘗試用一種新的結(jié)構(gòu)來解釋 ByteBuffer,用以加速正確、輕松地掌握 ByteBuffer 的使用,希望通過本文的鋪墊再去看 ByteBuffer 的注釋、源代碼以及應(yīng)用代碼,能掌握一種操控感。
一、工作區(qū)
? ? ? ? ByteBuffer 雖然是 byte 數(shù)組的封裝,但是應(yīng)用程序如何使用數(shù)組是受約束的,極少直接通過指定數(shù)組下標(biāo)的方式使用 ByteBuffer。
? ? ? ? 在此引入「工作區(qū)」的概念,用來助力理解。
? ? ? ? 工作區(qū)是一個(gè)兩邊伸縮變動(dòng)長度的區(qū)域,它的最左邊是始點(diǎn)(用 position 表示),右邊是它的終點(diǎn)(用 limit 表示)。現(xiàn)在請用你的右手掌擋住工作區(qū)的終點(diǎn)(limit 處),左手掌從工作區(qū)的左邊 position 處往右壓。這個(gè)過程中,右邊將保持靜止(淡定的靜止),隨著左手掌往右動(dòng),有節(jié)奏地一動(dòng)一停地往右,就像脈沖一樣的節(jié)奏往右。這樣,整個(gè)工作區(qū)將越來越小,position 位置漸漸地向 limit 點(diǎn)靠攏,直至兩只手掌合在一起,此時(shí) position 點(diǎn)和 limit 點(diǎn)重合。
? ? ? ? 要是覺得這個(gè)動(dòng)作有點(diǎn)幼稚,那就對了,說明完全掌握 ByteBuffer 其實(shí)也沒有很大難度。
? ? ? ? 工作區(qū)的大小,可由 limit - position 來表示,這也就是 ByteBuffer.remaining() 的實(shí)現(xiàn)
二、完成區(qū) & 禁區(qū)
? ? ? ? 在工作區(qū)的左右兩側(cè)另外分別有 1 個(gè)區(qū)域:工作區(qū)的左側(cè)是完成區(qū),右側(cè)是禁區(qū),如下圖。
? ? ? ? 這樣整個(gè) ByteBuffer 3 個(gè)區(qū)的結(jié)構(gòu)就構(gòu)建完畢了。這 3 個(gè)區(qū)先后順序是固定的,但大小是變化的。3個(gè)區(qū)的寬度大小,最小可為 0,最大可為 capacity 大。3 個(gè)區(qū)看起來還是很亂,請不要被這個(gè)影響,一定要把注意力優(yōu)先投資到兩個(gè)手掌之間的工作區(qū),這樣已經(jīng)足夠。現(xiàn)在豎起雙手掌,由于手是可以動(dòng)的,所以左手表示的 position 以及右手表示的 limit 是可以變動(dòng)的,特別是左手變動(dòng)是最頻繁。隨著動(dòng)作,工作區(qū)大小產(chǎn)生了變化,自然而然地也帶動(dòng)了左邊完成區(qū)以及右邊禁區(qū)的變化。
三、讀/寫操作
? ? ? ? 當(dāng)對 ByteBuffer 進(jìn)行操作時(shí),所有操作都是在工作區(qū)上完成的!
? ? ? ? 進(jìn)行 get() 時(shí),每一次 get() 的調(diào)用,工作區(qū)中的 position 位置的字節(jié)被讀出來,隨后工作區(qū)的始點(diǎn)向右運(yùn)動(dòng)一個(gè)位置。隨著不斷地 get(),工作區(qū)的始點(diǎn)一點(diǎn)一點(diǎn)地往右運(yùn)動(dòng),越變越小。// 手勢做起來哈,右手掌不動(dòng),左手掌往右手掌的方向動(dòng),get 一次,動(dòng)一次。盡量多做幾遍
? ? ? ? 同理的,進(jìn)行 put() 時(shí),每一次 put() 調(diào)用,字節(jié)都寫入到工作區(qū)的 position 位置中,隨后工作區(qū)的始點(diǎn)向右運(yùn)動(dòng)一個(gè)位置。隨著不斷地 put(),工作區(qū)的始點(diǎn)一點(diǎn)一點(diǎn)地往右運(yùn)動(dòng),越變越小。// put 和 get 的手勢完全一樣
? ? ? ? 一旦工作區(qū)大小變?yōu)?0 了,讀寫操作就不能再進(jìn)行了,禁區(qū)是不可用于讀寫操作的。如果強(qiáng)行繼續(xù)讀取或?qū)懭耄珺yteBuffer 將分別拋出 BufferUnderflowException 或 BufferOverflowException 異常。
四、reset() 回到原先設(shè)置的 mark 處
? ? ? ? 隨著 ByteBuffer 不斷地工作,工作區(qū)始點(diǎn)逐漸往右運(yùn)動(dòng),工作區(qū)越變越小。此時(shí)如果要重讀剛才讀取的內(nèi)容,或者覆蓋原先寫入的內(nèi)容,就可以調(diào)用 reset() 方法來滿足這個(gè)需求,將工作區(qū)的始點(diǎn)拉回之前設(shè)置的 mark 點(diǎn)。
? ? ? ? reset() 操作必須和 mark() 操作結(jié)合使用。調(diào)用 mark() 時(shí)候,ByteBuffer 會(huì)把當(dāng)時(shí)工作區(qū)的始點(diǎn)記錄下來(用 mark 表示這個(gè)位置),
? ? ? ? 調(diào)用 reset() 方法并不會(huì)把 mark 標(biāo)識清除,后續(xù)可以多次使用。如果之前沒有 mark() 過或者 mark 標(biāo)識被 rewind()、flip()、clear() 這些操作清理過,調(diào)用 reset() 沒有意義,ByteBuffer 會(huì)拋出異常。此時(shí)如果要回到某個(gè)點(diǎn),建議直接使用 ByteBuffer.position(int) 搞定,所謂調(diào)用 position(int) 的本質(zhì)也就是應(yīng)用程序自己來維護(hù) mark 記錄,這也是一個(gè)好辦法。
? ? ? ? 注意:reset 方法不是把緩沖區(qū)的字節(jié)設(shè)置為 0。
? ? ? ? 練習(xí):如何用手勢來模擬 reset() 操作呢?其實(shí)非常簡單,保持右手掌不懂,左手掌向左稍微挪動(dòng)幾步。
五、rewind() 倒帶重來
? ? ? ? 英文單詞 rewind 有重倒的意思。調(diào)用 rewind() 就是把工作區(qū)的始點(diǎn)拉到 0 處,使得接下來的工作區(qū)從 ByteBuffer 的最開始處工作。這個(gè)有啥用呢?想來想去可能在「復(fù)讀」這個(gè)場景比較有用:
? ? ? 當(dāng)一個(gè) ByteBuffer 要寫到多個(gè)輸出源的時(shí)候可以用得上:寫入到第一個(gè)輸出源后,完成區(qū)變大,工作區(qū)變小,通過調(diào)用 rewind() ,把工作區(qū)的始點(diǎn)拉到 ByteBuffer 最開始的地方,這樣就可以重新從讀取剛才已經(jīng)讀取的字節(jié)了。
? ? ? ? 在 ByteBuffer下 rewind() 就是 position(0)。所以,實(shí)際使用起來,直接使用 position(0) 可能更容易理解?另外一個(gè)區(qū)別點(diǎn), position(int) 方法在 ByteBuffer 上,沒在 Buffer 上。
? ? ? ? 練習(xí):如何用手勢來模擬 rewind() 操作呢?保持右手掌不懂,左手掌向左伸直移動(dòng)到最大的可能就是了。// reset() 和 rewind() 在手勢上的區(qū)分就是看左手伸的多少,到之前標(biāo)記的是 reset(),伸到盡頭的是 rewind()
四、flip() 翻轉(zhuǎn)工作區(qū)
? ? ? ? 英文單詞 flip 的意思有翻、轉(zhuǎn)的意思,比如海獅在沙灘上玩耍翻來翻去,調(diào)皮的同學(xué)在地上做個(gè)騰空翻等等類似的意思。
? ? ? ? 把 flip 用在 ByteBuffer 上,主要是用來表達(dá)一個(gè)動(dòng)機(jī):對 ByteBuffer 完成寫入的工作后,要開始從它里面讀取信息。ByteBuffer要求,當(dāng)對它從寫入到讀取的變化,需要應(yīng)用程序來告知 ByteBuffer 提前做一些內(nèi)部翻轉(zhuǎn)工作,flip() 方法充當(dāng)這個(gè)作用,由應(yīng)用程序來調(diào)用。
? ? ? ? 現(xiàn)在深入到 flip() 內(nèi)部。當(dāng)程序不斷把數(shù)據(jù)寫到 ByteBuffer,完成區(qū) 將越來越大,充滿了剛剛寫入的數(shù)據(jù),此時(shí)如果要將寫入的數(shù)據(jù)讀取出來,根據(jù) ByteBuffer 的哲學(xué),就需要先把這塊完成區(qū)區(qū)域設(shè)置為 工作區(qū) 才能在這片區(qū)域上工作,按應(yīng)用程序的預(yù)期完成任務(wù)。把完成區(qū)完全設(shè)置為工作區(qū)的操作工程中要注意 3 個(gè)細(xì)節(jié)就是:(1)新的工作區(qū)的終點(diǎn)就是原來完成區(qū)的終點(diǎn)、原來工作區(qū)的始點(diǎn);(2)新的工作區(qū)的始點(diǎn)在最左邊,因此新的工作區(qū)和舊的工作區(qū)大小沒有任何關(guān)系,所以兩者大小也不相等。(3) 舊的工作區(qū)變成現(xiàn)在新的工作區(qū)的右邊了,所以它成為禁區(qū)的一部分。
? ? ? ? flip() 這個(gè)方法是 ByteBuffer 的關(guān)鍵方法,重點(diǎn)記住這個(gè)方法吧。
? ? ? ? 練習(xí):豎起兩手的手掌,兩只手中間代表的是 flip() 之前的工作區(qū)。然后兩只手掌一起往左運(yùn)動(dòng)運(yùn)動(dòng)。左手掌拉伸到最左邊的盡頭,右手掌變動(dòng)原來左手掌的位置。
六、clear() 全部變?yōu)楣ぷ鲄^(qū)
? ? ? ? clear() 把緩沖區(qū)全部變?yōu)楣ぷ鲄^(qū),工作區(qū)最大也不過如此了。clear() 操作是唯一一個(gè)把禁區(qū)變?yōu)楣ぷ鲄^(qū)一部分的操作。可見 clear() 目的,就是讓 ByteBuffer 有最大的工作空間去容納一會(huì)進(jìn)來的字節(jié)。顯然,當(dāng) ByteBuffer 的信息全部被用來后,準(zhǔn)備要從輸入源中讀出新的信息寫入 ByteBuffer 時(shí),要調(diào)用 clear()。
? ? ? ? 注意:clear 方法不是把緩沖區(qū)的字節(jié)設(shè)置為 0。
? ? ? ? 練習(xí):豎起兩手的手掌,然后分別向兩邊拉伸到盡頭!
七、總結(jié)
? ? ? ? ? 用工作區(qū)的概念及其圖解、手勢的方式來理解 ByteBuffer,是本文的創(chuàng)新點(diǎn)。借助兩手手掌模擬工作區(qū),并演示 get、put、reset、rewind、position(i)、flip、clear 操作對手掌位置的影響可以有效地理解和記憶。這種辦法對其他人有沒有用我不清楚,反正自己是用上了,也輕松了許多。
? ? ? ? ? 如果以上有助于理解,接下來可以直接看下 java.nio.Buffer 的 Java Doc ,看看是否可以清晰一些,這個(gè)過程也是一次「思維加固」。
2017-11-23