「高并發通信框架Netty4 源碼解讀(三)」NIO緩沖區Buffer詳解

我們以 Buffer 類開始我們對 java.nio 軟件包的瀏覽歷程。這些類是 java.nio 的構基礎。我們將深入研究緩沖區, 了解各種不同的類型,并學會怎樣使用。到那時我們將明了 java.nio 緩沖區是如何與 java.nio.channels 這一通道類相聯系的。

1.前言

一個Buffer對象是固定數量的數據的容器。其作用是一個存儲器,或者分段運輸區,在這里數據可被存儲并在之后用于檢索。緩沖區如我們在上一篇所討論的那樣被寫滿和釋放。對于每個非布爾原始數據類型都有一個緩沖區類。盡管緩沖區作用于它們存儲的原始數據類型,但緩沖區十分傾向于處理字節。非字節緩沖區可以在后臺執行從字節或到字節的轉換,這取決于緩沖區是如何創建的。


緩沖區的工作與通道緊密聯系。通道是 I/O 傳輸發生時通過的入口,而緩沖區是這些數據傳輸的來源或目標。對于離開緩沖區的傳輸,您想傳遞出去的數據被置于一個緩沖區,被傳送到通道。對于傳回緩沖區的傳輸,一個通道將數據放置在您所提供的緩沖區中。這種在協同對象(通常是您所寫的對象以及一到多個 Channel 對象)之間進行的緩沖區數據傳遞是高效數據處理的關鍵。 下一篇博客詳解通道。


下圖是 Buffer 的類層次圖。在頂部是通用 Buffer 類。 Buffer 定義所有緩沖區類型共有的操作,無論是它們所包含的數據類型還是可能具有的特定行為。這一共同點將會成為我們的出發點。


2.緩沖區基礎

概念上,緩沖區是包在一個對象內的基本數據元素數組。 Buffer 類相比一個簡單數組的優點是它將關于數據的數據內容和信息包含在一個單一的對象中。 Buffer 類以及它專有的子類定義了
一個用于處理數據緩沖區的 API。

2.1 屬性

所有的緩沖區都具有四個屬性來提供關于其所包含的數據元素的信息。它們是:

  • 容量(Capacity)
    緩沖區能夠容納的數據元素的最大數量。這一容量在緩沖區創建時被設定,并且永遠不能被改變。
  • 上界(Limit)
    緩沖區的第一個不能被讀或寫的元素。或者說,緩沖區中現存元素的計數。
  • 位置(Position)
    下一個要被讀或寫的元素的索引。位置會自動由相應的 get( )和 put( )函數更新。
  • 標記(Mark)
    一個備忘位置。調用 mark( )來設定 mark = postion。調用 reset( )設定 position =mark。標記在設定前是未定義的(undefined)。
    這四個屬性之間總是遵循以下關系:0 <= mark <= position <= limit <= capacity,讓我們來看看這些屬性在實際應用中的一些例子。下圖展示了一個新創建的容量為 10的 ByteBuffer 邏輯視圖。

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

2.2 緩沖區 API

