Java - NIO之Buffer(上)

襲冷發表於2014-05-25

一、關於Buffer

    Java NIO中的Buffer用於和NIO通道進行互動。如你所知,資料是從通道讀入緩衝區,從緩衝區寫入到通道中的。

    緩衝區本質上是一塊可以寫入資料,然後可以從中讀取資料的記憶體。這塊記憶體被包裝成NIO Buffer物件,並提供了一組方法,用來方便的訪問該塊記憶體。


二、Buffer型別

    Java NIO 中有以下Buffer型別:ByteBuffer、MappedByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer和ShortBuffer等。
    這些Buffer型別代表了不同的基本資料型別,可以通過char、short、int、long、float或double型別來操作緩衝區中的位元組。

    這些類沒有一種能夠直接例項化。它們都是抽象類,但是都包含靜態工廠方法用來建立相應類的新例項。

    新的緩衝區是由分配或包裝操作建立的。分配操作建立一個緩衝區物件並分配一個私有的空間來儲存容量大小的資料元素;包裝操作建立一個緩衝區物件但是不分配任何空間來儲存資料元素,它使用提供的陣列作為儲存空間來儲存緩衝區中的資料元素。


三、使用示例

    使用Buffer讀寫資料一般遵循以下四個步驟:
        寫入資料到Buffer;呼叫flip()方法;從Buffer中讀取資料;呼叫clear()方法或者compact()方法

    當向buffer寫入資料時,buffer會記錄下寫了多少資料。一旦要讀取資料,需要通過flip()方法將Buffer從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到buffer的所有資料。一旦讀完了所有的資料,就需要清空緩衝區,讓它可以再次被寫入。

// 獲取檔案控制程式碼(相對於專案根路徑)
RandomAccessFile file = new RandomAccessFile("nio-data.txt", "rw");

// 獲取Channel
FileChannel inChannel = file.getChannel();

// 定義Buffer緩衝區,大小 48byte
ByteBuffer buf = ByteBuffer.allocate(48);

// 從Channel中讀取資料寫到Buffer
int bytesRead = inChannel.read(buf);

// 判斷是檔案否讀取完畢
while (bytesRead != -1) {

	// 輸出讀取到的資料大小
	System.out.println("Read Size:" + bytesRead);

	// 將buffer由寫模式切換到讀模式
	buf.flip(); 

	//判斷Buffer是否讀完
	while (buf.hasRemaining()) {
		//輸出從Buffer中讀取的內容
		System.out.print((char) buf.get());
	}

	// 清空緩衝區,讓它可以再次被寫入
	buf.clear();

	// 繼續從Channel中讀取資料到Buffer
	bytesRead = inChannel.read(buf);
}
//關閉
file.close();
四、四個屬性

        為了理解Buffer的工作原理,需要熟悉它的四個屬性:capacity(容量)、position(位置)、limit(限制)、mark(標記)

  • Capacity
        作為一個記憶體塊,Buffer有一個固定的大小值,也叫“capacity”.你只能往裡寫capacity個byte、long,char等型別。一旦Buffer滿了,需要將其清空(通過讀資料或者清除資料)才能繼續寫資料往裡寫資料。
  • Position
        當你寫資料到Buffer中時,position表示當前的位置。初始的position值為0.當一個byte、long等資料寫到Buffer後, position會向前移動到下一個可插入資料的Buffer單元。position最大可為capacity – 1
        當將Buffer從寫模式切換到讀模式時,position會被重置為0. 從Buffer的position處讀取資料時,position向前移動到下一個可讀的位置。
  • Limit
        在寫模式下,Buffer的limit表示你最多能往Buffer裡寫多少資料。即limit等於Buffer的capacity。
        當切換Buffer到讀模式時, limit表示你最多能讀到多少資料。(因此,當切換Buffer到讀模式時,limit會被設定成寫模式下的position值。換句話說,你能讀到之前寫入的所有資料(limit被設定成已寫資料的數量,這個值在寫模式下就是position))。

  • Mark
        一個備忘位置。標記在設定前是未定義的(undefined)。使用場景是,假設緩衝區中有 10 個元素,position 目前的位置為 2,現在只想傳送 6 - 10 之間的緩衝資料,此時我們可以 buffer.mark(buffer.position()),即把當前的 position 記入 mark 中,然後 buffer.postion(6),此時傳送給 channel 的資料就是 6 - 10 的資料。傳送完後,我們可以呼叫 buffer.reset() 使得 position = mark,因此這裡的 mark 只是用於臨時記錄一下位置用的。


