1. 程式人生 > >Java NIO筆記(二):NIO Buffer(緩衝區)之基礎

Java NIO筆記(二):NIO Buffer(緩衝區)之基礎

        緩衝區(Buffer)就是在記憶體中預留指定位元組數的儲存空間用來對輸入/輸出(I/O)的資料作臨時儲存,這部分預留的記憶體空間就叫做緩衝區;在Java NIO中,緩衝區的作用也是用來臨時儲存資料,可以理解為是I/O操作中資料的中轉站。緩衝區直接為通道(Channel)服務,寫入資料到通道或從通道讀取資料,這樣的操利用緩衝區資料來傳遞就可以達到對資料高效處理的目的。在NIO中主要有八種緩衝區類(其中MappedByteBuffer是專門用於記憶體對映的一種ByteBuffer):

緩衝區是包在一個物件內的基礎資料的陣列,Buffer類相比一般簡單陣列而言其優點是將資料的內容和相關資訊放在一個物件裡面,這個物件提供了處理緩衝區資料的豐富的API。

所有緩衝區都有4個屬性:capacity、limit、position、mark,並遵循:capacity>=limit>=position>=mark>=0,下表格是對著4個屬性的解釋:

屬性 描述
Capacity 容量,即可以容納的最大資料量;在緩衝區建立時被設定並且不能改變
Limit 上界,緩衝區中當前資料量
Position 位置,下一個要被讀或寫的元素的索引
Mark 標記,呼叫mark()來設定mark=position,再呼叫reset()可以讓position恢復到標記的位置即position=mark

一、建立緩衝區

        所有的緩衝區類都不能直接使用new關鍵字例項化,它們都是抽象類,但是它們都有一個用於建立相應例項的靜態工廠方法,以ByteBuffer類為例子:

		//建立一個容量為10的byte緩衝區
		ByteBuffer buff = ByteBuffer.allocate(10);

上面程式碼將會從堆空間中分配一個容量大小為10的byte陣列作為緩衝區的byte資料儲存器。對於其他緩衝區類上面方式也適用,如建立容量為10的CharBuffer:
		//建立一個容量為10的char緩衝區
		CharBuffer buff = CharBuffer.allocate(10);

        如果想用一個指定大小的陣列作為緩衝區的資料的儲存器,可以使用wrap()方法:
		//使用一個指定陣列作為緩衝區的儲存器
		byte[] bytes = new byte[10];
		ByteBuffer buff = ByteBuffer.wrap(bytes);
上面程式碼中緩衝區的資料會存放在bytes陣列中,bytes陣列或buff緩衝區任何一方中資料的改動都會影響另一方。還可以建立指定初始位置(position)和上界(limit)的緩衝區:
		//使用一個指定陣列作為緩衝區的儲存器
		//並建立一個position=3,limit=8,capacity=10的緩衝區
		byte[] bytes = new byte[10];
		ByteBuffer buff = ByteBuffer.wrap(bytes, 3, 8);
下圖是新建立的一個容量為10的位元組緩衝區的記憶體圖:


二、操作緩衝區

1、存取(Buffer.get() & Buffer.put())

        使用get()從緩衝區中取資料,使用put()向緩衝區中存資料。

		// 建立一個容量為10的byte資料緩衝區
		ByteBuffer buff = ByteBuffer.allocate(10);
		// 存入4次資料
		buff.put((byte) 'A');
		buff.put((byte) 'B');
		buff.put((byte) 'C');
		buff.put((byte) 'D');
		// 翻轉緩衝區
		buff.flip();
		// 讀取2次資料
		System.out.println((char)buff.get());
		System.out.println((char)buff.get());

        上面有提過緩衝區四個屬性值一定遵循capacity>=limit>=position>=mark>=0,put()時,若position超過limit則會丟擲BufferOverflowException;get()時,若position超過limit則會丟擲BufferUnderflowException。

        buff.flip()是將緩衝區翻轉,翻轉將在下面來說。

呼叫put()或get()時,每呼叫一次position的值會加1,指示下次存或取開始的位置;

上面程式碼put()四次後的緩衝區記憶體示意圖:


上面程式碼執行buff.flip()將緩衝區翻轉後的記憶體示意圖:


上面程式碼執兩次get()後的緩衝區記憶體示意圖:

        再向Buffer中讀寫資料時有2個方法也非常有用:

Buffer.remaining():返回從當前位置到上界的資料元素數量;