package java.nio;
public abstract class Buffer {
public final int capacity( )
public final int position( )
public final Buffer position (int newPositio
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( );
}

關于這個 API 有一點要注意的是,像 clear()這類函數,您通常應當返回 void,而不是 Buffer 引用。這些函數將引用返回到它們在(this)上被引用的對象。這是一個允許級
聯調用的類設計方法。級聯調用允許這種類型的代碼:

buffer.mark( );
buffer.position(5);
buffer.reset( );

被簡寫為:

buffer.mark().position(5).reset( );

java.nio 中的類被特意地設計為支持級聯調用。您可能已經在 StringBuffer 類中看到了級聯調用的使用。如果聰明地使用級聯調用, 就能產生簡潔,優美, 易讀的代碼。 但如果濫用,就會使代碼不知所云。 當級聯調用可以增加可讀性并使讓您的目標更加明確時使用它。如果使用級聯調用會使代碼作用不夠清晰,那么請不要使用它。請時刻保證您的代碼易于他人閱讀。


對于 API 還要注意的一點是 isReadOnly()函數。所有的緩沖區都是可讀的,但并非所有都可寫。每個具體的緩沖區類都通過執行 isReadOnly()來標示其是否允許該緩存區的內容 被 修 改 。 一 些 類 型 的 緩 沖 區 類 可 能 未 使 其 數 據 元 素 存 儲 在 一 個 數 組 中 。 例 如MappedByteBuffer 的內容可能實際是一個只讀文件。您也可以明確地創建一個只讀視圖緩沖 區 , 來 防 止 對 內 容 的 意 外 修 改 。 對 只 讀 的 緩 沖 區 的 修 改 嘗 試 將 會 導 致ReadOnlyBufferException 拋出。但是我們要提前做好準備。

2.3存取

讓我們從起點開始。緩沖區管理著固定數目的數據元素。但在任何特定的時刻,我們可能只對緩沖區中的一部分元素感興趣。換句話說,在我們想清空緩沖區之前,我們可能只使用了緩沖區的一部分。這時,我們需要能夠追蹤添加到緩沖區內的數據元素的數量,放入下一個元素的位置等等的方法。位置屬性做到了這一點。它在調用 put()時指出了下一個數據元素應該被插入的位置,或者當 get()被調用時指出下一個元素應從何處檢索。聰明的讀者會注意到上文所列出的的 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);
}

Get 和 put 可以是相對的或者是絕對的。在前面的程序列表中,相對方案是不帶有索引參數的函數。當相對函數被調用時,位置在返回時前進一。如果位置前進過多,相對運算就會拋 出 異 常 。 對于put(),如果運 算會導致位置超出上界 , 就會拋出BufferOverflowException 異常。對于 get(),如果位置不小于上界,就會拋出
BufferUnderflowException 異常。絕對存取不會影響緩沖區的位置屬性,但是如果您所提供的索引超出范圍(負數或不小于上界),也將拋出 IndexOutOfBoundsException 異常。

2.4填充

讓我們看一個例子。 我們將代表“Hello”字符串的 ASCII 碼載入一個名為 buffer 的ByteBuffer 對象中。所有類型底層維護一個數組,比如ByteBuffer維護的數據如下:

public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer>
{

    final byte[] hb;                  // 底層維護的字節數組
    final int offset;
    boolean isReadOnly;                 // Valid only for heap buffers
.......省略

當在上文中的圖所新建的緩沖區上執行以下代碼后,緩沖區的結果狀態如下圖所示:
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');


注意本例中的每個字符都必須被強制轉換為 byte。我們不能不經強制轉換而這樣操做:buffer.put('H');
因為我們存放的是字節而不是字符。記住在 java 中,字符在內部以 Unicode 碼表示,每個 Unicode 字符占 16 位。例子使用包含 ascii 字符集數值的字節。通過將char 強制轉換為 byte,我們刪除了前八位來建立一個八位字節值。這通常只適合于拉丁字符而不能適合所有可能的 Unicode 字符。為了讓事情簡化,我們暫時故意忽略字符集的映射問題。以后將詳細涉及字符編碼。
既然我們已經在 buffer 中存放了一些數據,如果我們想在不丟失位置的情況下進行一些更改該怎么辦呢? put()的絕對方案可以達到這樣的目的。假設我們想將緩沖區中的內容從“Hello”的 ASCII 碼更改為“Mellow”。我們可以這樣實現:

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

這里通過進行一次絕對方案的 put 將 0 位置的字節代替為十六進制數值 0x4d,將 0x77放入當前位置(當前位置不會受到絕對 put()的影響)的字節,并將位置屬性加一。結果如下圖所示:


2.4翻轉

我們已經寫滿了緩沖區,現在我們必須準備將其清空。我們想把這個緩沖區傳遞給一個通道,以使內容能被全部寫出。但如果通道現在在緩沖區上執行 get(),那么它將從我們剛剛插入的有用數據之外取出未定義數據。如果我們將位置值重新設為 0,通道就會從正確位置開始獲取,但是它是怎樣知道何時到達我們所插入數據末端的呢?這就是上界屬性被引入的目的。上界屬性指明了緩沖區有效內容的末端。我們需要將上界屬性設置為當前位置,然后將位置重置為 0。我們可以人工用下面的代碼實現:

buffer.limit(buffer.position()).position(0);

但這種從填充到釋放狀態的緩沖區翻轉是 API 設計者預先設計好的,他們為我們提供了一個非常便利的函數:flip(),Flip()函數將一個能夠繼續添加數據元素的填充狀態的緩沖區翻轉成一個準備讀出元素
的釋放狀態。在翻轉之后,上圖變成下圖這樣:


我們看下flip函數的源碼:

public final Buffer flip() {
        limit = position;//位置元素賦值給上限元素
        position = 0;//位置元素設置為0
        mark = -1;
        return this;
    }

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

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

2.4釋放

如果我們現在將上圖中的緩沖區傳入通道,它將取出我們存放在那里的數據,從位置開始直到上界結束。很簡單,不是嗎?
同樣地,如果您接收到一個在別處被填滿的緩沖區,您可能需要在檢索內容之前將其翻轉。例如,如果一個通道的 read()操作完成,而您想要查看被通道放入緩沖區內的數據,那么您需要在調用 get()之前翻轉緩沖區。通道對象在緩沖區上調用 put()增加數據;put和read 可以隨意混合使用。
布爾函數 hasRemaining()會在釋放緩沖區時告訴您是否已經達到緩沖區的上界。源碼如下:

  public final boolean hasRemaining() {
        return position < limit;//很簡單
    }

以下是一種將數據元素從緩沖區釋放到一個數組的方法。

for (int i = 0; buffer.hasRemaining( );i++) {
    myByteArray [i] = buffer.get( );
}

作為選擇, remaining()函數將告知您從當前位置到上界還剩余的元素數目。 您也可以通過下面的循環來釋放緩沖區。

int count = buffer.remaining( );
for (int i = 0; i < count, i++) {
    myByteArray [i] = buffer.get( );
}

//remaining源碼
 public final int remaining() {
        return limit - position;
    }

一旦緩沖區對象完成填充并釋放,它就可以被重新使用了。 Clear()函數將緩沖區重置為空狀態。 它并不改變緩沖區中的任何數據元素,而是僅僅將上界設為容量的值,并把位置設回 0,這使得緩沖區可以被重新填入。

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

看一個完整的案例:填充和釋放緩沖區

package com.ronsoft.books.nio.buffers;
import java.nio.CharBuffer;
/**
* Buffer fill/drain example. This code uses the simplest
* means of filling and draining a buffer: one element at
* a time.
* @author Ron Hitchens (ron@ronsoft.com)
*/
public class BufferFillDrain
{

    private static int index = 0;
    private static String[] strings = {
            "A random string value",
            "The product of an infinite number of monkeys",
            "Hey hey we're the Monkees",
            "Opening act for the Monkees: Jimi Hendrix",
            "'Scuse me while I kiss this fly", // Sorry Jimi ;-)
            "Help Me! Help Me!",
    };

     public static void main(String[] argv) throws Exception {
        CharBuffer buffer = CharBuffer.allocate(100);
        while (fillBuffer(buffer)) {
            buffer.flip();
            drainBuffer(buffer);
            buffer.clear();
        }
    }

    private static void drainBuffer(CharBuffer buffer) {
        while (buffer.hasRemaining()) {
            System.out.print(buffer.get());
        }
        System.out.println("");
    }

    private static boolean fillBuffer(CharBuffer buffer) {
        if (index >= strings.length) {
            return (false);
        }
        String string = strings[index++];
        for (int i = 0; i < string.length(); i++) {
            buffer.put(string.charAt(i));
        }
        return (true);
    }
}

2.5壓縮

public abstract class ByteBuffer
extends Buffer implements Comparable
{
  // This is a partial API listing
  public abstract ByteBuffer compact( );
}

有時,您可能只想從緩沖區中釋放一部分數據,而不是全部,然后重新填充。為了實現這一點,未讀的數據元素需要下移以使第一個元素索引為 0。盡管重復這樣做會效率低下,但這有時非常必要,而 API 對此為您提供了一個 compact()函數。這一緩沖區工具在復制數據時要比您使用 get()和 put()函數高效得多。所以當您需要時,請使用 compact()。下圖顯示了一個我們已經釋放了一些元素,并且現在我們想要對其進行壓縮的緩沖區。


這樣操作:
buffer.compact();
會導致緩沖區的狀態如下圖

這里發生了幾件事。您會看到數據元素 2-5 被復制到 0-3 位置。位置 4 和 5 不受影響,但現在正在或已經超出了當前位置,因此是“死的”。它們可以被之后的 put()調用重寫。還要注意的是,位置已經被設為被復制的數據元素的數目。也就是說,緩沖區現在被定位在緩沖區中最后一個“存活”元素后插入數據的位置。最后,上界屬性被設置為容量的值,因此緩沖區可以被再次填滿。調用 compact()的作用是丟棄已經釋放的數據,保留未釋放的數據,并使緩沖區對重新填充容量準備就緒。源碼如下

  public ByteBuffer compact() {
      //將未讀出的元素拷貝到前面,
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
        position(remaining());
        limit(capacity());
        discardMark();
        return this;
    }

如果您想在壓縮后釋放數據,緩沖區會像之前所討論的那樣需要被翻轉。無論您之后是否要向緩沖區中添加新的數據,這一點都是必要的。

2.6標記

mark()標記,使緩沖區能夠記住一個位置并在之后將其返回。緩沖區的標記在 mark( )函數被調用之前是未定義的,調用時標記被設為當前位置的值。 reset( )函數將位置設為當前的標記值。如果標記值未定義,調用 reset( )將導致 InvalidMarkException 異常。一些緩沖區函數會拋棄已經設定的標記( rewind( ), clear( ),以及 flip( )總是拋棄標記,即mark=-1)。如果新設定的值比當前的標記小,調用limit( )或 position( )帶有索引參數的版本會拋棄標記。

public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }

拋棄標記

//所謂的拋棄,就是設置mark=-1啦
public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

public final Buffer position(int newPosition) {
        if ((newPosition > limit) || (newPosition < 0))
            throw new IllegalArgumentException();
        position = newPosition;
        if (mark > position) mark = -1;//拋棄標記
        return this;
    }

2.7比較

有時候比較兩個緩沖區所包含的數據是很有必要的。所有的緩沖區都提供了一個常規的equals( )函數用以測試兩個緩沖區的是否相等,以及一個 compareTo( )函數用以比較緩沖區。

public abstract class ByteBuffer
extends Buffer implements Comparable
{
  // This is a partial API listing
  public boolean equals (Object ob)
  public int compareTo (Object ob)
}

兩個緩沖區可用下面的代碼來測試是否相等:

if (buffer1.equals (buffer2)) {
  doSomething( );
}

//equals 源碼,只比較剩余元素是否相等。

    public boolean equals(Object ob) {
        if (this == ob)//同一個對象,肯定相等
            return true;
        if (!(ob instanceof ByteBuffer))//不是同一個類型,肯定不相等
            return false;
        ByteBuffer that = (ByteBuffer)ob;
        if (this.remaining() != that.remaining())//剩余數量是否相等
            return false;
        int p = this.position();
        for (int i = this.limit() - 1, j = that.limit() - 1; i >= p; i--, j--)//剩余元素是否相等
            if (!equals(this.get(i), that.get(j)))
                return false;
        return true;
    }