五、Buffer建立

    通過allocate()或者wrap()函式建立的緩衝區通常都是間接的 ,間接的緩衝區使用備份陣列

		/**
		 * 堆空間中分配了一個char型陣列作為備份儲存器來儲存100個char變數 
		 */
		CharBuffer charBuffer1 = CharBuffer.allocate (100);
		
		
		/**
		 * 構造了一個新的緩衝區物件,但資料元素會存在於陣列中。
		 * 通過呼叫put()函式造成的對緩衝區的改動會直接影響這個陣列,而且對這個陣列的任何改動也會對這個緩衝區物件可見
		 */
		char [] charArray = new char [100];
		CharBuffer charbuffer2 = CharBuffer.wrap(charArray);
		
		/**
		 * 建立了一個position值為 12,limit值為54,容量為charArray.length的緩衝區
		 * 並非建立了一個只佔用了一個陣列子集的緩衝區,offset和length引數只是設定了初始的狀態
		 */
		CharBuffer charbuffer3 = CharBuffer.wrap (charArray, 12, 42);
		
		/**
		 * 關於wrap(array, offset, length)的一些注意事項
		 */
		char[] charArray2 = new char[10];
		CharBuffer charbuffer4 = CharBuffer.wrap(charArray2, 4, 4);
		/* 可以執行 */
		charbuffer4.put("abcd");  
		/* 不可以,buffer越界 */
		charbuffer4.put("abcde");
		/* 可以執行, buffer狀態已經重置,position為0 limit為10 */
		charbuffer4.clear();
		charbuffer4.put("abcdefgh");
六、讀寫資料

    寫入資料到Buffer有兩種方式:
        從Channel寫到Buffer;通過Buffer的put()方法寫到Buffer裡

		//  從Channel寫到Buffer
		int bytesRead = inChannel.read(buf); 

		//  通過put方法寫Buffer
		buf.put(127);

    從Buffer中讀取資料有兩種方式:
        從Buffer讀取資料到Channel;使用get()方法從Buffer中讀取資料

		//  從Buffer讀取資料到Channel的例子
		int bytesWritten = inChannel.write(buf);

		//  使用get()方法從Buffer中讀取資料的例子。get方法有很多版本,允許你以不同的方式從Buffer中讀取資料
		byte aByte = buf.get();
    一些注意的事項:

        put操作如果操作會導致位置超出上界 (Limit), 就會丟擲BufferOverflowException異常
        get操作如果
位置大於或等於上界 (Limit),就會丟擲BufferUnderflowException異常

        對只讀的緩衝區的修改嘗試將會導致ReadOnlyBufferException丟擲
絕對存取不會影響緩衝區的位置屬性,但是如果您所提供的索引超出範圍(負數或不小於上界),也將丟擲IndexOutOfBoundsException異常

 

    一個簡單的示例:

		/**
		 * 建立一個ByteBuffer並寫入字串Hello
		 */
		ByteBuffer buffer = ByteBuffer.allocate(10);
		buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');

        

		/**
		 * 更改ByteBuffer的資料,將'H'變更為'M'並追加'w'
		 */
		buffer.put(0,(byte)'M').put((byte)'w');
        
		/**
		 * 翻轉讀寫模式,這裡由寫轉為讀
		 */
		buffer.flip();
        

		/**
		 * 讀取資料
		 * hasRemaining()會在釋放(讀)緩衝區時告訴您是否已經達到緩衝區的上界即讀完
		 */
		while(buffer.hasRemaining()){
			/**
			 * 相對方式讀取buffer中的資料
			 */
			System.out.println((char)buffer.get());
		}

    特別需要注意的是,讀取操作不涉及清理。相對方式(get())僅讀取當前資料和更新position的值,絕對方式(get(index))僅讀取指定位置的資料,他們都不會將讀取過的資料清除

 

七、翻轉讀寫模式

    flip方法將Buffer從寫模式切換到讀模式。呼叫flip()方法會將position設回0,並將limit設定成之前position的值。也就是說,position在翻轉之後用於標記讀的位置,limit表示之前寫進了多少個(byte、char等)現在能讀取多少個

   注意:如果將緩衝區翻轉兩次,大小會變為0;嘗試對緩衝區上位置和上界都為0的get()操作會導致BufferUnderflowException異常,而put()則會導致BufferOverflowException異常


八、位置標記

    使緩衝區能夠記住一個位置並在之後將其返回。緩衝區的標記在mark()函式被呼叫之前是未定義的,呼叫時標記被設為當前位置的值。reset()函式將位置設為當前的標記值。如果標記值未定義,呼叫reset()將導致InvalidMarkException異常

		/**
		 * 初始化資料
		 */
		ByteBuffer buffer = ByteBuffer.allocate(10);
		buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');  //寫入hello
		
		/**
		 * 翻轉模式
		 */
		buffer.flip();
				
		/**
		 *buffer現在的position是在H的位置,Mark它 
		 */
		buffer.mark(); 
		
		/**
		 * 手動調整position到第一個l的位置
		 */
		buffer.position(2); 
		
		/**
		 * 開始讀取資料
		 */
		while(buffer.hasRemaining()){
			System.out.print((char)buffer.get());
		}
		
		System.out.println();
		
		/**
		 * 通過reset()回到Mark記住的位置即H的位置
		 */
		buffer.reset();
		
		//
		/**
		 * 再次讀取資料,從H的位置 
		 */
		while(buffer.hasRemaining()){
			System.out.print((char)buffer.get());
		}

附:    Java - NIO之Buffer(下) 

   

 

 

 

相關文章