NIO 三 緩衝區數據讀寫

一 自增讀寫

  先打預防針,緩衝區的讀寫複用是通過數據寫入覆蓋實現的,所謂數據清理不過是移動核心參數值,實際上數據寫入後會永久存在,這意味着在讀寫緩衝區數據時,必須要時刻了解position、limit、mark和capacity。

  讀寫數據時對position參數的依賴非常強,很多時候都可以將position作爲下一次讀寫數據對應的數組的下標索引值。

  Buffer的直接派生類均提供了一種position自增的讀寫方法:

  put([對應緩衝區數據類型的值類型] data)/get()

  自增讀寫指的是讀取或寫入N個數據時,position會自動增加N長度,如果可讀寫數據長度不足N,那麼get方法會拋出BufferUnderflowException,put方法會拋出BufferOverflowException。

1.1 單數據自增讀寫

  單數據自增讀寫指的是每次通過緩衝區對象讀寫單個數據,並且position自增1,如ByteBuffer中提供瞭如下方法:

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
	...
	public abstract ByteBuffer put(byte b);
	public abstract byte get();
	...
}

  ByteBuffer的派生類實現了此方法,如HeapByteBuffer:

class HeapByteBuffer extends ByteBuffer {
	...
	public ByteBuffer put(byte x) {
		hb[ix(nextPutIndex())] = x;
		return this;
    }
	public byte get() {
        return hb[ix(nextGetIndex())];
    }
	...
}

  其中nextPutIndex和nextGetInex方法是基類Buffer提供的:

public abstract class Buffer {
	...
	final int nextPutIndex() {
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }
	final int nextGetIndex() {
        if (position >= limit)
            throw new BufferUnderflowException();
        return position++;
    }
	...
}

  由此也印證了put/get因可讀寫數據長度不足導致的異常以及position自增。測試一下:

public class ByteBufferTest {
    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(3);
        System.out.println("capacity:" + byteBuffer.capacity() + " limit:" + byteBuffer.limit() + " position:" + byteBuffer.position());
        for (int i = 0; i < byteBuffer.limit(); i++) {
            byteBuffer.put((byte) i);
            System.out.println("position:" + byteBuffer.position());
        }
        byteBuffer.flip();
        for (int i = 0; i < byteBuffer.limit(); i++) {
            System.out.println("position:" + byteBuffer.position());
            System.out.println(byteBuffer.get() + " position:" + byteBuffer.position());
        }
    }
}

輸出結果:
capacity:3 limit:3 position:0
position:1
position:2
position:3
position:0
0 position:1
position:1
1 position:2
position:2
2 position:3

1.2 批量數據自增讀寫

  批量數據的自增讀寫實際可以認爲是單數據自增讀寫的一個封裝版本,這裏還以ByteBuffer舉例。

1.2.1 定長讀寫

  可以指定批量讀寫的長度,在ByteBuffer中此類讀寫方法的定義如下:

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
	...
	public ByteBuffer put(byte[] src, int offset, int length) {
		// 第一步就開始檢查能夠批量讀寫
        checkBounds(offset, length, src.length);
        if (length > remaining())
            throw new BufferOverflowException();
        int end = offset + length;
		// 實際上還是通過put方法來遍歷插入數據的,因此具有position遞增特性
        for (int i = offset; i < end; i++)
            this.put(src[i]);
        return this;
    }
	public ByteBuffer get(byte[] dst, int offset, int length) {
        checkBounds(offset, length, dst.length);
		// 第一步就開始檢查能夠批量讀寫
        if (length > remaining())
            throw new BufferUnderflowException();
        int end = offset + length;
		// 實際上還是通過get方法來遍歷讀取數據的,因此具有position遞增特性
        for (int i = offset; i < end; i++)
            dst[i] = get();
        return this;
    }
	...
}

  這裏需要強調一下,批量的自增讀寫方法都要求傳入供讀寫的數組對象,緩衝區讀寫開始位置的偏移量,需讀寫數據的長度。通過上面的源碼也可以看出來,其中偏移量指的是本次讀寫於緩衝區中的起始位置,通過checkBounds方法來進行檢查,此方法由基類Buffer提供:

public abstract class Buffer {
	...
	static void checkBounds(int off, int len, int size) {
        if ((off | len | (off + len) | (size - (off + len))) < 0)
            throw new IndexOutOfBoundsException();
    }
	...
}

  這意味着如果緩衝區中可讀寫的數據長度小於本次讀寫的數據長度時,將拋出越界異常。如果參數檢查無誤,那麼後續的處理邏輯實際上就是遍歷的讀寫數據,綜上批量的自增讀寫方法實際上等價於下面的寫法:

// 等價寫
for (int i = offset; i < offset + length; i++) {
	put(src[i]);
}

// 等價讀
for (int i = offset; i < offset + length; i++) {
	src[i] = get();
}

