BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 上
此係列文章會詳細解讀NIO的功能逐步豐滿的路程,為Reactor-Netty 庫的講解鋪平道路。
關於Java程式設計方法論-Reactor與Webflux的視訊分享,已經完成了Rxjava 與 Reactor,b站地址如下:
Rxjava原始碼解讀與分享: www.bilibili.com/video/av345…
Reactor原始碼解讀與分享: www.bilibili.com/video/av353…
本系列原始碼解讀基於JDK11 api細節可能與其他版本有所差別,請自行解決jdk版本問題。
本系列前幾篇:
BIO到NIO原始碼的一些事兒之NIO 下 之 Selector
Buffer
在Java BIO中,通過 BIO到NIO原始碼的一些事兒之BIO 開篇的Demo可知,所有的讀寫API,都是直接使用byte陣列作為緩衝區的,簡單直接。我們來拿一個杯子做例子,我們不講它的材質,只說它的使用屬性,一個杯子在使用過程中會首先看其最大容量,然後加水,這裡給一個限制,即加到杯子中的水量為杯子最大容量的一半,然後喝水,我們最多也只能喝杯子裡所盛水量。由這個例子,我們思考下,杯子是不是可以看作是一個緩衝區,對於杯子倒水的節奏我們是不是可以輕易的控制,從而帶來諸多方便,那是不是可以將之前 BIO
中的緩衝區也加入一些特性,使之變的和我們使用杯子一樣便捷。 於是,我們給 buffer
新增幾個屬性,對比杯子的最大容量,我們設計新增一個 capacity
屬性,對比加上的容量限制,我們設計新增一個 limit
屬性,對於加水加到杯中的當前位置,我們設計新增一個 position
屬性,有時候我們還想在杯子上自己做個標記,比如喝茶,我自己的習慣就是喝到杯裡剩三分之一水的時候再加水加到一半,針對這個情況,設計新增一個 mark
屬性。由此,我們來總結下這幾個屬性的關係, limit
不可能比 capacity
大的, position
又不會大於 limit
, mark
可以理解為一個標籤,其也不會大於 position
,也就是 mark <= position <= limit <= capacity
。
結合以上概念,我們來對buffer中這幾個屬性使用時的行為進行下描述:
-
capacity
也就是緩衝區的容量大小。我們只能往裡面寫
capacity
個byte
、long
、char
等型別。一旦Buffer
滿了,需要將其清空(通過讀資料或者清除資料)才能繼續寫資料往裡寫資料。 -
position
(1)當我們寫資料到
Buffer
中時,position
表示當前的位置。初始的position
值為0.當一個byte
、long
、char
等資料寫到Buffer
後,position
會向前移動到下一個可插入資料的Buffer
位置。position
最大可為capacity – 1
。(2)當讀取資料時,也是從某個特定位置讀。當將
Buffer
從寫模式切換到讀模式,position
會被重置為0
. 當從Buffer
的position
處讀取資料時,position
向前移動到下一個可讀的位置。 -
limit
(1)在寫模式下,
Buffer
的limit
表示你最多能往Buffer
裡寫多少資料。寫模式下,limit
等於Buffer
的capacity
。(2)讀模式時,
limit
表示你最多能讀到多少資料。因此,當切換Buffer
到讀模式時,limit
會被設定成寫模式下的position
值。換句話說,你能讀到之前寫入的所有資料(limit
被設定成已寫資料的數量,這個值在寫模式下就是position
) -
mark
類似於喝茶喝到剩餘三分之一誰加水一樣,當buffer呼叫它的reset方法時,當前的位置
position
會指向mark
所在位置,同樣,這個也根據個人喜好,有些人就喜歡將水喝完再新增的,所以mark
不一定總會被設定,但當它被設定值之後,那設定的這個值不能為負數,同時也不能大於position
。還有一種情況,就是我喝水喝不下了,在最後將水一口喝完,則對照的此處的話,即如果對mark
設定了值(並非初始值-1),則在將position
或limit
調整為小於mark
的值的時候將mark
丟棄掉。如果並未對mark
重新設定值(即還是初始值-1),那麼在呼叫reset
方法會丟擲InvalidMarkException
異常。
可見,經過包裝的Buffer是Java NIO中對於緩衝區的抽象。在Java有8中基本型別: byte、short、int、long、float、double、char、boolean
,除了 boolean
型別外,其他的型別都有對應的 Buffer
具體實現,可見, Buffer
是一個用於儲存特定基本資料型別的容器。再加上資料時有序儲存的,而且 Buffer
有大小限制,所以, Buffer
可以說是特定基本資料型別的線性儲存有限的序列。
接著,我們通過下面這幅圖來展示下上面幾個屬性的關係,方便大家更好理解:

