Java NIO 之緩衝區

amadan發表於2021-09-09

一個 Buffer 物件是固定數量的資料的容器。通道是 I/O 傳輸發生時通過的入口,而緩衝區是這些資料傳輸的來源或目標。

緩衝區基礎

所有的緩衝區都具有四個屬性來 供關於其所包含的資料元素的資訊。

  • capacity(容量):緩衝區能夠容納資料的最大值,建立緩衝區後不能改變。
  • limit(上界):緩衝區的第一個不能被讀或寫的元素。或者,緩衝區現存元素的計數。
  • position(位置):下一個要被讀或寫的元素的索引。呼叫 get 或 put 函式會更新。
  • mark(標記):一個備忘位置。呼叫 mark() 來設定 mark=postion。呼叫 reset() 設定position= mark。標記在設定前是未定義的(undefined)。

這四個屬性之間總是 循以下關係:

0 <= mark <= position <= limit <= capacity

下圖是一個新建立的 ByteBuffer :

位置被設為 0,而且容量和上界被設為 10, 好經過緩衝區能夠容納的最後一個位元組。 標記最初未定義。容量是固定的,但另外的三個屬性可以在使用緩衝區時改變。

如下是 Buffer 的方法簽名:

public abstract class Buffer {

    public final int capacity() {
    }

    public final int position() {
    }

    public final Buffer position(int newPosition) {
    }

    public final int limit() {
    }

    public final Buffer limit(int newLimit) {
    }

    public final Buffer mark() {
    }

    public final Buffer reset() {
    }

    public final Buffer clear() {
    }

    public final Buffer flip() {
    }

    public final Buffer rewind() {
    }

    public final int remaining() {
    }

    public final boolean hasRemaining() {
    }

    public abstract boolean isReadOnly();

    public abstract boolean hasArray();

    public abstract Object array();

    public abstract int arrayOffset();

    public abstract boolean isDirect();
}

上文所列出的的 Buffer API 並沒有包括 get() 或 put() 函式。每一個 Buffer 類都有這兩個函式,但它們所採用的引數型別,以及它們返回的資料型別,對每個子類來說都是唯一的,所以它們不能在頂層 Buffer 類中被抽象地宣告。

如下是 ByteBuffer 的宣告:

public abstract class ByteBuffer extends Buffer implements Comparable
{
    // This is a partial API listing
    public abstract byte get( );
    public abstract byte get (int index);
    public abstract ByteBuffer put (byte b);
    public abstract ByteBuffer put (int index, byte b);
}

一個例子看 ByteBuffer 的儲存:

buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');

如果在進行如下操作:

buffer.put(0,(byte)'M').put((byte)'w');

當緩衝區寫滿了,要把內容讀出來,我們需要翻轉緩衝區,可以呼叫 flip 方法,如下是 ByteBuffer 當中的 flip 方法:

public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

rewind()函式與 flip()相似,但不影響上界屬性。它只是將位置值設回 0。您可以使用 rewind()後 ,重讀已經被翻轉的緩衝區中的資料。

hasRemaining() 方法會返回當前是否到達緩衝區的上界。

remaining() 方法會返回到達上界的距離。

緩衝區的標記在 mark() 函式被呼叫之前是未定義的,呼叫時標記被設為當前位置的值。

reset() 函式將位置設為當前的標記值。如果標記值未定義,呼叫 reset()將導致 InvalidMarkException 異常。

rewind( ),clear( ),以及 flip( )總是拋棄標記,即設定成 -1。

兩個緩衝區被認為相等的充要條件是:

  • 兩個物件型別相同。包含不同資料型別的buffer 遠不會相等,而且buffer 絕不會等於非 buffer 物件。
  • 兩個物件都剩餘同樣數量的元素。Buffer 的容量不需要相同,而且緩衝區中剩餘資料的索引也不必相同。但每個緩衝區中剩餘元素的數目(從位置到上界)必須相同。
  • 在每個緩衝區中應被Get()函式返回的剩餘資料元素序列必須一致。

建立緩衝區

//分配一個容量為 100 個 char 變數的 Charbuffer
CharBuffer charBuffer = CharBuffer.allocate (100);

