1. 程式人生 > >Java NIO之緩衝區Buffer分析

Java NIO之緩衝區Buffer分析

目錄

Buffer介紹

ByteBuffer介紹

ByteBuffer案例

總結


Buffer介紹

1.Buffer簡介

      緩衝區(Buffer)是中NIO中基礎的內容,存在於包java.nio下面.一個Buffer物件可以看做是一個儲存資料的容器,資料被儲存到這裡後可以進行檢索。緩衝區工作與通道關聯,我們不與通道直接進行互動,而是通過緩衝區將資料傳送到通道里,或者從通道獲取資料放到緩衝區。Buffer類是一個頂層的抽象類,裡面定義了緩衝區操作的基本方法。Buffer的子類包含了java基本型別中除布林型別以外的其他基本資料型別對應緩衝區。需要注意的是資料傳輸的基本單位是位元組,所以唯一與通道互動的緩衝區是ByteBuffer,儲存的是原始的位元組資料,而其他的緩衝區提供了用java基本資料型別來檢視ByteBuffer中位元組資料的檢視,比如呼叫asCharBuffer(),就是將位元組轉換成字元檢視ByteBuffer裡面的資料,實際儲存資料的真正地方依舊是ByteBuffer緩衝區。

      ByteBuffer是原始位元組儲存的地方,如要將位元組進行正確轉換成字元資料,需要將位元組資料先進行特定編碼轉成字元輸入到字元緩衝區,或者在輸出的時候將資料按指定字符集進行解碼處理,否則會出現亂碼的情況。

java.nio包下面包含java基本型別中除boolean型別的緩衝區有ByteBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,CharBuffer

DoubleBuffer,此外還有MappedByteBuffer(繼承自ByteBuffer),還有不能直接訪問的HeapByteBuffer,DirectByteBuffer

。其中HeapByteBuffer是呼叫ByteBuffer中allocate()時底層的實現,分配空間是受JVM管控的堆記憶體。而DirectByteBuffer是ByteBuffer.allocateDirect()方法時底層實現,分配的空間是虛擬機器之外的記憶體。Buffer的子類子類如下:

                 

基本資料型別  緩衝區
byte ByteBuffer
short ShortBuffer
int IntBuffer
long LongBuffer
boolean -
char  CharBuffer
float FloatBuffer
double DoubleBuffer
- MappedByteBuffer
- HeapByteBuffer(底層實現,無法直接訪問,JVM堆記憶體)
- DirectByteBuffer(底層實現,無法直接訪問,JVM外的記憶體)

2.內部變數

private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
long address;
  • mark----呼叫mark()方法將會當前位置postion儲存,-1表示的是當前沒有標記。
  • position----下一個要寫入或者讀取元素的索引位置,position值不會為負數,並且不會大於limit的值。
  • limit----第一個不能寫入或者讀取的元素的索引位置,limit值不會為負數,並且不會大於capacity的值。
  • capacity----緩衝區的容量,capacity值不會為負數,並且不能被改變。

所以Buffer變數內部變數滿足:0 <= mark<=position <= limit <= capacity.

2.內部方法

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()
  • capacity()----返回的是緩衝區容量的大小。
  • position()----返回position的值大小。
  • position(int newPosition)----設定緩衝區中position的值。
  • limit()----返回此緩衝區的限制值limit.
  • limit(int newLimit)----設定此緩衝區的限制值Limit,並返回緩衝區物件。
  • mark()----標記緩衝區中位置。
  • reset()----將緩衝區位置position設定為標記的位置。
  • clear()----清空緩衝區,呼叫此方法將position設定為0,limit的值設定為緩衝區容量值,標記將被丟棄。
  • flip()----呼叫此方法將limit設定當前position的值,position設定為0,有標記情況下,標記將被丟棄。
  • rewind()----呼叫此方法將position值設定為0,標記將被丟棄.,limit值不會變化。
  • remaining()----返回當前位置與限制limit之間的值,即limit-position的值。
  • hasRemaing()---布林型別,當前位置與限制值之間是否有元素。
  • isReadOnly()----抽象方法,返回此緩衝區是否只讀。
  • hasArray()----抽象方法,是否能用一個可訪問的陣列來備份緩衝區。
  • array()----抽象方法,用一個可訪問的陣列來備份緩衝區。
  • arrayOffset()----抽象方法,緩衝區中的第一個元素到備份陣列中的偏移量。
  • isDirect()----判斷此緩衝區是否為直接緩衝區(呼叫allocateDirect()分配的是直接緩衝區,即不受JVM管控的堆外記憶體)。

