ByteBuffer的Put和Get的用法和要注意的細節

最近再看java的NIO,裏面提到了幾個基本的類,其中ByteBuffer是最基礎的,用於Channel的讀寫傳輸數據使用。下面總結一下我理解的ByteBuffer。
先從代碼開始分析

    static public void asIntBuffer() {
        ByteBuffer bBuf = ByteBuffer.allocate(512);
        bBuf.putInt(1);
        bBuf.putInt(2);
        bBuf.putInt(3);
        bBuf.putInt(4);
        bBuf.putInt(5);
        bBuf.putInt(6);
        bBuf.putInt(7);
        bBuf.flip();
        bBuf.putInt(8);
        bBuf.putInt(9);
        System.out.println("緩衝區Pos:" + bBuf.position() + "  緩衝區Limit:"
                + bBuf.limit());
        System.out.println(bBuf.getInt());
        System.out.println(bBuf.getInt());
        System.out.println(bBuf.getInt());
        System.out.println(bBuf.getInt());
        System.out.println(bBuf.getInt());
    }

輸出:

緩衝區Pos:8  緩衝區Limit28
3
4
5
6
7

從上面的輸出發現當flip()被調用之後如果在網buffer裏面put數據會覆蓋之前寫入的數據,導致Position位置後移,如果在加一句get()就會出現java.nio.BufferUnderflowException異常,見下面的輸出。

緩衝區Pos:8  緩衝區Limit:28
3
4
5
6
7
Exception in thread "main" java.nio.BufferUnderflowException
    at java.nio.Buffer.nextGetIndex(Buffer.java:498)
    at java.nio.HeapByteBuffer.getInt(HeapByteBuffer.java:355)
    at com.Demo.asIntBuffer(Demo.java:52)
    at com.Demo.main(Demo.java:22)

簡單的分析一下put、get和flip的源代碼。

ByteBuffer bBuf = ByteBuffer.allocate(512);

首先看allocate函數,通過傳入一個capacity用來指定buffer的容量,返回了一個HeapByteBuffer的對象,該對象是ByteBuffer的一個子類,其構造函數直接調用了ByteBuffer的構造函數。

    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }
HeapByteBuffer的構造函數:
    HeapByteBuffer(int cap, int lim) {            // package-private
        super(-1, 0, lim, cap, new byte[cap], 0);//初始化 limit pos cap mark等參數
        /*
        hb = new byte[cap];
        offset = 0;
        */
    }

    HeapByteBuffer(byte[] buf, int off, int len) { // package-private
        super(-1, off, off + len, buf.length, buf, 0);
        /*
        hb = buf;
        offset = 0;
        */
    }

    protected HeapByteBuffer(byte[] buf,
                                   int mark, int pos, int lim, int cap,
                                   int off)
    {
        super(mark, pos, lim, cap, buf, off);
        /*
        hb = buf;
        offset = off;
        */
    }

此時我們就得到了一個帶有Capacity大小緩衝區的ByteBuffer對象,下面開始往緩衝區寫數據,以int類型數據爲列子。來分析一下putInt(int i)的源碼。putInt()的實現是在HeapByteBuffer類中,通過調用了Bits的靜態函數putInt完成的,其中put之後pos的移動是通過nextPutIndex()函數完成,Int大小4個字節,向後移動4個,該函數實在Buffer基類中實現的。bigEndian是一個bool變量,用來表示當前是大端存儲還是小端存儲,默認大端。

   public ByteBuffer putInt(int x) {
        Bits.putInt(this, ix(nextPutIndex(4)), x, bigEndian);
        return this;
    }
    protected int ix(int i) {
        return i + offset;//加上位置偏移
    }
    final int nextPutIndex(int nb) {                    // package-private
        if (limit - position < nb)
            throw new BufferOverflowException();
        int p = position;
        position += nb;//Pos指針後移
        return p;//原始Pos指針返回,用來計算此次取出的數據
    }

下面看Bits的put函數:

    static void putInt(ByteBuffer bb, int bi, int x, boolean bigEndian) {
        if (bigEndian)//根據不同的存儲方式調用不同的解析函數
            putIntB(bb, bi, x);
        else
            putIntL(bb, bi, x);
    }
    //以大端爲例,這裏主要是後面的intX()函數,用來對x進行位運算,取出相應位置的數據,放入到緩衝區的相應位置
    static void putIntB(ByteBuffer bb, int bi, int x) {
        bb._put(bi    , int3(x));
        bb._put(bi + 1, int2(x));
        bb._put(bi + 2, int1(x));
        bb._put(bi + 3, int0(x));
    }

    private static byte int3(int x) { return (byte)(x >> 24); }
    private static byte int2(int x) { return (byte)(x >> 16); }
    private static byte int1(int x) { return (byte)(x >>  8); }
    private static byte int0(int x) { return (byte)(x      ); }