char [] myArray = new char [100];
CharBuffer charbuffer = CharBuffer.wrap (myArray);
//這段程式碼構造了一個新的緩衝區物件,但資料元素會存在於陣列中。這意味著通過呼叫 put()函式造成的對緩衝區的改動會直接影響這個陣列,而且對這個陣列的任何改動也會對這 個緩衝區物件可見。

CharBuffer.wrap(array, offset, length);可以指定 position 和 length

通過 allocate() 或者 wrap() 函式建立的緩衝區通常都是間接的,間接的緩衝區使用備份陣列。 hasArray() 返回這個緩衝區是否有一個可存取的備份陣列。

複製緩衝區

一些複製緩衝區的 api :

public abstract class CharBuffer extends Buffer implements CharSequence, Comparable {
	// This is a partial API listing
	public abstract CharBuffer duplicate( );
	public abstract CharBuffer asReadOnlyBuffer( );
	public abstract CharBuffer slice( );
}

duplicate() 函式建立了一個與原始緩衝區相似的新緩衝區。兩個緩衝區共享資料元素,擁有同樣的容量,但每個緩衝區擁有各自的位置,上界和標記屬性。對一個緩衝區內的資料元素所做的改變會反映在另外一個緩衝區上。如果原始的緩衝區為只讀,或者為直接緩衝區,新的緩衝區將繼承這些屬性。

如下示例:

public static void main(String[] args) {
		CharBuffer buffer = CharBuffer.allocate(8);
		buffer.position(3).limit(6).mark().position(5);
		CharBuffer dupeBuffer = buffer.duplicate();
		System.out.println(dupeBuffer.position());
		System.out.println(dupeBuffer.limit());
		dupeBuffer.clear();
		System.out.println(dupeBuffer.position());
		System.out.println(dupeBuffer.limit());
	}

//out
5
6
0
8

asReadOnlyBuffer() 函式來生成一個只讀的緩衝區圖。

程式碼說明如下:

public static void main(String[] args) {
		CharBuffer buffer = CharBuffer.allocate(8);
		CharBuffer dupeBuffer = buffer.asReadOnlyBuffer();
		System.out.println(dupeBuffer.isReadOnly());
		dupeBuffer.put('S');//只讀buffer呼叫丟擲異常
	}

//out
true
Exception in thread "main" java.nio.ReadOnlyBufferException
	at java.nio.HeapCharBufferR.put(HeapCharBufferR.java:172)
	at nio.test.TestMain.main(TestMain.java:10)

slice() 建立一個從原始緩衝區的當前位置開始的新緩衝 區,並且其容量是原始緩衝區的剩餘元素數量(limit-position)。這個新緩衝區與原始 緩衝區共享一段資料元素子序列。分出來的緩衝區也會繼承只讀和直接屬性。

public static void main(String[] args) {
		CharBuffer buffer = CharBuffer.allocate(8);
		buffer.position(3).limit(5);
		CharBuffer sliceBuffer = buffer.slice();
	}

位元組緩衝區

在 java.nio 中,位元組順序由 ByteOrder 類封裝。

ByteOrder.nativeOrder() 方法返回 JVM 執行的硬體平臺位元組順序。

直接緩衝區

只有位元組緩衝區有資格參與 I/O 操作。

I/O 操作的目標記憶體區域必須是連續的位元組序列。

直接緩衝區被用於與通道和固有 I/O 例程互動。

直接位元組緩衝區通常是 I/O 操作最好的選擇。直接位元組緩衝區支援 JVM 可用的最高效 I/O 機制。非直接位元組緩衝區可以被傳遞給通道,但是這樣可能導致效能耗。通常非直接緩衝不可能成為一個本地 I/O 操作的目標。如果向一個通道中傳遞一個非直接 ByteBuffer 物件用於寫入會每次隱含呼叫下面的操作:

  1. 建立一個臨時的直接 ByteBuffer 物件。
  2. 將非直接緩衝區的內容複製到直接臨時緩衝中。
  3. 使用直接臨時緩衝區執行低層次 I/O 操作。
  4. 直接臨時緩衝區物件離開作用域,並最終成為被回的無用資料。