ByteBuffer介紹

a.建立緩衝區方法

public static ByteBuffer allocateDirect(int capacity){}
public static ByteBuffer allocate(int capacity) {}
public static ByteBuffer wrap(byte[] array,int offset, int length){}
public static ByteBuffer wrap(byte[] array) {}
public final boolean hasArray() {}
public final byte[] array() {}
public final int arrayOffset() {}

allocateDirect()與allocate()相比較,分配的是直接緩衝區,直接緩衝區的建立是由作業系統方面的程式碼分配的,是JVM堆外的記憶體,而allocate()分配的是JVM堆記憶體,兩者各有利弊。直接分別緩衝區脫離了JVM的束縛,是IO操作方面的最佳選擇,但建立和銷燬直接緩衝區花銷更大,非直接緩衝區分配的是JVM堆記憶體,建立非直接緩衝區向通道傳輸資料底層可能會先建立一個臨時直接緩衝區,然後將非直接緩衝區資料放到臨時直接緩衝區,使用臨時緩衝區與IO進行互動,這會建立大量物件,但垃圾物件能得到及時回收。

wrap()方法建立可視陣列作為緩衝區的備份儲存器,對於陣列更改還是緩衝區更改,都會直接影響到對方。對於wrap()方法需要注意的是:

byte[] byteArray = new byte[100];

ByteBuffer buffer = ByteBuffer.wrap(byteArray,10,50);

建立的是一個position為10,limit為10+50=60,容量大小為100的位元組緩衝區,而不是位元組陣列byteArray的子集(容易混淆的地方)

hasArray()返回的是緩衝區是否有一個可存取的備份陣列。

array()返回的是這個緩衝區所使用的陣列儲存空間的引用。

arrayOffset()返回緩衝區資料在陣列儲存開始位置的偏移量。

b.存取方法。

public abstract byte get()
public abstract byte get(int index)
public abstract ByteBuffer put(byte b)
public abstract ByteBuffer put(int index, byte b)

get()和put()不帶索引的方法,存取的相對位置的元素。帶了索引index的get()和put()方法,讀取的是指定索引位置的元素或者將元素儲存到指定位置。

c.批量存取方法

public ByteBuffer get(byte[] dst, int offset, int length) {}
public ByteBuffer get(byte[] dst){}
public ByteBuffer put(byte[] src, int offset, int length) {}
public final ByteBuffer put(byte[] src) {}
public ByteBuffer put(ByteBuffer src){}

緩衝區的目的就是為了一次可以存取整塊資料,批量資料存取方法get()或者put(),實現了資料從緩衝區或者到緩衝區的高效存取。get()方法實現將緩衝區資料批量複製放到位元組陣列或者位元組陣列的子集中,其中offset是位元組陣列中偏移量,length是資料長度。如果沒有指定長度情況下,必須要填滿整個陣列。

byte[] bytes = new byte[101];

ByteBuffer buffer = ByteBuffer.allocate(100);

buffer.get(bytes,10,92); 將緩衝區複製資料到位元組陣列bytes中,從bytes的索引10開始,填充長度為92個,超過了bytes的長度,將會丟擲IndexOutOfBoundsException

buffer.get(bytes);從緩衝區複製資料到位元組陣列bytes中,沒有設定長度,即要填充101個位元組,但是緩衝只有100個位元組,所以將會丟擲異常BufferUnderflowException