1.2.2 完全讀寫

  不同於定長讀寫,這裏指將參數數組中的全部數據寫入緩衝區,或從緩衝區讀取數組長度的數據:

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
	...
	public final ByteBuffer put(byte[] src) {
        return put(src, 0, src.length);
    }
	public ByteBuffer get(byte[] dst) {
        return get(dst, 0, dst.length);
    }
	...
}

  從源碼中可以看出,完全模式的讀寫僅僅是定長讀寫的一種簡便寫法,偏移量始終爲0,讀寫數據的長度就是參數數組的長度。

1.3 剩餘數據寫入

  ByteBuffer還提供了一種position自增的批量寫入模式,和批量的完全寫模式差不多,但是參數是另一個緩衝區對象:

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
	...
	public ByteBuffer put(ByteBuffer src) {
        if (src == this)
            throw new IllegalArgumentException();
        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;
    }
	...
}

  從源碼中可以看出來只有非當前緩衝區引用、非只讀模式、參數緩衝區的剩餘數據長度小於目標緩衝區的可寫入數據長度(即limit - position)時,纔會執行數據寫入動作,此時通過自增的put方法向目標緩衝區中寫入,通過自增的get方法從參數緩衝區對象中讀取,因此通過此方法寫入數據後,目標緩衝區和參數緩衝區對象的position都會發生變化:

 public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(3);
        ByteBuffer byteBufferOther = ByteBuffer.allocate(3);
        System.out.println("capacity:" + byteBuffer.capacity() + " limit:" + byteBuffer.limit() + " position:" + byteBuffer.position());
        byteBuffer.put(1, (byte)1);
        byteBuffer.put(2, (byte)2);
        byteBufferOther.put(byteBuffer);
        System.out.println("byteBuffer position:" + byteBuffer.position() + " byteBufferOther position:" + byteBufferOther.position());
}

輸出結果如下:
capacity:3 limit:3 position:0
byteBuffer position:3 byteBufferOther position:3

二 目標讀寫

  前文中介紹的讀寫實際上是依賴自行維護值的position實現的,調用者不需要考慮如何移動position,那麼當需要讀寫指定位置上的數據時,自增讀寫便無法應對。

  ByteBuffer中提供了帶有數據位置參數的讀寫方法,聲明如下:

```java
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
	...
	public abstract ByteBuffer put(int index, byte b);
	public abstract byte get(int index);
	...
}

  依然通過實現類HeapByteBuffer來查閱具體實現:

class HeapByteBuffer extends ByteBuffer {
	...
	public ByteBuffer put(int i, byte x) {
        hb[ix(checkIndex(i))] = x;
        return this;
    }
	public byte get(int i) {
        return hb[ix(checkIndex(i))];
    }
	...

  注意,這裏的put/get方法沒有再使用nextPut/GetIndex,而是使用的CheckIndex方法,此方法由基類Buffer提供:

public abstract class Buffer {
	...
	final int checkIndex(int i) {
        if ((i < 0) || (i >= limit))
            throw new IndexOutOfBoundsException();
        return i;
    }
	...
}

  當向緩衝區目標位置讀寫數據時,如果參數位置小於零或者大於限制會拋出越界異常。方法內部沒有對position參數進行值操作,因此使用目標位置讀寫模式,position不會自增,示例如下:

public class ByteBufferTest {
    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(3);
        System.out.println("capacity:" + byteBuffer.capacity() + " limit:" + byteBuffer.limit() + " position:" + byteBuffer.position());
        byteBuffer.put(1, (byte)1);
        byteBuffer.put(2, (byte)2);
        System.out.println("position:" + byteBuffer.position());
        System.out.println(byteBuffer.get(1));
        System.out.println("position:" + byteBuffer.position());
    }
}

輸出結果如下:
capacity:3 limit:3 position:0
position:0
1
position:0

三 值類型數據讀寫

  在介紹創建緩衝區一文中提到過ByteBuffer特有的asXXBuffer方法,同樣的在緩衝數據讀寫方面,ByteBuffer也提供了支持不同值類型的數據的讀寫方法,同前文中介紹的實現模式一樣,針對值類型數據的讀寫也分爲自增讀寫和目標位置讀寫:

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
	...
	// 不帶index參數的方法會自增position,增加的position值爲值類型位數,如Char類型自增2
	public abstract ByteBuffer putChar(char value);
	public abstract char getChar();
	// 帶index參數的方法不會自增position
	public abstract ByteBuffer putChar(int index, char value);
	public abstract char getChar(int index);
	// 其他的Doubule、Int等等類型的put/get方法不再贅述
	...
}

四 結語

  如果想關注更多硬技能的分享,可以參考積少成多系列傳送門,未來每一篇關於硬技能的分享都會在傳送門中更新鏈接。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章