直接緩衝區時 I/O 的最佳選擇,但可能比建立非直接緩衝區要花費更高的成本。直接緩衝區使用的記憶體是通過呼叫本地作業系統方面的程式碼分配的, 過了標準 JVM 。

ByteBuffer.allocateDirect() 建立直接緩衝區。isDirect() 返回是否直接緩衝區。

檢視緩衝區

檢視緩衝區通過已存在的緩衝區物件例項的工方法來建立。這種圖物件維護它自己的屬性,容量,位置,上界和標記,但是和原來的緩衝區共享資料元素。

ByteBuffer 類允許建立圖來將 byte 型緩衝區位元組資料對映為其它的原始資料型別。

public abstract CharBuffer asCharBuffer( );
public abstract ShortBuffer asShortBuffer( );
public abstract IntBuffer asIntBuffer( );
public abstract LongBuffer asLongBuffer( );
public abstract FloatBuffer asFloatBuffer( );
public abstract DoubleBuffer asDoubleBuffer( );

看如下一個例子的示意圖:

ByteBuffer byteBuffer = ByteBuffer.allocate (7).order (ByteOrder.BIG_ENDIAN);
CharBuffer charBuffer = byteBuffer.asCharBuffer( );

public class BufferCharView {
       public static void main (String [] argv)  throws Exception  {
          ByteBuffer byteBuffer = ByteBuffer.allocate (7).order (ByteOrder.BIG_ENDIAN);
          CharBuffer charBuffer = byteBuffer.asCharBuffer( );
          // Load the ByteBuffer with some bytes
          byteBuffer.put (0, (byte)0);
          byteBuffer.put (1, (byte)'H');
          byteBuffer.put (2, (byte)0);
          byteBuffer.put (3, (byte)'i');
          byteBuffer.put (4, (byte)0);
          byteBuffer.put (5, (byte)'!');
          byteBuffer.put (6, (byte)0);
          println (byteBuffer);
          println (charBuffer);
       }
       // Print info about a buffer
       private static void println (Buffer buffer)  {
		  System.out.println ("pos=" + buffer.position() + ", limit=" +
          	buffer.limit() + ", capacity=" + buffer.capacity() + ": '" + buffer.toString( ) + "'");
	}
}

//執行 BufferCharView 程式的輸出是:
//pos=0, limit=7, capacity=7: 'java.nio.HeapByteBuffer[pos=0 lim=7 cap=7]'
//pos=0, limit=3, capacity=3: 'Hi!

資料元素檢視

ByteBuffer 類為每一種原始資料型別 供了存取的和轉化的方法:

public abstract class ByteBuffer extends Buffer implements Comparable {
	public abstract char getChar();

	public abstract char getChar(int index);

	public abstract short getShort();

	public abstract short getShort(int index);

	public abstract int getInt();

	public abstract int getInt(int index);

	public abstract long getLong();

	public abstract long getLong(int index);

	public abstract float getFloat();

	public abstract float getFloat(int index);

	public abstract double getDouble();

	public abstract double getDouble(int index);

	public abstract ByteBuffer putChar(char value);

	public abstract ByteBuffer putChar(int index, char value);

	public abstract ByteBuffer putShort(short value);

	public abstract ByteBuffer putShort(int index, short value);

	public abstract ByteBuffer putInt(int value);

	public abstract ByteBuffer putInt(int index, int value);

	public abstract ByteBuffer putLong(long value);

	public abstract ByteBuffer putLong(int index, long value);

	public abstract ByteBuffer putFloat(float value);

	public abstract ByteBuffer putFloat(int index, float value);

	public abstract ByteBuffer putDouble(double value);

	public abstract ByteBuffer putDouble(int index, double value);
}

假如一個 bytebuffer 處於如下狀態:

那麼 int value = buffer.getInt();

實際的返回值取決於緩衝區的當前的位元排序(byte-order)設定。更具體的寫法是:

int value = buffer.order (ByteOrder.BIG_ENDIAN).getInt( );

這將會返回值 0x3BC5315E,同時:

int value = buffer.order (ByteOrder.LITTLE_ENDIAN).getInt( );

返回值 0x5E31C53B

如果您試圖獲取的原始型別需要比緩衝區中存在的位元組數更多的位元組,會丟擲 BufferUnderflowException。

相關文章