d.比較方法

public boolean equals(Object ob)
public int compareTo(ByteBuffer that)

其中equals()方法比較的是兩個緩衝區剩餘內容是否一樣,相等充要條件:第一點是兩個物件必須都是Buffer型別且同一個資料型別的Buffer;其次Buffer容量可以不一致,只要剩餘元素數目相等;第三點是get()函式返回的剩餘元素的序列一致。

compareTo()方法容許以詞典順序比較兩個緩衝區中的剩餘元素序列,不容許不同物件之間的比較,如果傳遞了物件型別錯誤,將會丟擲 ClassCastException異常

e.轉換檢視的方法

public abstract xxxx getXxxx()
public abstract xxxx getXxxx(int index)
public abstract ByteBuffer putXxxx(xxxx value)
public abstract ByteBuffer putXxxx(int index, xxxx value)
public abstract XxxxBuffer asXxxxBuffer()

ByteBuffer中有將緩衝區位元組資料對映成除了布林型別資料外其他原始資料型別的方法。並且提供將位元組對映為指定資料型別的getXxxx()方法和將java原始資料型別儲存到位元組緩衝區的putXxxx()方法,其中Xxxx可以是除布林型別以外的基本資料型別short,int ,long ,char ,float,double。其中asXxxxBuffer()提供了將位元組緩衝區ByteBuffer轉換成其他資料型別的檢視來檢視位元組資料,但是要注意的是實際儲存資料的地方依舊是ByteBuffer。例如:double資料型別的方法。

public abstract double getDouble()
public abstract double getDouble(int index)
public abstract ByteBuffer putDouble(double value)
public abstract ByteBuffer putDouble(int index, double value)
public abstract DoubleBuffer asDoubleBuffer()

f.複製緩衝區

public abstract ByteBuffer slice()
public abstract ByteBuffer duplicate()
public abstract ByteBuffer asReadOnlyBuffer()

slice()建立的原始緩衝區當前位置開始的新緩衝區,並且與原始緩衝區共享一段資料元素子序列,其中只讀屬性和基本屬性包含position,limit,mark屬性與原始緩衝區是一致的。

duplicate()建立的是與原始緩衝區相似的緩衝區,新緩衝區會繼承原始緩衝區只讀屬性,兩個緩衝區共享容量和資料元素,但是有各自的position,limit,mark屬性,並且一個緩衝區的元素改變也會反映在另外一個緩衝區上。

asReadOnlyBuffer()建立的是與duplicate()操作方法類似的緩衝區,但只是只讀檢視的緩衝區。

g.compact()/rewind()/flip()/mark()/reset()等

呼叫compact()方法將緩衝區資料進行壓縮,具體是丟棄釋放的元素,儲存未釋放的元素。也就是將position位置之前的元素全部丟棄,然後將position開始之後元素全部向前移動。緩衝區當前位置將會被置為緩衝區中最後一個“存活”元素後插入資料的位置。

其中rewind().flip(),mark(),reset()方法完全繼承自父類Buffer。

新建立的ByteBuffer緩衝區,position=0,limit與capacity的大小相等。

                                                           

使用put()方法進行填充後。

                                                             

就緩衝區上面目前狀態,分別呼叫flip()rewind()方法,flip()與rewind()方法都會將緩衝區中position置為0。但兩者的區別是呼叫flip()方法會將將limit設定為position,然後將position置為0;rewind()方法不會改變limit的值。

                                                           

呼叫compact()方法,會將position之前的資料丟棄。後面的剩餘未讀取資料會整體向前移動,而後面的資料不會變化,position會設定為緩衝區中最後一個數據插入的位置,limit值會被值為capacity。例如:下圖中"HE"已經被讀取,將被丟棄,後面的資料整體向前移動,可以看到"LLO"複製到了原先緩衝區中“HEL”位置,但是後面的兩個資料“LO”並沒有變化,position設定為原緩衝區最後一個數據“O”的後面插入資料的位置,即緩衝區“O”後面的資料“L”位置。

                                                               