到此位置,數據被放入到了緩衝區中,下面開始讀取。讀取之前一定要先調用flip()函數,該函數可以控制pos和limit的值,使得緩衝區可以在讀寫之間很好的切換,它的實現實在Buffer基類中,主要工作就是,limit轉換成當前緩衝區在最後一次寫入數據後的位置,pos和mark重置,從頭開始讀取數據,這就是爲什麼,在寫入之後調用flip()函數在寫入不但會覆蓋之前寫入的值,還會導致pos位置發生變化,不能從最開始讀取數據。

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

下面看一下get函數,get函數的實現也是在子類HeapByteBuffer中,nextGetIndex函數實在雞肋Buffer中實現的,主要功能就是get之後的pos後移工作。Bits.getInt和前面的Bits.putInt相似,不錯過多介紹。

    public int getInt() {
        return Bits.getInt(this, ix(nextGetIndex(4)), bigEndian);
    }
    final int nextGetIndex(int nb) {                    // package-private
        if (limit - position < nb)
            throw new BufferUnderflowException();
        int p = position;
        position += nb;
        return p;
    }

至此 講的差不多了。一些函數開頭的判斷沒有詳細的去講,他們的主要工作就是在put和get的時候越界的異常拋出。
在看源碼的時候發現了另一個函數,這個函數很有意思public int getInt(int i) 從字面上看上去好像是獲取第i個Int,調用一下試試,看看療效。

    public int getInt(int i) {
        return Bits.getInt(this, ix(checkIndex(i, 4)), bigEndian);
    }
    static public void asIntBuffer() {
        ByteBuffer bBuf = ByteBuffer.allocate(512);
        bBuf.putInt(1);
        bBuf.putInt(2);
        bBuf.putInt(3);
        bBuf.putInt(4);
        bBuf.putInt(5);
        bBuf.putInt(6);
        bBuf.putInt(7);
        bBuf.flip();
        System.out.println("緩衝區Pos:" + bBuf.position() + "  緩衝區Limit:"
                + bBuf.limit());
         for (int i = 0; i < 7; i++) {
             System.out.println(bBuf.getInt(i));
         }
    }
對應輸出:
緩衝區Pos:0  緩衝區Limit:28
1
256
65536
16777216
2
512
131072

這時候機會發現,他並沒有像我們想想的那樣去工作,其中256,65536是怎麼來的呢。繼續看public int getInt(int i) 的源碼。發現它和之前分getInt唯一不同的就是在checkIndex(4) 通過看 final int checkIndex(int i, int nb) 的源碼發現,該函數什麼都沒做只是check了一下limit。那256優勢怎麼來的呢?

    final int checkIndex(int i, int nb) {               // package-private
        if ((i < 0) || (nb > limit - i))
            throw new IndexOutOfBoundsException();
        return i;
    }

下面開一下Bits.getInt()和 getIntB()以及makeInt() 的源碼,我們能夠知道,當我們要獲取第i個位置的int時,也就是bi。此時bi並沒有跳過4個字節,而是在Buffer數組總按照我們提供的i去取了i之後的三個字節,在加上第i個構成了一個4字節的int。

    static int getInt(ByteBuffer bb, int bi, boolean bigEndian) {
        return bigEndian ? getIntB(bb, bi) : getIntL(bb, bi) ;
    }
    static int getIntB(ByteBuffer bb, int bi) {
        return makeInt(bb._get(bi    ),
                       bb._get(bi + 1),
                       bb._get(bi + 2),
                       bb._get(bi + 3));
    }
    static private int makeInt(byte b3, byte b2, byte b1, byte b0) {
        return (((b3       ) << 24) |
                ((b2 & 0xff) << 16) |
                ((b1 & 0xff) <<  8) |
                ((b0 & 0xff)      ));
    }

至於256怎麼來的?下面分析一下,Buffer是一個字節數組。當我們putInt(1)putInt(2) 之後,裏面的前8個字節數組是這個樣子的(大端存儲)

16進制
00 00 00 01 00 00 00 02 
轉成2進制
00000000 00000000 00000000 00000001 00000000 00000000 00000000 00000002 

當我們取調用getInt(2) 時其實取出來的是包括第二個字節以及後面的三個,也就是00000000 00000000 00000001 00000000 也就是256, 後面的數字同理。

當我們知道getInt(i) 後我們在來看一下putInt(index,i); 看似是在第index位置插入Int值i,其實不然

    static public void asIntBuffer() {
        ByteBuffer bBuf = ByteBuffer.allocate(512);
        for (int i = 0; i < 10; i++) {
            bBuf.putInt(i, i);
        }
        System.out.println("緩衝區Pos:" + bBuf.position() + "  緩衝區Limit:"
                + bBuf.limit());

    }
輸出:緩衝區Pos:0  緩衝區Limit:512

我們發現,pos壓根沒有移動,Buffer中壓根沒數據。同getInt(i) 類似pputInt(inex,i) 同樣沒引起pos的移動,pos始終處於0的位置,在我們get數據時,在nextGetIndex() 函數校驗時就拋出異常了,總上,使用putInt(index,i) 必須在index位置有數據的情況下使用。

    final int nextGetIndex(int nb) {                    // package-private
        if (limit - position < nb)
            throw new BufferUnderflowException();
        int p = position;
        position += nb;
        return p;
    }
發佈了48 篇原創文章 · 獲贊 104 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章