NIO之緩沖區(qū)詳解
點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)

作者 | 汪偉俊
出品 | Java技術(shù)迷(ID:JavaFans1024)
先來(lái)介紹NIO中的一個(gè)基本概念,緩沖區(qū)。在NIO中,任何數(shù)據(jù)的讀寫都需要借助緩沖區(qū),你可以把緩沖區(qū)理解成一個(gè)數(shù)組,當(dāng)要寫入數(shù)據(jù)時(shí)就向數(shù)組中存值,當(dāng)要讀取數(shù)據(jù)時(shí)就從數(shù)組中取值。
創(chuàng)建緩沖區(qū)
對(duì)于緩沖區(qū)的創(chuàng)建,JDK提供了兩種方式(以字節(jié)緩沖區(qū)ByteBuffer為例):
allocate wrap
其中allocate用于創(chuàng)建一個(gè)指定大小的緩沖區(qū):
@Test
public void buffer(){
// 指定長(zhǎng)度的緩沖區(qū)
ByteBuffer byteBuffer = ByteBuffer.allocate(5);
for (int i = 0; i < 5; i++) {
// 從緩沖區(qū)中獲取數(shù)據(jù)
System.out.print(byteBuffer.get() + "\t");
}
}
運(yùn)行結(jié)果:
0 0 0 0 0
通過(guò)緩沖區(qū)的get方法可以獲取緩沖區(qū)中的數(shù)據(jù),初始為數(shù)據(jù)類型的零值。
而wrap方法可以創(chuàng)建一個(gè)指定內(nèi)容的緩沖區(qū):
@Test
public void buffer(){
// 指定內(nèi)容的緩沖區(qū)
ByteBuffer wrap = ByteBuffer.wrap("test".getBytes());
for (int i = 0; i < 4; i++) {
System.out.print((char) wrap.get() + "\t");
}
}
運(yùn)行結(jié)果:
t e s t
那么緩沖區(qū)內(nèi)部的具體結(jié)構(gòu)是如何的呢?數(shù)據(jù)的存取是怎樣進(jìn)行的呢?你需要了解緩沖區(qū)中的幾個(gè)標(biāo)記:
position:當(dāng)前索引位置 limit:最大索引位置 capacity:緩沖區(qū)的總?cè)萘?/section> remaining:緩沖區(qū)的剩余容量
來(lái)創(chuàng)建一個(gè)容量為10的緩沖區(qū),然后分別輸出這些標(biāo)記的值:
@Test
public void buffer(){
ByteBuffer allocate = ByteBuffer.allocate(10);
System.out.print(allocate.position() + "\t"); // 當(dāng)前索引位置
System.out.print(allocate.limit() + "\t"); // 最大索引位置,初始等于緩沖區(qū)大小
System.out.print(allocate.capacity() + "\t"); // 返回緩沖區(qū)的總長(zhǎng)度
System.out.print(allocate.remaining() + "\t"); // 剩余能操作的容量(limit - position)
}
運(yùn)行結(jié)果:
0 10 10 10
每個(gè)標(biāo)記的位置如下圖所示:

position指向的是當(dāng)前索引位置,當(dāng)向緩沖區(qū)中添加數(shù)據(jù)時(shí),position便會(huì)隨之移動(dòng),而limit指向的是最大索引位置(初始等于capacity),即position最大不會(huì)等于limit,remaining為緩沖區(qū)的剩余容量,remaining = limit - position。
向緩沖區(qū)添加數(shù)據(jù)
現(xiàn)在向緩沖區(qū)添加一個(gè)數(shù)據(jù):
// 向緩沖區(qū)添加一個(gè)字節(jié)
allocate.put((byte) 97);
此時(shí)緩沖區(qū)標(biāo)記會(huì)如何變化呢?首先position會(huì)右移一位,然后remaining變?yōu)?,其它的不影響,如下圖所示:

我們可以試驗(yàn)一下是不是這樣:
@Test
public void buffer(){
ByteBuffer allocate = ByteBuffer.allocate(10);
// 向緩沖區(qū)添加一個(gè)字節(jié)
allocate.put((byte) 97);
System.out.print(allocate.position() + "\t");
System.out.print(allocate.limit() + "\t");
System.out.print(allocate.capacity() + "\t");
System.out.print(allocate.remaining() + "\t");
}
運(yùn)行結(jié)果:
1 10 10 9
結(jié)果正如我們所料。
緩沖區(qū)的put方法還能夠傳遞一個(gè)數(shù)組,將一串?dāng)?shù)據(jù)進(jìn)行添加:
// 向緩沖區(qū)添加一個(gè)字節(jié)
allocate.put("0123456789".getBytes());
若是當(dāng)前緩沖區(qū)已經(jīng)滿了,則再向一個(gè)滿的緩沖區(qū)添加數(shù)據(jù)會(huì)拋出異常:
@Test
public void buffer(){
ByteBuffer allocate = ByteBuffer.allocate(10);
allocate.put("0123456789".getBytes());
allocate.put((byte) 1);
}
運(yùn)行結(jié)果:
java.nio.BufferOverflowException
at java.nio.Buffer.nextPutIndex(Buffer.java:521)
at java.nio.HeapByteBuffer.put(HeapByteBuffer.java:169)
at com.wwj.nio.BufferDemo.buffer(BufferDemo.java:144)
通過(guò)緩沖區(qū)的hasRemaining方法可以判斷當(dāng)前緩沖區(qū)是否還能夠繼續(xù)添加數(shù)據(jù):
@Test
public void buffer(){
ByteBuffer allocate = ByteBuffer.allocate(10);
System.out.println(allocate.hasRemaining());
allocate.put("0123456789".getBytes());
System.out.println(allocate.hasRemaining());
}
運(yùn)行結(jié)果:
true
false
緩沖區(qū)支持動(dòng)態(tài)修改標(biāo)記位置,以達(dá)到重新寫入的需求:
@Test
public void buffer(){
ByteBuffer allocate = ByteBuffer.allocate(10);
allocate.put("0123456789".getBytes());
// 修改當(dāng)前索引位置
allocate.position(0);
allocate.put((byte) 1);
System.out.print(allocate.position() + "\t");
System.out.print(allocate.limit() + "\t");
System.out.print(allocate.capacity() + "\t");
System.out.print(allocate.remaining() + "\t");
}
運(yùn)行結(jié)果:
1 10 10 9
把position位置修改為0之后,又相當(dāng)于對(duì)一個(gè)空的緩沖區(qū)進(jìn)行操作了。
讀取緩沖區(qū)數(shù)據(jù)
接下來(lái)介紹一下緩沖區(qū)數(shù)據(jù)的讀取,在最開(kāi)始我們已經(jīng)使用過(guò)get方法來(lái)讀取緩沖區(qū)的數(shù)據(jù)了,如下:
@Test
public void buffer() {
ByteBuffer allocate = ByteBuffer.allocate(10);
allocate.put("0123".getBytes());
for (int i = 0; i < 4; i++) {
System.out.print(allocate.get() + "\t");
}
}
大家可以猜一猜運(yùn)行結(jié)果是什么呢:
0 0 0 0
也許有同學(xué)很奇怪,為什么添加的數(shù)據(jù)沒(méi)有被讀取出來(lái),其實(shí),如果你掌握了緩沖區(qū)中的標(biāo)記,就能明白是為什么。
在創(chuàng)建了一個(gè)容量為10的緩沖區(qū)之后,標(biāo)記如下圖所示:

當(dāng)向緩沖區(qū)添加了一個(gè)字節(jié)數(shù)組后,標(biāo)記發(fā)生了變化:

此時(shí)我們調(diào)用get方法進(jìn)行讀取,它將從position位置也就是索引4位置開(kāi)始往后讀取,這樣讀取到的數(shù)據(jù)當(dāng)然就是0了,若是想讀取添加到緩沖區(qū)中的數(shù)據(jù),則需要將position移動(dòng)到索引0位置才行,不過(guò)JDK已經(jīng)提供了這樣的方法給我們:
@Test
public void buffer() {
ByteBuffer allocate = ByteBuffer.allocate(10);
allocate.put("0123".getBytes());
// 切換為讀模式
allocate.flip();
for (int i = 0; i < allocate.limit(); i++) {
System.out.print((char) allocate.get() + "\t");
}
}
運(yùn)行結(jié)果:
0 1 2 3
查看一下flip方法的源碼:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
關(guān)鍵就在于limit = position和position = 0,通過(guò)改變這兩個(gè)標(biāo)記后:

position重新回到了索引0的位置,這樣就可以進(jìn)行正常的讀取了,而limit也修改為了寫入數(shù)據(jù)的末尾位置,可以通過(guò)判斷l(xiāng)imit來(lái)終止讀取條件。
與寫入數(shù)據(jù)一樣,緩沖區(qū)在讀取數(shù)據(jù)的時(shí)候,也會(huì)不停地移動(dòng)position,當(dāng)所有數(shù)據(jù)都被讀取后,再次讀取數(shù)據(jù)將會(huì)拋出異常,因?yàn)閜osition必須小于等于limit:
@Test
public void buffer() {
ByteBuffer allocate = ByteBuffer.allocate(10);
allocate.put("0123".getBytes());
// 切換為讀模式
allocate.flip();
for (int i = 0; i < allocate.limit(); i++) {
System.out.print((char) allocate.get() + "\t");
}
allocate.get();
}
運(yùn)行結(jié)果:
0 1 2 3
java.nio.BufferUnderflowException
at java.nio.Buffer.nextGetIndex(Buffer.java:500)
at java.nio.HeapByteBuffer.get(HeapByteBuffer.java:135)
at com.wwj.nio.BufferDemo.buffer(BufferDemo.java:148)
但是通過(guò)索引讀取數(shù)據(jù)將不會(huì)判斷position是否小于等于limit,也不會(huì)移動(dòng)position:
@Test
public void buffer() {
ByteBuffer allocate = ByteBuffer.allocate(10);
allocate.put("0123".getBytes());
// 切換為讀模式
allocate.flip();
for (int i = 0; i < allocate.limit(); i++) {
System.out.print((char) allocate.get() + "\t");
}
// 通過(guò)索引讀取數(shù)據(jù)
System.out.println((char) allocate.get(1));
}
運(yùn)行結(jié)果:
0 1 2 3 1
數(shù)據(jù)讀取完畢后,若是想重新對(duì)該緩沖區(qū)進(jìn)行讀取,則可以將position手動(dòng)置為0,也可以調(diào)用JDK提供的方法:
// 調(diào)用rewind方法可以將當(dāng)前索引重置為0
allocate.rewind();
rewind方法內(nèi)部也是對(duì)position進(jìn)行賦值為0的操作:
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
若是想重新對(duì)緩沖區(qū)進(jìn)行寫入,則調(diào)用clear方法:
// 切換寫模式,此時(shí)會(huì)將當(dāng)前索引置為0,將最大索引置為緩沖區(qū)容量
allocate.clear();
注意rewind方法和clear方法的區(qū)別,它們雖然都會(huì)將position置為0,但是clear方法還會(huì)將limit置為capacity的值,所以當(dāng)想要再次讀取緩沖區(qū)中的數(shù)據(jù)時(shí),則可以調(diào)用rewind方法;當(dāng)想要再次寫入數(shù)據(jù)到緩沖區(qū)時(shí),則可以調(diào)用clear方法。
來(lái)驗(yàn)證一下:
@Test
public void buffer() {
ByteBuffer allocate = ByteBuffer.allocate(10);
allocate.put("0123".getBytes());
// 切換為讀模式
allocate.flip();
for(int i = 0;i <allocate.limit();++i){
allocate.get();
}
System.out.print("position:" + allocate.position() + "\t");
System.out.print("limit:" + allocate.limit() + "\t");
System.out.print("capacity:" + allocate.capacity() + "\t");
System.out.print("remaining:" + allocate.remaining() + "\t");
System.out.println();
allocate.rewind();
System.out.print("position:" + allocate.position() + "\t");
System.out.print("limit:" + allocate.limit() + "\t");
System.out.print("capacity:" + allocate.capacity() + "\t");
System.out.print("remaining:" + allocate.remaining() + "\t");
System.out.println();
allocate.clear();
System.out.print("position:" + allocate.position() + "\t");
System.out.print("limit:" + allocate.limit() + "\t");
System.out.print("capacity:" + allocate.capacity() + "\t");
System.out.print("remaining:" + allocate.remaining() + "\t");
}
運(yùn)行結(jié)果:
position:4 limit:4 capacity:10 remaining:0
position:0 limit:4 capacity:10 remaining:4
position:0 limit:10 capacity:10 remaining:10
往 期 推 薦
點(diǎn)分享
點(diǎn)收藏
點(diǎn)點(diǎn)贊
點(diǎn)在看