標記方法呼叫mark()方法,會記錄position的位置,當呼叫reset()方法時會將position重置為標記位置。

                                                               

h.儲存位元組順序

 public final ByteOrder order() {}
 public final ByteBuffer order(ByteOrder bo) {}

位元組順序的儲存是由計算機硬體決定的。位元組儲存到緩衝區中,儲存量大於一個位元組情況,就會出現位元組順序問題,比如short(2個位元組),double(8個位元組)等超過一個位元組的資料。位元組儲存順序有兩種:大端(big endian)和小端(little-endian)。大端就是將高位位元組儲存到低位地址,小端就是高位位元組儲存到高位地址。記憶體地址是低位到高位,而資料是高位位元組到低位位元組。如:0x4F6B,大端儲存就是4F6B,小端儲存就是6B4F。多數預設的儲存資料的順序是大端。

ByteBuffer案例

public class BufferDemo {
  public static void main(String[] args) {
    testViewBuffer();
    testOtherMethod();
    testByteOrder();
  }

   //位元組儲存順序的測試 
  private static void testByteOrder() {
    ByteBuffer buffer = ByteBuffer.allocate(6);
    buffer.asCharBuffer().put("abc");
    System.out.println(Arrays.toString(buffer.array()));
    buffer.rewind();
    buffer.order(ByteOrder.BIG_ENDIAN);
    buffer.asCharBuffer().put("abc");
    System.out.println(Arrays.toString(buffer.array()));
    buffer.rewind();
    buffer.order(ByteOrder.LITTLE_ENDIAN);
    buffer.asCharBuffer().put("abc");
    System.out.println(Arrays.toString(buffer.array()));
  }  

  private static void testOtherMethod() {
    ByteBuffer buffer = ByteBuffer.allocate(100);
    buffer.put("helloworld".getBytes());
    
    //呼叫flip()方法後,position=0,limit=10。
    //flip()方法從寫入模式切換到讀取模式
    buffer.flip();
    System.out.println(buffer); 
    //ByteBuffer中元素通過char檢視瀏覽,需要解碼。
    String encoding = System.getProperty("file.encoding");
    System.out.println(Charset.forName(encoding).decode(buffer));

    buffer.rewind();
    System.out.println(buffer);
    //測試mark(),reset()方法。
    System.out.println((char)buffer.get());
    buffer.mark();
    System.out.println("mark()-------"+(char)buffer.get());
    int i=0;
    while(i++<4) {
      System.out.print((char)buffer.get());
    }
    System.out.println();
    buffer.reset();
    System.out.println("reset()-------"+(char)buffer.get());
    System.out.println("讀取元素"+(char)buffer.get());
    //關於compact()方法並非資料整體前移。
    //而是整體移動的時候,還是會保留部分資料
    buffer.compact();
    System.out.println(buffer);
    while(buffer.hasRemaining()) {
      System.out.print((char)buffer.get());
    }
    System.out.println();
    
    buffer.rewind();
    buffer.asCharBuffer().put("a");
    System.out.println(buffer.getChar());
    
    buffer.asShortBuffer().put((short)11247);
    System.out.println(buffer.getShort());
  }
  