Buffer.hasRemaining():告訴我們從當前位置到上界是否有資料元素;

2、翻轉(Buffer.flip())

        翻轉就是將一個處於存資料狀態的緩衝區變為一個處於準備取資料的狀態,使用flip()方式實現翻轉。Buffer.flip()的原始碼如下:

	public final Buffer flip() {
		limit = position;
		position = 0;
		mark = -1;
		return this;
	}
相信看到了實現的原始碼應該就會清楚flip()的作用了。rewind()方法與flip()很相似,區別在於rewind()不會影響limit,而flip()會重設limit屬性值,Buffer.rewind()的原始碼如下:
	public final Buffer rewind() {
		position = 0;
		mark = -1;
		return this;
	}

3、壓縮(Buffer.compact())

        壓縮就是將已讀取了的資料丟棄,保留未讀取的資料並將保留的資料重新填充到緩衝區的頂部,然後繼續向緩衝區寫入資料。

		// 建立一個容量為10的byte資料緩衝區
		ByteBuffer buff = ByteBuffer.allocate(10);
		// 填充緩衝區
		buff.put((byte)'A');
		buff.put((byte)'B');
		buff.put((byte)'C');
		buff.put((byte)'D');
		System.out.println("first put : " + new String(buff.array()));
		//翻轉
		buff.flip();
		//釋放
		System.out.println((char)buff.get());
		System.out.println((char)buff.get());
		//壓縮
		buff.compact();
		System.out.println("compact after get : " + new String(buff.array()));
		//繼續填充
		buff.put((byte)'E');
		buff.put((byte)'F');
		//輸出所有
		System.out.println("put after compact : " + new String(buff.array()));

以上程式碼列印結果:

first put : ABCD

A

B

compact after get : CDCD

put after compact : CDEF

控制檯中輸出內容中有正方形的亂碼,是正常。因為位元組緩衝區中沒有賦值的記憶體塊預設值是0,而Unicode編碼中沒有0編碼,所以亂碼。

4、標記(Buffer.mark())

        標記就是記住當前位置(使用mark()方法標記),之後可以將位置恢復到標記處(使用reset()方法恢復),mark()和reset()原始碼如下:

	public final Buffer mark() {
		mark = position;
		return this;
	}

	public final Buffer reset() {
		int m = mark;
		if (m < 0)
			throw new InvalidMarkException();
		position = m;
		return this;
	}
5、比較兩個緩衝區是否相等

        比較兩個緩衝區是否相等有2種方法:equals(Object ob) 和compareTo(ByteBuffer that),這兩個方法都是在Buffer的子類中實現的。

        equals比較的兩個緩衝區中的每個值,所以允許不同的Buffer物件進行比較;compareTo有型別限制,ByteBuffer只能和ByteBuffer進行比較;比較兩個緩衝區實際上是比較兩個緩衝區中每個緩衝區position到limit之間(不包括limit)的緩衝值。如下圖:

6、批量移動緩衝區的資料

        緩衝區的目的就是高效傳輸資料,高效傳輸資料就應杜絕一個一個的傳輸,所以Buffer API提供了相應的方法來進行批量移動。下面是個例子:

		byte[] bytes = "hello world!".getBytes();
		// 建立一個容量等bytes容量的byte資料緩衝區
		ByteBuffer buff = ByteBuffer.allocate(bytes.length);
		//將byte資料寫入緩衝區,下面程式碼和buff.put(bytes)效果一致
		buff.put(bytes, 0, bytes.length);
		//翻轉緩衝區
		buff.flip();
		//輪詢判斷是否有資料,有則將緩衝區的資料批量讀到array中
		byte[] array = 	new byte[bytes.length];
		while(buff.hasRemaining()){
			buff.get(array, 0, buff.remaining());
		}
		//輸出衝緩衝區讀出來的資料
		System.out.println(new String(array));
以上面程式碼為例,

寫資料到緩衝區時,若bytes.length > buff.capacity()則會丟擲java.nio.BufferOverflowException;

從緩衝區中讀資料時,若array.length < buff.limit()則會丟擲java.lang.IndexOutOfBoundsException。

7、複製緩衝區

        複製一個與源緩衝區共享資料的緩衝區,各自管理自己的屬性

  • asReadOnlyBuffer():複製一個只讀緩衝區
  • duplicate():複製一個可讀可寫的緩衝區
  • slice():複製一個從源緩衝position到limit的新緩衝區