如果每個緩沖區中剩余的內容相同,那么 equals( )函數將返回 true,否則返回 false。因為這個測試是用于嚴格的相等而且是可換向的。前面的程序清單中的緩沖區名稱可以顛倒,并會產生相同的結果。
緩沖區也支持用 compareTo( )函數以詞典順序進行比較。這一函數在緩沖區參數小于,等于,或者大于引用 compareTo( )的對象實例時,分別返回一個負整數, 0 和正整數。這些就是所有典型的緩沖區所實現的 java.lang.Comparable 接口語義。這意味著緩沖區數組可以通過調用 java.util.Arrays.sort()函數按照它們的內容進行排序。 equals( )相似, compareTo( )不允許不同對象間進行比較。但 compareTo( )更為嚴格:如果您傳遞一個類型錯誤的對象,它會拋出 ClassCastException 異常,但 equals( )只會返回false。比較是針對每個緩沖區內剩余數據進行的,與它們在 equals( )中的方式相同,直到不相等的元素被發現或者到達緩沖區的上界。如果一個緩沖區在不相等元素發現前已經被耗盡,較短的緩沖區被認為是小于較長的緩沖區。不像 equals( ), compareTo( )不可交換:順序問題。在本例中,一個小于零的結果表明 buffer2 小于 buffer1,而表達式的值就會是 true:

if (buffer1.compareTo (buffer2) < 0) {
doSomething( );
}

源碼

   public int compareTo(ByteBuffer that) {
        int n = this.position() + Math.min(this.remaining(), that.remaining());//當前對象應讀取的長度
        for (int i = this.position(), j = that.position(); i < n; i++, j++) {
            int cmp = compare(this.get(i), that.get(j));//比較是否相等
            if (cmp != 0)//不相等
                return cmp;
        }
//比較剩余個數,this多說明this的剩余數包含了that的剩余數,
        return this.remaining() - that.remaining();且包含的內容相等
    }

2.8批量移動

緩沖區的涉及目的就是為了能夠高效傳輸數據。一次移動一個數據元素并不高效。如您在下面的程序清單中所看到的那樣, buffer API 提供了向緩沖區內外批量移動數據元素的函數。

public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable
{
  // This is a partial API listing
  public CharBuffer get (char [] dst)
  public CharBuffer get (char [] dst, int offset, int length)
  public final CharBuffer put (char[] src)
  public CharBuffer put (char [] src, int offset, int length)
  public CharBuffer put (CharBuffer src)
  public final CharBuffer put (String src)
  public CharBuffer put (String src, int start, int end)
}

有兩種形式的 get( )可供從緩沖區到數組進行的數據復制使用。第一種形式只將一個數組作為參數,將一個緩沖區釋放到給定的數組。第二種形式使用 offset 和 length 參數來指定目標數組的子區間。這些批量移動的合成效果與前文所討論的循環是相同的,但是這些方法可能高效得多,因為這種緩沖區實現能夠利用本地代碼或其他的優化來移動數據。批量移動總是具有指定的長度。也就是說,您總是要求移動固定數量的數據元素。當參看程序簽名時這一點還不明顯,但是對 get( )的這一引用:
buffer.get(myArray);
等價于:
buffer.get(myArray,0,myArray.length);
批量傳輸的大小總是固定的。省略長度意味著整個數組會被填滿。
如果您所要求的數量的數據不能被傳送,那么不會有數據被傳遞,緩沖區的狀態保持不變,同時拋出 BufferUnderflowException 異常。因此當您傳入一個數組并且沒有指定長度,您就相當于要求整個數組被填充。如果緩沖區中的數據不夠完全填滿數組,您會得到一個異常。這意味著如果您想將一個小型緩沖區傳入一個大型數組,您需要明確地指定緩沖區中剩余的數據長度。上面的第一個例子不會如您第一眼所推出的結論那樣,將緩沖區內剩余的數據元素復制到數組的底部。要將一個緩沖區釋放到一個大數組中,要這樣做:

char [] bigArray = new char [1000];
// 獲取剩余元素數量
int length = buffer.remaining( );
buffer.get (bigArrray, 0, length);
// 處理
processData (bigArray, length);

另一方面,如果緩沖區存有比數組能容納的數量更多的數據,您可以重復利用如下文所示的程序塊進行讀取:

char [] smallArray = new char [10];
while (buffer.hasRemaining( )) {
  int length = Math.min (buffer.remaining( ), smallArray.length);
  buffer.get (smallArray, 0, length);
  processData (smallArray, length);
}