Buffer的基本用法
先來看一個Demo:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw"); FileChannel inChannel = aFile.getChannel(); //create buffer with capacity of 48 bytes ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf); //read into buffer. while (bytesRead != -1) { buf.flip();//make buffer ready for read while(buf.hasRemaining()){ System.out.print((char) buf.get()); // read 1 byte at a time } buf.clear(); //make buffer ready for writing bytesRead = inChannel.read(buf); } aFile.close(); 複製程式碼
我們拋去前兩行,來總結下buffer的使用步驟:
- 通過相應型別Buffer的allocate的靜態方法來分配指定型別大小的緩衝資料區域(此處為buf);
- 寫入資料到Buffer;
- 呼叫flip()方法:Buffer從寫模式切換到讀模式;
- 從buffer讀取資料;
- 呼叫clear()方法或則compact()方法。
Buffer分配
那我們依據上面的步驟來一一看下其相應原始碼實現,這裡我們使用ByteBuffer來解讀。首先是Buffer分配。
//java.nio.ByteBuffer#allocate public static ByteBuffer allocate(int capacity) { if (capacity < 0) throw createCapacityException(capacity); return new HeapByteBuffer(capacity, capacity); } //java.nio.ByteBuffer#allocateDirect public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); } 複製程式碼
ByteBuffer
是一個抽象類,具體的實現有 HeapByteBuffer
和 DirectByteBuffer
。分別對應 Java
堆緩衝區與堆外記憶體緩衝區。Java堆緩衝區本質上就是byte陣列(由之前分析的,我們只是在位元組陣列上面加點屬性,輔以邏輯,實現一些更復雜的功能),所以實現會比較簡單。而堆外記憶體涉及到JNI程式碼實現,較為複雜,所以我們先來分析 HeapByteBuffer
的相關操作,隨後再專門分析 DirectByteBuffer
。
我們來看 HeapByteBuffer
相關構造器原始碼:
//java.nio.HeapByteBuffer#HeapByteBuffer(int, int) HeapByteBuffer(int cap, int lim) { super(-1, 0, lim, cap, new byte[cap], 0); /* hb = new byte[cap]; offset = 0; */ this.address = ARRAY_BASE_OFFSET; } //java.nio.ByteBuffer#ByteBuffer(int, int, int, int, byte[], int) ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) { super(mark, pos, lim, cap); this.hb = hb; this.offset = offset; } //java.nio.Buffer#Buffer Buffer(int mark, int pos, int lim, int cap) { if (cap < 0) throw createCapacityException(cap); this.capacity = cap; limit(lim); position(pos); if (mark >= 0) { if (mark > pos) throw new IllegalArgumentException("mark > position: (" + mark + " > " + pos + ")"); this.mark = mark; } } 複製程式碼
由上, HeapByteBuffer
通過初始化位元組陣列 hd
,在虛擬機器堆上申請記憶體空間。 因在 ByteBuffer
中定義有 hb
這個欄位,它是一個 byte[]
型別,為了獲取這個欄位相對於當前這個 ByteBuffer
物件所在記憶體地址,通過 private static final long ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(byte[].class)
中這個 UNSAFE
操作來獲取這個陣列第一個元素位置與該物件所在地址的相對長度,這個物件的地址代表你的頭所在的位置,將這個陣列看作你的鼻子,而這裡返回的是你的鼻子距離頭位置的那個長度,即陣列第一個位置距離這個物件開始地址所在位置,這個是在class位元組碼載入到jvm裡的時候就已經確定了。 如果 ARRAY_INDEX_SCALE = UNSAFE.arrayIndexScale(byte[].class)
為返回非零值,則可以使用該比例因子以及此基本偏移量(ARRAY_BASE_OFFSET)來形成新的偏移量,以訪問這個類的陣列元素。知道這些,在 ByteBuffer
的 slice
duplicate
之類的方法,就能理解其操作了,就是計算陣列中每一個元素所佔空間長度得到 ARRAY_INDEX_SCALE
,然後當我確定我從陣列第5個位置作為該陣列的開始位置操作時,我就可以使用 this.address = ARRAY_BASE_OFFSET + off * ARRAY_INDEX_SCALE
。 我們再通過下面的原始碼對上述內容對比消化下:
//java.nio.HeapByteBuffer protected HeapByteBuffer(byte[] buf, int mark, int pos, int lim, int cap, int off) { super(mark, pos, lim, cap, buf, off); /* hb = buf; offset = off; */ this.address = ARRAY_BASE_OFFSET + off * ARRAY_INDEX_SCALE; } public ByteBuffer slice() { return new HeapByteBuffer(hb, -1, 0, this.remaining(), this.remaining(), this.position() + offset); } ByteBuffer slice(int pos, int lim) { assert (pos >= 0); assert (pos <= lim); int rem = lim - pos; return new HeapByteBuffer(hb, -1, 0, rem, rem, pos + offset); } public ByteBuffer duplicate() { return new HeapByteBuffer(hb, this.markValue(), this.position(), this.limit(), this.capacity(), offset); } 複製程式碼
Buffer的讀寫
每個 buffer
都是可讀的,但不是每個 buffer
都是可寫的。這裡,當 buffer
有內容變動的時候,會首先呼叫 buffer
的 isReadOnly
判斷此 buffer
是否只讀,只讀 buffer
是不允許更改其內容的,但 mark
、 position
和 limit
的值是可變的,這是我們人為給其額外的定義,方便我們增加功能邏輯的。當在只讀 buffer
上呼叫修改時,則會丟擲 ReadOnlyBufferException
異常。我們來看 buffer
的 put
方法:
//java.nio.ByteBuffer#put(java.nio.ByteBuffer) public ByteBuffer put(ByteBuffer src) { if (src == this) throw createSameBufferException(); if (isReadOnly()) throw new ReadOnlyBufferException(); int n = src.remaining(); if (n > remaining()) throw new BufferOverflowException(); for (int i = 0; i < n; i++) put(src.get()); return this; } //java.nio.Buffer#remaining public final int remaining() { return limit - position; } 複製程式碼
上面 remaining
方法表示還剩多少資料未讀,上面的原始碼講的是,如果 src
這個 ByteBuffer
的 src.remaining()
的數量大於要存放的目標 Buffer
的還剩的空間,直接拋溢位的異常。然後通過一個for迴圈,將 src
剩餘的資料,依次寫入目標 Buffer
中。接下來,我們通過 src.get()
來探索下 Buffer
的讀操作。
//java.nio.HeapByteBuffer#get() public byte get() { return hb[ix(nextGetIndex())]; } public byte get(int i) { return hb[ix(checkIndex(i))]; } //java.nio.HeapByteBuffer#ix protected int ix(int i) { return i + offset; } //java.nio.Buffer#nextGetIndex() final int nextGetIndex() { if (position >= limit) throw new BufferUnderflowException(); return position++; } 複製程式碼
這裡,為了依次讀取陣列中的資料,這裡使用 nextGetIndex()
來獲取要讀位置,即先返回當前要獲取的位置值,然後position自己再加1。以此在前面 ByteBuffer#put(java.nio.ByteBuffer)
所示原始碼中的 for
迴圈中依次對剩餘資料的讀取。上述 get(int i)
不過是從指定位置獲取資料,實現也比較簡單 HeapByteBuffer#ix
也只是確定所要獲取此陣列物件指定位置資料,其中的 offset
表示第一個 可讀 位元組在該位元組陣列中的位置(就好比我喝茶杯底三分之一水是不喝的,每次都從三分之一水量開始位置計算喝了多少或者加入多少水)。 接下來看下單個位元組儲存到指定位元組陣列的操作,與獲取位元組陣列單個位置資料相對應,程式碼比較簡單:
//java.nio.HeapByteBuffer#put(byte) public ByteBuffer put(byte x) { hb[ix(nextPutIndex())] = x; return this; } public ByteBuffer put(int i, byte x) { hb[ix(checkIndex(i))] = x; return this; } //java.nio.Buffer#nextPutIndex() final int nextPutIndex() {// package-private if (position >= limit) throw new BufferOverflowException(); return position++; } 複製程式碼
前面的都是單個位元組的,下面來講下批量操作位元組陣列是如何進行的,因過程知識點重複,這裡只講get,先看原始碼:
//java.nio.ByteBuffer#get(byte[]) public ByteBuffer get(byte[] dst) { return get(dst, 0, dst.length); } //java.nio.ByteBuffer#get(byte[], int, int) public ByteBuffer get(byte[] dst, int offset, int length) { // 檢查引數是否越界 checkBounds(offset, length, dst.length); // 檢查要獲取的長度是否大於Buffer中剩餘的資料長度 if (length > remaining()) throw new BufferUnderflowException(); int end = offset + length; for (int i = offset; i < end; i++) dst[i] = get(); return this; } //java.nio.Buffer#checkBounds static void checkBounds(int off, int len, int size) { // package-private if ((off | len | (off + len) | (size - (off + len))) < 0) throw new IndexOutOfBoundsException(); } 複製程式碼
通過這個方法將這個buffer中的位元組資料讀到我們給定的目標陣列dst中,由checkBounds可知,當要寫入目標位元組陣列的可寫長度小於將要寫入資料的長度的時候,會產生邊界異常。當要獲取的長度是大於Buffer中剩餘的資料長度時丟擲 BufferUnderflowException
異常,當驗證通過後,接著就從目標陣列的 offset
位置開始,從 buffer
獲取並寫入 offset + length
長度的資料。 可以看出, HeapByteBuffer
是封裝了對byte陣列的簡單操作。對緩衝區的寫入和讀取本質上是對陣列的寫入和讀取。使用 HeapByteBuffer
的好處是我們不用做各種引數校驗,也不需要另外維護陣列當前讀寫位置的變量了。 同時我們可以看到, Buffer
中對 position
的操作沒有使用鎖保護,所以 Buffer
不是執行緒安全的。如果我們操作的這個 buffer
會有多個執行緒使用,則針對該 buffer
的訪問應通過適當的同步控制機制來進行保護。
ByteBuffer的模式
jdk本身是沒這個說法的,只是按照我們自己的操作習慣,我們將 Buffer
分為兩種工作模式,一種是接收資料模式,一種是輸出資料模式。我們可以通過 Buffer
提供的 flip
等操作來切換 Buffer
的工作模式。
我們來新建一個容量為10的 ByteBuffer
:
ByteBuffer.allocate(10); 複製程式碼
由前面所學的 HeapByteBuffer
的構造器中的相關程式碼可知,這裡的 position
被設定為0,而且 capacity
和 limit
設定為 10, mark
設定為-1, offset
設定為0。 可參考下圖展示:

新建的 Buffer
處於接收資料的模式,可以向 Buffer
放入資料,在放入一個對應基本型別的資料後(此處假如放入一個char型別資料),position加一,參考我們上面所示原始碼,如果position已經等於limit了還進行 put
操作,則會丟擲 BufferOverflowException
異常。 我們向所操作的buffer中put 5個char型別的資料進去:
buffer.put((byte)'a').put((byte)'b').put((byte)'c').put((byte)'d').put((byte)'e'); 複製程式碼
會得到如下結果檢視:

由之前原始碼分析可知,Buffer的讀寫的位置變數都是基於 position
來做的,其他的變數都是圍繞著它進行輔助管理的,所以如果從 Buffer
中讀取資料,要將 Buffer
切換到輸出資料模式(也就是讀模式)。此時,我們就可以使用 Buffer
提供了flip方法。
//java.nio.Buffer#flip public Buffer flip() { limit = position; position = 0; mark = -1; return this; } 複製程式碼
我們知道,在put的時候,會進行 java.nio.Buffer#nextPutIndex()
的呼叫,裡面會進行 position >= limit
,所以,此時再進行寫操作的話,會從第0個位置開始進行覆蓋,而且只能寫到 flip
操作之後 limit
的位置。
//java.nio.Buffer#nextPutIndex() final int nextPutIndex() {// package-private if (position >= limit) throw new BufferOverflowException(); return position++; } 複製程式碼
在做完 put
操作後, position
會自增一下,所以, flip
操作示意圖如下:

也是因為 position
為0了,所以我們可以很方便的從Buffer中第0個位置開始讀取資料,不需要別的附加操作。由之前解讀可知,每次讀取一個元素, position
就會加一,如果 position
已經等於 limit
還進行讀取,則會丟擲 BufferUnderflowException
異常。
我們通過 flip
方法把 Buffer
從接收寫模式切換到輸出讀模式,如果要從輸出模式切換到接收模式,可以使用 compact
或者 clear
方法,如果資料已經讀取完畢或者資料不要了,使用 clear
方法,如果只想從緩衝區中釋放一部分資料,而不是全部(即釋放已讀資料,保留未讀資料),然後重新填充,使用 compact
方法。
對於 clear
方法,我們先來看它的原始碼:
//java.nio.Buffer#clear public Buffer clear() { position = 0; limit = capacity; mark = -1; return this; } 複製程式碼
我們可以看到,它的 clear
方法內並沒有做清理工作,只是修改位置變數,重置為初始化時的狀態,等待下一次將資料寫入緩衝陣列。 接著,來看 compact
操作的原始碼:
//java.nio.HeapByteBuffer#compact public ByteBuffer compact() { System.arraycopy(hb, ix(position()), hb, ix(0), remaining()); position(remaining()); limit(capacity()); discardMark(); return this; } //java.nio.ByteBuffer#position ByteBuffer position(int newPosition) { super.position(newPosition); return this; } //java.nio.Buffer#position(int) public Buffer position(int newPosition) { if (newPosition > limit | newPosition < 0) throw createPositionException(newPosition); position = newPosition; if (mark > position) mark = -1; return this; } //java.nio.ByteBuffer#limit ByteBuffer limit(int newLimit) { super.limit(newLimit); return this; } //java.nio.Buffer#limit(int) public Buffer limit(int newLimit) { if (newLimit > capacity | newLimit < 0) throw createLimitException(newLimit); limit = newLimit; if (position > limit) position = limit; if (mark > limit) mark = -1; return this; } //java.nio.Buffer#discardMark final void discardMark() { mark = -1; } 複製程式碼
這裡使用了陣列的拷貝操作,將未讀元素轉移到該位元組陣列從 0 開始的位置,由於 remaining()
返回的是 limit - position
,假如在 flip
操作的時候填入的元素有 5 個,那麼 limit
為 5 ,此時讀到了第三個元素,也就是在呼叫 compact
時 position
的數值為 2 ,那 remaining()
的值就為 3 ,也就是此時 position
為 3 , compact
操作後, limit
會迴歸到和初始化陣列容量大小一樣,並將 mark 值置為 -1 。
我們來看示意圖,在進行 buffer.compact()
呼叫前:

buffer.compact()
呼叫後:

ByteBuffer的其他方法
接下來,我們再接觸一些 ByteBuffer
的其他方法,方便在適當的條件下進行使用。
rewind方法
首先來看它的原始碼:
//java.nio.Buffer#rewind public Buffer rewind() { position = 0; mark = -1; return this; } 複製程式碼
這裡就是將 position
設定為0, mark
設定為-1,其他設定的管理屬性( capacity
, limit
)不變。結合前面的知識,在位元組陣列寫入資料後,它的 clear
方法也只是重置我們在 Buffer
中設定的那幾個增強管理屬性( capacity
、 position
、 limit
、 mark
),此處的英文表達的意思也很明顯: 倒帶 ,也就是可以回頭重新寫,或者重新讀。但是我們要注意一個前提,我們要確保已經恰當的設定了 limit
。這個方法可以在 Channel
的讀或者寫之前呼叫,如:
out.write(buf);// Write remaining data buf.rewind();// Rewind buffer buf.get(array);// Copy data into array 複製程式碼
我們通過下圖來進行展示執行 rewind
操作後的結果:

duplicate 方法
在JDK9版本中,新增了這個方法。用來建立一個與原始 Buffer
一樣的新 Buffer
。新 Buffer
的內容和原始 Buffer
一樣。改變新 Buffer
內的資料,同樣會體現在原始 Buffer
上,反之亦然。兩個 Buffer
都擁有自己獨立的 position
, limit
和 mark
屬性。 剛建立的新 Buffer
的 position
, limit
和 mark
屬性與原始 Buffer
對應屬性的值相同。 還有一點需要注意的是,在 HeapByteBuffer
與 DirectByteBuffer
對此方法的實現是不同的,對於 HeapByteBuffer
來說,如果原始 Buffer
是隻讀的(即 HeapByteBufferR
),那麼新 Buffer
也是隻讀的。如果原始 Buffer
是 DirectByteBuffer
,那新 Buffer
也是 DirectByteBuffer
。 我們來看相關原始碼實現:
//java.nio.HeapByteBuffer#duplicate public ByteBuffer duplicate() { return new HeapByteBuffer(hb, this.markValue(), this.position(), this.limit(), this.capacity(), offset); } //java.nio.HeapByteBufferR#duplicate public ByteBuffer duplicate() { return new HeapByteBufferR(hb, this.markValue(), this.position(), this.limit(), this.capacity(), offset); } //java.nio.DirectByteBuffer#duplicate public ByteBuffer duplicate() { return new DirectByteBuffer(this, this.markValue(), this.position(), this.limit(), this.capacity(), 0); } 複製程式碼
基本型別的引數傳遞都是值傳遞,所以由上面原始碼可知每個新緩衝區都擁有自己的 position
、 limit
和 mark
屬性,而且他們的初始值使用了原始 Buffer
此時的值。 但是,從 HeapByteBuffer
角度來說,對於 hb 作為一個數組物件,屬於物件引用傳遞,即新老 Buffer
共用了同一個位元組陣列物件。無論誰操作,都會改變另一個。 從 DirectByteBuffer
角度來說,直接記憶體看重的是地址操作,所以,其在建立這個新 Buffer
的時候傳入的是原始 Buffer
的引用,進而可以獲取到相關地址。
asReadOnlyBuffer
可以使用 asReadOnlyBuffer()
方法來生成一個只讀的緩衝區。這與 duplicate()
實現有些相同,除了這個新的緩衝區不允許使用 put()
,並且其 isReadOnly()
函式 將會返回true 。 對這一隻讀緩衝區呼叫 put()
操作,會導致 ReadOnlyBufferException
異常。 我們來看相關原始碼:
//java.nio.ByteBuffer#put(java.nio.ByteBuffer) public ByteBuffer put(ByteBuffer src) { if (src == this) throw createSameBufferException(); if (isReadOnly()) throw new ReadOnlyBufferException(); int n = src.remaining(); if (n > remaining()) throw new BufferOverflowException(); for (int i = 0; i < n; i++) put(src.get()); return this; } //java.nio.HeapByteBuffer#asReadOnlyBuffer public ByteBuffer asReadOnlyBuffer() { return new HeapByteBufferR(hb, this.markValue(), this.position(), this.limit(), this.capacity(), offset); } //java.nio.HeapByteBufferR#asReadOnlyBuffer //HeapByteBufferR下直接呼叫其duplicate方法即可,其本來就是隻讀的 public ByteBuffer asReadOnlyBuffer() { return duplicate(); } //java.nio.DirectByteBuffer#asReadOnlyBuffer public ByteBuffer asReadOnlyBuffer() { return new DirectByteBufferR(this, this.markValue(), this.position(), this.limit(), this.capacity(), 0); } //java.nio.DirectByteBufferR#asReadOnlyBuffer public ByteBuffer asReadOnlyBuffer() { return duplicate(); } //java.nio.HeapByteBufferR#HeapByteBufferR protected HeapByteBufferR(byte[] buf, int mark, int pos, int lim, int cap, int off) { super(buf, mark, pos, lim, cap, off); this.isReadOnly = true; } //java.nio.DirectByteBufferR#DirectByteBufferR DirectByteBufferR(DirectBuffer db, int mark, int pos, int lim, int cap, int off) { super(db, mark, pos, lim, cap, off); this.isReadOnly = true; } 複製程式碼
可以看到, ByteBuffer
的只讀實現,在構造器裡首先將 isReadOnly
屬性設定為 true
。接著, HeapByteBufferR
繼承了 HeapByteBuffer
類( DirectByteBufferR
也是類似實現,就不重複了),並重寫了所有可對buffer修改的方法。把所有能修改 buffer
的方法都直接丟擲ReadOnlyBufferException來保證只讀。來看 DirectByteBufferR
相關原始碼,其他對應實現一樣:
//java.nio.DirectByteBufferR#put(byte) public ByteBuffer put(byte x) { throw new ReadOnlyBufferException(); } 複製程式碼
slice 方法
slice
從字面意思來看,就是 切片 ,用在這裡,就是分割 ByteBuffer
。即建立一個從原始 ByteBuffer
的當前位置( position
)開始的新 ByteBuffer
,並且其容量是原始 ByteBuffer
的剩餘消費元素數量( limit-position
)。這個新 ByteBuffer
與原始 ByteBuffer
共享一段資料元素子序列,也就是設定一個offset值,這樣就可以將一個相對陣列第三個位置的元素看作是起點元素,此時新 ByteBuffer
的 position
就是0,讀取的還是所傳入這個 offset
的所在值。分割出來的 ByteBuffer
也會繼承只讀和直接屬性。 我們來看相關原始碼:
//java.nio.HeapByteBuffer#slice() public ByteBuffer slice() { return new HeapByteBuffer(hb, -1, 0, this.remaining(), this.remaining(), this.position() + offset); } protected HeapByteBuffer(byte[] buf, int mark, int pos, int lim, int cap, int off) { super(mark, pos, lim, cap, buf, off); /* hb = buf; offset = off; */ this.address = ARRAY_BASE_OFFSET + off * ARRAY_INDEX_SCALE; } 複製程式碼
由原始碼可知,新 ByteBuffer
和原始 ByteBuffer
共有了一個數組,新 ByteBuffer
的 mark
值為-1, position
值為0, limit
和 capacity
都為原始 Buffer
中 limit-position
的值。 於是,我們可以通過下面兩幅圖來展示 slice
方法前後的對比。
原始 ByteBuffer
:

呼叫 slice
方法分割後得到的新 ByteBuffer
:

本篇到此為止,在下一篇中,我會著重講下 DirectByteBuffer
的實現細節。
本文參考及圖片來源: www.jianshu.com/p/12c81abb5…