《Java源碼分析》:Java NIO 之 Buffer

[轉載]: https://blog.csdn.net/u010412719/article/details/52775637

《Java源碼分析》:Java NIO 之 Buffer

在上篇博文中,我們介紹了Java NIO 中Channel 和Buffer的基本使用方法,這篇博文將從源碼的角度來看下Buffer的內部實現。

在Java API文檔中,對Buffer的說明摘入如下:

Buffer:是一個用於特定基本數據類型的容器。這裏的特定基本數據類型指的是:除boolean類型的其他基本上數據類型。

緩衝區是特定基本數據類型元素的線性有限序列。除內容外,緩衝區餓基本屬性還包括三個重要的屬性,如下:

1、capacity(容量):表示Buffer容量的大小。

2、limit(限制):是第一個不應該讀取或寫入的元素的索引。緩衝區的限制不能爲負數,並且不能大於其容量。

3、position(位置):表示下一個要讀取或寫入的元素的索引。緩衝區的位置不能爲負數,並且不能大於其限制。

剛看文檔的時候,可能對limit和position的含義不是太能理解。這個我們看完源碼之後,就會懂了哈,不急。

不過在看源碼之前,可以提前來通過畫圖的方式來解釋下limit 和 position。

在Buffer中有兩種模式,一種是寫模式,一種是讀模式。

有了上圖應該比較好理解position和limit的含義了。

下面就看下Buffer的源代碼了。由於Buffer有很多子類,這裏主要以IntBuffer爲例。

Buffer的幾個重要屬性

    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;//位置
    private int limit;//限制
    private int capacity;//容量
  • 1
  • 2
  • 3
  • 4
  • 5

這幾個屬性就也就是我們上面說到的position、limit、capacity,最後還有一個mark。