Put()的批量版本工作方式相似,但以相反的方向移動數據,從數組移動到緩沖區。他們在傳送數據的大小方面有著相同的語義:
buffer.put(myArray);
等價于:
buffer.put(myArray,0,myArray.length);
如果緩沖區有足夠的空間接受數組中的數據( buffer.remaining()>myArray.length),數據將會被復制到從當前位置開始的緩沖區,并且緩沖區位置會被提前所增加數據元素的數量。如果緩沖區中沒有足夠的空間,那么不會有數據被傳遞,同時拋出一個 BufferOverflowException 異常。也可以通過調用帶有一個緩沖區引用作為參數的 put()來在兩個緩沖區內進行批量傳遞。
buffer.put(srcBuffer);
這等價于(假設 dstBuffer 有足夠的空間):

while (srcBuffer.hasRemaining( )) {
dstBuffer.put (srcBuffer.get( ));
}

3創建緩沖區

上面講過有七種主要的緩沖區類,每一種都具有一種 Java 語言中的非布爾類型的原始類型數據。(第 8 種也在圖中顯示出來, MappedByteBuffer,是ByteBuffer 專門用于內存映射文件的一種特例。我們將會在通道內容時討論內存映射)。這些類沒有一種能夠直接實例化。它們都是抽象類,但是都包含靜態工廠方法用來創建相應類的新實例。
對于這一討論,我們將以 CharBuffer 類為例,但是對于其它六種主要的緩沖區類也是適用的: IntBuffer, DoubleBuffer, ShortBuffer, LongBuffer, FloatBuffer和 ByteBuffer。下面是創建一個緩沖區的關鍵函數,對所有的緩沖區類通用(要按照需要替換類名):

public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable
{
    // This is a partial API listing
    public static CharBuffer allocate (int capacity)
    public static CharBuffer wrap (char [] array)
    public static CharBuffer wrap (char [] array, int offset,int length)
    public final boolean hasArray( )
    public final char [] array( )
    public final int arrayOffset( )
}

新的緩沖區是由分配或包裝操作創建的。分配操作創建一個緩沖區對象并分配一個私有的空間來儲存容量大小的數據元素。包裝操作創建一個緩沖區對象但是不分配任何空間來儲存數據元素。它使用您所提供的數組作為存儲空間來儲存緩沖區中的數據元素。要分配一個容量為 100 個 char 變量的 Charbuffer:
CharBuffer charBuffer = CharBuffer.allocate (100);,我們看他的構造函數:

HeapCharBuffer(int cap, int lim) {            // package-private
        super(-1, 0, lim, cap, new char[cap], 0);
 }
CharBuffer(int mark, int pos, int lim, int cap,   // package-private
                 char[] hb, int offset)
    {
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }

其實就是在底層生成了一個char數組,并設置了mark position limit capacity屬性。大家能看到,這個Buffer就是圍繞數組展開的。
如果您想提供您自己的數組用做緩沖區的備份存儲器,請調用 wrap()函數:
char [] myArray = new char [100];
CharBuffer charbuffer = CharBuffer.wrap (myArray);
wrap底層源碼:

public static CharBuffer wrap(char[] array) {
        return wrap(array, 0, array.length);
    }

public static CharBuffer wrap(char[] array,
                                    int offset, int length)
    {
        try {
            return new HeapCharBuffer(array, offset, length);
        } catch (IllegalArgumentException x) {
            throw new IndexOutOfBoundsException();
        }
    }

還是維護了一個數組,只不過這個數組不是自動生成的,而是程序員提供的。


通過 allocate()或者 wrap()函數創建的緩沖區通常都是間接的(直接緩沖區下一篇博文講)。間接的緩沖區使用備份數組,像我們之前討論的,您可以通過上面列出的API 函數獲得對這些數組的存取權。 Boolean 型函數 hasArray()告訴您這個緩沖區是否有一個可存取的備份數組。如果這個函數的返回 true, array()函數會返回這個緩沖區對象所使用的數組存儲空間的引用。如果 hasArray()函數返回 false,不要調用 array()函數或者 arrayOffset()函數。如果您這樣做了您會得到一個UnsupportedOperationException 異常。如果一個緩沖區是只讀的,它的備份數組將會是超出上界的,即使一個數組對象被提供給 wrap()函數。調用 array()函數或者 arrayOffset()會拋出一個 ReadOnlyBufferException 異常,來阻止您得到存取權來修改只讀緩沖區的內容。如果您通過其它的方式獲得了對備份數組的存取權限,對這個數組的修改也會直接影響到這個只讀緩沖區。