  //測試其他檢視瀏覽ByteBuffer裡面的元素
  private static void testViewBuffer() {
    ByteBuffer buffer = ByteBuffer.wrap(new byte[] {0,0,0,0,0,0,0,'a'});
    buffer.rewind();
    System.out.println("----ByteBuffer----");
    while(buffer.hasRemaining()) {
      System.out.print(buffer.position()+"-->"+buffer.get()+", ");
    }
    System.out.println();
    
    //char佔兩個位元組
    buffer.rewind();
    CharBuffer charBuffer = buffer.asCharBuffer();
    System.out.println("----CharBuffer----");
    while(charBuffer.hasRemaining()) {
      System.out.print(charBuffer.position()+"-->"+charBuffer.get()+", ");
    }
    System.out.println();
    
    //short佔用兩個位元組
    buffer.rewind();
    ShortBuffer shortBuffer = buffer.asShortBuffer();
    System.out.println("----ShortBuffer----");
    while(shortBuffer.hasRemaining()) {
      System.out.print(shortBuffer.position()+"-->"+shortBuffer.get()+", ");
    }
    System.out.println();
    
    //int佔用四個位元組
    buffer.rewind();
    IntBuffer intBuffer = buffer.asIntBuffer();
    System.out.println("----IntBuffer----");
    while(intBuffer.hasRemaining()) {
      System.out.print(intBuffer.position()+"-->"+intBuffer.get()+", ");
    }
    System.out.println();
    
    //long佔用8個位元組
    buffer.rewind();
    LongBuffer longBuffer = buffer.asLongBuffer();
    System.out.println("----LongBuffer----");
    while(longBuffer.hasRemaining()) {
      System.out.print(longBuffer.position()+"-->"+longBuffer.get()+", ");
    }
    System.out.println();
    
    //float佔用兩個位元組
    buffer.rewind();
    FloatBuffer floatBuffer = buffer.asFloatBuffer();
    System.out.println("----FloatBuffer----");
    while(floatBuffer.hasRemaining()) {
      System.out.print(floatBuffer.position()+"-->"+floatBuffer.get()+", ");
    }
    System.out.println();
    
    //double佔用8個位元組
    buffer.rewind();
    DoubleBuffer doubleBuffer = buffer.asDoubleBuffer();
    System.out.println("----DoubleBuffer----");
    while(doubleBuffer.hasRemaining()) {
      System.out.print(doubleBuffer.position()+"-->"+doubleBuffer.get()+", ");
    }
    System.out.println();
  }
}

執行結果:

testViewBuffer()方法執行結果:

----ByteBuffer----
0-->0, 1-->0, 2-->0, 3-->0, 4-->0, 5-->0, 6-->0, 7-->97, 
----CharBuffer----
0-->, 1-->, 2-->, 3-->a,
----ShortBuffer----
0-->0, 1-->0, 2-->0, 3-->97, 
----IntBuffer----
0-->0, 1-->97, 
----LongBuffer----
0-->97, 
----FloatBuffer----
0-->0.0, 1-->1.36E-43, 
----DoubleBuffer----
0-->4.8E-322

testOtherMethod()方法執行結果:

java.nio.HeapByteBuffer[pos=0 lim=10 cap=100]
helloworld
java.nio.HeapByteBuffer[pos=0 lim=10 cap=100]
h
mark()-------e
llow
reset()-------e
讀取資料l
java.nio.HeapByteBuffer[pos=7 lim=100 cap=100]
rld
a
11247

testByteOrder()方法執行結果:

[0, 97, 0, 98, 0, 99]         
[0, 97, 0, 98, 0, 99]
[97, 0, 98, 0, 99, 0]

總結

1.rewind()與flip()都是將緩衝區的position置為0,不同點是rewind()不會改變limit的值,flip()方法會先設定limit為position,然後將position值置為0。

2.從通道中獲取的資料原始資料是位元組,儲存到ByteBuffer緩衝區,用其他資料型別檢視檢視位元組資料。例如呼叫asCharBuffer(),也只是通過char檢視瀏覽ByteBuffer緩衝區中資料,實際儲存資料的地方依舊是ByteBuffer。

3.ByteBuffer.wrap(byte[] array,int offset, int length)建立的緩衝區並非是array的子集,而是設定了position=offset,

limit=offset+llength,緩衝區的容量capacity大小是array.length。這是比較容易混淆的。

4.關於compact()壓縮方法,壓縮資料除了丟棄已讀取資料,將未讀取資料整體向前移動,還會保留後面部分資料。此外還會設定position和limit的值。

參考文獻:《Java程式設計思想》《Java NIO》