Buffer的構造方法

    //構造函數,根據指定的參數來初始化Buffer特定的屬性
    //此構造函數時包私有的
    Buffer(int mark, int pos, int lim, int cap) {       // package-private
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

構造函數中的邏輯相當簡單,就是一個初始化,不過在裏面包括的相關的有效性檢查。

要注意的是此構造函數是包私有的。
  • 1
  • 2

可能有人像我一樣,會有這樣一個疑問:Buffer類是抽象類,爲什麼會有構造函數呢?

由於關於抽象類中有構造函數,自己也是第一次見到。因此查閱的相關資料。發現如下:

1、抽象類是可以有構造函數的。但很多人認爲,構造函數用於實例化一個對象(或建立一個對象的實例),而抽象類不能被實例化,所以抽象類不應該有公共的構造函數。但不應該有“公共”的構造函數,和不應該有構造函數,這是兩個不同的概念,所以,如果抽象類需要構造函數,那麼應該聲明爲“protected”。

2、既然抽象類是可以,甚至有時候應該有構造函數,那抽象類的構造函數的作用是什麼?我覺得至少有兩個:

(1)初始化抽象類的成員;

(2)爲繼承自它的子類使用。

在Buffer這個抽象類中我們可以明顯的感受到以上兩點的作用。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3、即使我們聲明一個沒有構造函數的抽象類,編譯器還會爲我們生成一個默認的保護級別的構造函數。子類實例化時(不管是否爲帶參構造)只會調用所有父類的無參構造函數,而帶參構造必須通過顯式去調用.調用順序是先調用抽象類的無參構造函數。如果子類實例化時是使用帶餐的構造函數,則再接着調用抽象類的帶參構造函數,最後調用子類本身的構造函數。

在構造函數,涉及到兩個函數:limit(int newLimit)和position(int newPosition),如下:

    //函數的功能:設置Buffer的limit,如果position大於newLimit,則將position設置爲新的limit。
    //如果mark被定義且大於新的limit,則就會被拋棄。
    public final Buffer limit(int newLimit) {
        //有效性檢查,即limit必須滿足這樣的關係:0<=limit<=position.
        if ((newLimit > capacity) || (newLimit < 0)) 
            throw new IllegalArgumentException();
        limit = newLimit;
        //如果position大於newLimit,則將position設置爲新的limit。
        if (position > limit) position = limit;
        //如果mark被定義且大於新的limit,則會被拋棄(即設置爲-1)
        if (mark > limit) mark = -1;
        return this;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

limit(int newLimit)此函數的功能:簡單來說就是設置limit。

    //函數功能:設置Buffer的position.如果mark被定義且大於new position,則就會被拋棄。
    public final Buffer position(int newPosition) {
        //有效性檢查,即0<=newPosition<=limit.
        if ((newPosition > limit) || (newPosition < 0))
            throw new IllegalArgumentException();
        position = newPosition;
        //如果mark被定義且大於new position,則就會被拋棄。
        if (mark > position) mark = -1;
        return this;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

position(int newPosition)此函數的功能簡單來說:就是設置position.

這兩個函數都比較簡單哈,沒有任何的難點。

Buffer中常見的方法介紹

在上篇博文的Demo代碼中,我們會看到使用了Buffer中如下幾個函數:

1、allocate :分配一段空間的Buffer對象

2、put :用於往Buffer中添加元素

3、flip():用於將寫模式轉化爲讀模式

4、hasRemaining:判斷Buffer中是否還有元素可讀

5、get():讀取Buffer中position位置的元素

6、clear() 或者是 compact()方法:清除元素

下面我們主要就看下以上幾個方法的源代碼

1、allocate(int cap)方法介紹

由於Buffer類是一個抽象類,是不可以實例化對象的,因此在Buffer中是不存在allocate(int cap)方法的,allocate(int cap)方法在其子類中均有實現。這裏就以IntBuffer爲例,看下Buffer子類IntBuffer的allocate(int cap)方法。

    public static IntBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapIntBuffer(capacity, capacity);
    }
  • 1
  • 2
  • 3
  • 4
  • 5

函數功能:分配一個新的用來裝載int類型數據的Buffer對象實例。這個new buffer的position爲0,limit爲capacity,mark爲未定義的(即爲-1)。buffer中的元素全部初始化爲零。

在allocate方法中直接是實例化了一個IntBuffer子類的對象。

既然這裏涉及要HeapIntBuffer類,我們就看下這個類

    public abstract class IntBuffer
        extends Buffer
        implements Comparable<IntBuffer>

    class HeapIntBuffer
        extends IntBuffer  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

從繼承關係我們知道:HeapIntBuffer這個類是IntBuffer的子類

這個類的構造函數爲

    HeapIntBuffer(int cap, int lim) {            // package-private

        super(-1, 0, lim, cap, new int[cap], 0);

    }
  • 1
  • 2
  • 3
  • 4
  • 5

在這個構造函數中直接調用了父類IntBuffer中對應的構造函數。看下IntBuffer類中的這個構造函數。

在看構造函數之前,這裏要說下IntBuffer類中的幾個屬性。

    final int[] hb;                  // Non-null only for heap buffers
    final int offset;
    boolean isReadOnly;                 // Valid only for heap buffers
  • 1
  • 2
  • 3

IntBuffer類中主要包括一個int類型的數組,即從這裏我們知道,Buffer類的底層數據結構是藉助於數組來完成的。

繼續看IntBuffer類的構造方法

    // Creates a new buffer with the given mark, position, limit, capacity,
    // backing array, and array offset
    //
    IntBuffer(int mark, int pos, int lim, int cap,   // package-private
                 int[] hb, int offset)
    {
        super(mark, pos, lim, cap);//調用父類Buffer中對應有參的構造函數
        this.hb = hb;
        this.offset = offset;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

以上,就是當我們使用如下代碼得到IntBuffer實例的整個過程。 
IntBuffer buffer = IntBuffer.allocate(cap);

2、put(int i)方法介紹

下面來看IntBuffer類中的put方法

    public abstract IntBuffer put(int i);

    public abstract int get(int index);
  • 1
  • 2
  • 3

在IntBuffer類中put、get方法都是抽象的。 
有了上面allocate方法的過程分析,我們知道IntBuffer buffer = IntBuffer.allocate(cap) 
中的buffer實際上是父類的引用指向的是子類的對象。當我們使用buffer.put(value)/buffer.get(). 
實際上是調用子類HeapIntBuffer類中的put/get方法,這就是多態。在Java中相當重要的一個特徵。

HeapIntBuffer類中put方法的實現如下:

    public IntBuffer put(int x) {

        hb[ix(nextPutIndex())] = x;
        return this;

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

put方法實現的思想就是:將值存儲在position位置即可。

這裏涉及到兩個新的函數,分別爲:

1、nextPutIndex(),函數功能:簡單來說就是返回下一個要寫入元素的索引位置(即當前position值),並將position進行加一操作。

2、ix(int i):偏移offset個位置

這兩個函數的實現如下:

    //函數功能:首先檢查當前的position是否小於limit,如果小於則存儲,否則拋異常。
    final int nextPutIndex() {                          // package-private
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }
    //函數功能:偏移offset個位置
    protected int ix(int i) {  
        return i + offset;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

3、get()方法介紹

看完了put方法,接下來來看下get方法

函數的功能:取得Buffer中position位置所指向的元素。

    //函數功能:獲取buffer中position所指向的元素。
    public int get() {
        return hb[ix(nextGetIndex())];
    }
    //函數功能:獲取buffer中索引爲i位置的元素
    public int get(int i) {
        return hb[ix(checkIndex(i))];
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在get()方法中也涉及到兩個另外的函數:

1、nextGetIndex(),函數功能:返回下一個讀取的元素的索引位置(即position值)

2、ix(int i)

    final int nextGetIndex() {                          // package-private
        if (position >= limit)
            throw new BufferUnderflowException();
        return position++;
    }
    //將position向右移動nb個位置,這個函數目前還沒有看見在哪裏得到的應用
    final int nextGetIndex(int nb) {                    // package-private
        if (limit - position < nb)
            throw new BufferUnderflowException();
        int p = position;
        position += nb;
        return p;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

以上就是get方法的內部實現,也相當簡單哈。

4、flip()方法介紹

其實,在剛開始看別人博客的時候,是最不能理解這個函數的功能的,疑問在於:爲什麼要在讀Buffer中的內容時,要先調用這個方法呢。

其實在自己把Buffer的讀模式和寫模式弄清楚之後,這個函數的功能我也就明白了。

    /**
     * Flips this buffer.  The limit is set to the current position and then
     * the position is set to zero.  If the mark is defined then it is
     * discarded.
     *
     * <p> After a sequence of channel-read or <i>put</i> operations, invoke
     * this method to prepare for a sequence of channel-write or relative
     * <i>get</i> operations.  For example:
     *
     * <blockquote><pre>
     * buf.put(magic);    // Prepend header
     * in.read(buf);      // Read data into rest of buffer
     * buf.flip();        // Flip buffer
     * out.write(buf);    // Write header + data to channel</pre></blockquote>
     *
     */
     //函數功能:將Buffer從寫模式轉化爲讀模式。
     //具體爲:將limit設置爲此時position並且將position設置爲零。如果mark被定義了則被拋棄(即設置爲-1)
     //這個方法的應用場景爲:在管道讀或調用put方法之後,調用此方法來爲管道寫或者是get操作做準備。
    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

以上就是flip()方法的介紹,是不是也比較簡單哈。

5、hasRemaining()介紹

函數功能:判斷Buffer中是否還有可讀取的元素。


    /**
     * Tells whether there are any elements between the current position and
     * the limit.
     */
    public final boolean hasRemaining() {
        return position < limit;
    }   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

此方法的內部實現直接是判斷position是否小於limit. 
所以,邏輯也是相當簡單的。

6、clear() compact()方法的介紹

clear()函數功能:清空Buffer中所有的元素。

其內部實現直接是將Buffer裏面幾個屬性進行了重置。

    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最後看下compact方法

compact()方法的功能:將已讀元素進行清除,未讀元素拷貝保留並拷貝到Buffer最開始位置。這個也是和clear()方法不同的地方。

具體實現如下:

    public IntBuffer compact() {
        //先進行拷貝,即將剩餘的沒有訪問的元素拷貝到Buffer從零開始的位置
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());//remaining()函數返回的是剩餘元素個數
        //設置position爲下一個寫入元素的位置
        position(remaining());
        //設置limit爲Buffer容量
        limit(capacity());
        discardMark();//廢棄mark,即將mark設置爲-1
        return this;

    }

    public final int remaining() {
        return limit - position;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

小結

以上就將Buffer的內部實現給過了一篇,過了一篇之後的感受就是其內部實現邏輯是相當相當的簡單的哈。

我們只需要理解了Buffer中幾種重要的屬性:position、limit、capacity在讀模式和寫模式的含義的區別就可以很好的理解Buffer的內部實現了。

接下來自己還會看下Java NIO中Selector的內部實現。

完。

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