最后一個函數, arrayOffset(),返回緩沖區數據在數組中存儲的開始位置的偏移量(從數組頭 0 開始計算)。如果您使用了帶有三個參數的版本的 wrap()函數來創建一個緩沖區,對于這個緩沖區, arrayOffset()會一直返回 0,像我們之前討論的那樣。然而,如果您切分了由一個數組提供存儲的緩沖區,得到的緩沖區可能會有一個非 0 的數組偏移量。這個數組偏移量和緩沖區容量值會告訴您數組中哪些元素是被緩沖區使用的,這個馬上會講到。


4.復制緩沖區

如我們剛剛所討論的那樣,可以創建描述從外部存儲到數組中的數據元素的緩沖區對象。但是緩沖區不限于管理數組中的外部數據。它們也能管理其他緩沖區中的外部數據。當一個管理其他緩沖器所包含的數據元素的緩沖器被創建時,這個緩沖器被稱為視圖緩沖器。大多數的視圖緩沖器都是 ByteBuffer的視圖。在繼續前往字節緩沖器的細節之前,我們先將注意力放在所有存儲器類型的共同視圖上。
視圖存儲器總是通過調用已存在的存儲器實例中的函數來創建。使用已存在的存儲器實例中的工廠方法意味著視圖對象為原始存儲器的內部實現細節私有。數據元素可以直接存取,無論它們是存儲在數組中還是以一些其他的方式,而不需經過原始緩沖區對象的 get()/put()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 CharBuffer duplicate() {
        return new HeapCharBuffer(hb,
                                        this.markValue(),
                                        this.position(),
                                        this.limit(),
                                        this.capacity(),
                                        offset);
    }

看源碼一目了然,比上面講一大堆都好用,duplicate(),簡單講,就是生成一個新的Buffer對象,這個buffer對象引用了原對象底層數組的引用。但彼此維護著各自的mark position limit capacity offset等屬性,其實,對底層數組的寫操作是線程不安全的,大家要注意。


您 可 以 使 用 asReadOnlyBuffer() 函 數 來 生 成 一 個 只 讀 的 緩 沖 區 視 圖 。 這 與duplicate()相同,除了這個新的緩沖區不允許使用 put(),并且其 isReadOnly()函數將 會 返 回 true 。 對 這 一 只 讀 緩 沖 區 的 put() 函 數 的 調 用 嘗 試 會 導 致 拋 出ReadOnlyBufferException 異常。

    public CharBuffer asReadOnlyBuffer() {

        return new HeapCharBufferR(hb,
                                     this.markValue(),
                                     this.position(),
                                     this.limit(),
                                     this.capacity(),
                                     offset);
    }

HeapCharBufferR繼承了HeapCharBuffer類,這個類將isReadOnly屬性設置為true

    protected HeapCharBufferR(char[] buf,
                                   int mark, int pos, int lim, int cap,
                                   int off)
    {
        super(buf, mark, pos, lim, cap, off);
        this.isReadOnly = true;

    }

一目了然,多嗎?


分割緩沖區與復制相似,但 slice()創建一個從原始緩沖區的當前位置開始的新緩沖區,并且其容量是原始緩沖區的剩余元素數量(limit-position)。這個新緩沖區與原始緩沖區共享一段數據元素子序列。分割出來的緩沖區也會繼承只讀和直接屬性。

   public CharBuffer slice() {
        return new HeapCharBuffer(hb,
                                        -1,
                                        0,
                                        this.remaining(),
                                        this.remaining(),
                                        this.position() + offset);
    }
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378