前言
此係列文章會詳細解讀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
對應屬性的值相同。
還有一點需要注意的是,如果原始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…