最近再看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 緩衝區Limit:28
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;
}