Java NIO 之 Buffer(緩衝區)

SnailClimb發表於2018-05-14

歷史回顧:

Java NIO 概覽

其他高贊文章:

面試中關於Redis的問題看這篇就夠了

一文輕鬆搞懂redis叢集原理及搭建與使用

一 Buffer(緩衝區)介紹

Java NIO Buffers用於和NIO Channel互動。 我們從Channel中讀取資料到buffers裡,從Buffer把資料寫入到Channels.

Buffer本質上就是一塊記憶體區,可以用來寫入資料,並在稍後讀取出來。這塊記憶體被NIO Buffer包裹起來,對外提供一系列的讀寫方便開發的介面。

在Java NIO中使用的核心緩衝區如下(覆蓋了通過I/O傳送的基本資料型別:byte, char、short, int, long, float, double ,long):

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • FloatBuffer
  • DoubleBuffer
  • LongBuffer

Java NIO中使用的核心緩衝區

利用Buffer讀寫資料,通常遵循四個步驟:

  1. 把資料寫入buffer;
  2. 呼叫flip;
  3. 從Buffer中讀取資料;
  4. 呼叫buffer.clear()或者buffer.compact()。

當寫入資料到buffer中時,buffer會記錄已經寫入的資料大小。當需要讀資料時,通過 flip() 方法把buffer從寫模式調整為讀模式;在讀模式下,可以讀取所有已經寫入的資料。

當讀取完資料後,需要清空buffer,以滿足後續寫入操作。清空buffer有兩種方式:呼叫 clear()compact() 方法。clear會清空整個buffer,compact則只清空已讀取的資料,未被讀取的資料會被移動到buffer的開始位置,寫入位置則近跟著未讀資料之後。

Buffer的容量,位置,上限(Buffer Capacity, Position and Limit)

Buffer緩衝區實質上就是一塊記憶體,用於寫入資料,也供後續再次讀取資料。這塊記憶體被NIO Buffer管理,並提供一系列的方法用於更簡單的操作這塊記憶體。

一個Buffer有三個屬性是必須掌握的,分別是:

  • capacity容量
  • position位置
  • limit限制

position和limit的具體含義取決於當前buffer的模式。capacity在兩種模式下都表示容量。

下面有張示例圖,描訴了讀寫模式下position和limit的含義:

不同模式下position和limit的含義

容量(Capacity)

作為一塊記憶體,buffer有一個固定的大小,叫做capacit(容量)。也就是最多隻能寫入容量值得位元組,整形等資料。一旦buffer寫滿了就需要清空已讀資料以便下次繼續寫入新的資料。

位置(Position)

當寫入資料到Buffer的時候需要從一個確定的位置開始,預設初始化時這個位置position為0,一旦寫入了資料比如一個位元組,整形資料,那麼position的值就會指向資料之後的一個單元,position最大可以到capacity-1.

當從Buffer讀取資料時,也需要從一個確定的位置開始。buffer從寫入模式變為讀取模式時,position會歸零,每次讀取後,position向後移動。

上限(Limit)

在寫模式,limit的含義是我們所能寫入的最大資料量,它等同於buffer的容量。

一旦切換到讀模式,limit則代表我們所能讀取的最大資料量,他的值等同於寫模式下position的位置。換句話說,您可以讀取與寫入數量相同的位元組數(限制設定為寫入的位元組數,由位置標記)。

二 Buffer的常見方法

方法 介紹
abstract Object array() 返回支援此緩衝區的陣列 (可選操作)
abstract int arrayOffset() 返回該緩衝區的緩衝區的第一個元素的背襯陣列中的偏移量 (可選操作)
int capacity() 返回此緩衝區的容量
Buffer clear() 清除此快取區。將position = 0;limit = capacity;mark = -1;
Buffer flip() flip()方法可以吧Buffer從寫模式切換到讀模式。呼叫flip方法會把position歸零,並設定limit為之前的position的值。 也就是說,現在position代表的是讀取位置,limit標示的是已寫入的資料位置。
abstract boolean hasArray() 告訴這個緩衝區是否由可訪問的陣列支援
boolean hasRemaining() return position < limit,返回是否還有未讀內容
abstract boolean isDirect() 判斷個緩衝區是否為 direct
abstract boolean isReadOnly() 判斷告知這個緩衝區是否是隻讀的
int limit() 返回此緩衝區的限制
Buffer position(int newPosition) 設定這個緩衝區的位置
int remaining() return limit - position; 返回limit和position之間相對位置差
Buffer rewind() 把position設為0,mark設為-1,不改變limit的值
Buffer mark() 將此緩衝區的標記設定在其位置

三 Buffer的使用方式/方法介紹

分配緩衝區(Allocating a Buffer)

為了獲得緩衝區物件,我們必須首先分配一個緩衝區。在每個Buffer類中,allocate()方法用於分配緩衝區。

下面來看看ByteBuffer分配容量為28位元組的例子:

ByteBuffer buf = ByteBuffer.allocate(28);
複製程式碼

下面來看看另一個示例:CharBuffer分配空間大小為2048個字元。

CharBuffer buf = CharBuffer.allocate(2048);
複製程式碼

寫入資料到緩衝區(Writing Data to a Buffer)

寫資料到Buffer有兩種方法:

  • 從Channel中寫資料到Buffer
  • 手動寫資料到Buffer,呼叫put方法

下面是一個例項,演示從Channel寫資料到Buffer:

 int bytesRead = inChannel.read(buf); //read into buffer.
複製程式碼

通過put寫資料:

buf.put(127);
複製程式碼

put方法有很多不同版本,對應不同的寫資料方法。例如把資料寫到特定的位置,或者把一個位元組資料寫入buffer。看考JavaDoc文件可以查閱的更多資料。

翻轉(flip())

flip()方法可以吧Buffer從寫模式切換到讀模式。呼叫flip方法會把position歸零,並設定limit為之前的position的值。 也就是說,現在position代表的是讀取位置,limit標示的是已寫入的資料位置。

從Buffer讀取資料(Reading Data from a Buffer)

從Buffer讀資料也有兩種方式。

  • 從buffer讀資料到channel
  • 從buffer直接讀取資料,呼叫get方法

讀取資料到channel的例子:

int bytesWritten = inChannel.write(buf);
複製程式碼

呼叫get讀取資料的例子:

byte aByte = buf.get();
複製程式碼

get也有諸多版本,對應了不同的讀取方式。

rewind()

Buffer.rewind()方法將position置為0,這樣我們可以重複讀取buffer中的資料。limit保持不變。

clear() and compact()

一旦我們從buffer中讀取完資料,需要複用buffer為下次寫資料做準備。只需要呼叫clear()或compact()方法。

如果呼叫的是clear()方法,position將被設回0,limit被設定成 capacity的值。換句話說,Buffer 被清空了。Buffer中的資料並未清除,只是這些標記告訴我們可以從哪裡開始往Buffer裡寫資料。

如果Buffer還有一些資料沒有讀取完,呼叫clear就會導致這部分資料被“遺忘”,因為我們沒有標記這部分資料未讀。

針對這種情況,如果需要保留未讀資料,那麼可以使用compact。 因此 compact()clear() 的區別就在於: 對未讀資料的處理,是保留這部分資料還是一起清空

mark()與reset()方法

通過呼叫Buffer.mark()方法,可以標記Buffer中的一個特定position。之後可以通過呼叫Buffer.reset()方法恢復到這個position。例如:

buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset();  //set position back to mark.    
複製程式碼

equals() and compareTo()

可以用eqauls和compareTo比較兩個buffer

equals():

判斷兩個buffer相對,需滿足:

  • 型別相同
  • buffer中剩餘位元組數相同
  • 所有剩餘位元組相等

從上面的三個條件可以看出,equals只比較buffer中的部分內容,並不會去比較每一個元素。

compareTo():

compareTo也是比較buffer中的剩餘元素,只不過這個方法適用於比較排序的:

四 Buffer常用方法測試

這裡以ByteBuffer為例子說明抽象類Buffer的實現類的一些常見方法的使用:

package channel;

import java.nio.ByteBuffer;

public class ByteBufferMethods {
    public static void main(String args[]){
        //分配緩衝區(Allocating a Buffer)
        ByteBuffer buffer = ByteBuffer.allocate(33);

        System.out.println("-------------Test reset-------------");
        //clear()方法,position將被設回0,limit被設定成 capacity的值
        buffer.clear();
       // 設定這個緩衝區的位置
        buffer.position(5);
        //將此緩衝區的標記設定在其位置。沒有buffer.mark();這句話會報錯
        buffer.mark();
        buffer.position(10);
        System.out.println("before reset:      " + buffer);
        //將此緩衝區的位置重置為先前標記的位置。(buffer.position(5))
        buffer.reset();
        System.out.println("after reset:       " + buffer);

        System.out.println("-------------Test rewind-------------");
        buffer.clear();
        buffer.position(10);
        //返回此緩衝區的限制。
        buffer.limit(15);
        System.out.println("before rewind:       " + buffer);
        //把position設為0,mark設為-1,不改變limit的值
        buffer.rewind();
        System.out.println("before rewind:       " + buffer);

        System.out.println("-------------Test compact-------------");
        buffer.clear();
        buffer.put("abcd".getBytes());
        System.out.println("before compact:       " + buffer);
        System.out.println(new String(buffer.array()));
        //limit = position;position = 0;mark = -1; 翻轉,也就是讓flip之後的position到limit這塊區域變成之前的0到position這塊,
        //翻轉就是將一個處於存資料狀態的緩衝區變為一個處於準備取資料的狀態
        buffer.flip();
        System.out.println("after flip:       " + buffer);
        //get()方法:相對讀,從position位置讀取一個byte,並將position+1,為下次讀寫作準備
        System.out.println((char) buffer.get());
        System.out.println((char) buffer.get());
        System.out.println((char) buffer.get());
        System.out.println("after three gets:       " + buffer);
        System.out.println("\t" + new String(buffer.array()));
        //把從position到limit中的內容移到0到limit-position的區域內,position和limit的取值也分別變成limit-position、capacity。
        // 如果先將positon設定到limit,再compact,那麼相當於clear()
        buffer.compact();
        System.out.println("after compact:       " + buffer);
        System.out.println("\t" + new String(buffer.array()));

        System.out.println("-------------Test get-------------");
        buffer = ByteBuffer.allocate(32);
        buffer.put((byte) 'a').put((byte) 'b').put((byte) 'c').put((byte) 'd')
                .put((byte) 'e').put((byte) 'f');
        System.out.println("before flip():       " + buffer);
        // 轉換為讀取模式
        buffer.flip();
        System.out.println("before get():       " + buffer);
        System.out.println((char) buffer.get());
        System.out.println("after get():       " + buffer);
        // get(index)不影響position的值
        System.out.println((char) buffer.get(2));
        System.out.println("after get(index):       " + buffer);
        byte[] dst = new byte[10];
        buffer.get(dst, 0, 2);
        System.out.println("after get(dst, 0, 2):       " + buffer);
        System.out.println("\t dst:" + new String(dst));
        System.out.println("buffer now is:       " + buffer);
        System.out.println("\t" + new String(buffer.array()));

        System.out.println("-------------Test put-------------");
        ByteBuffer bb = ByteBuffer.allocate(32);
        System.out.println("before put(byte):       " + bb);
        System.out.println("after put(byte):       " + bb.put((byte) 'z'));
        System.out.println("\t" + bb.put(2, (byte) 'c'));
        // put(2,(byte) 'c')不改變position的位置
        System.out.println("after put(2,(byte) 'c'):       " + bb);
        System.out.println("\t" + new String(bb.array()));
        // 這裡的buffer是 abcdef[pos=3 lim=6 cap=32]
        bb.put(buffer);
        System.out.println("after put(buffer):       " + bb);
        System.out.println("\t" + new String(bb.array()));
    }
}
複製程式碼

參考:

官方JDK相關文件

谷歌搜尋排名第一的Java NIO教程

《Java程式設計師修煉之道》

ByteBuffer常用方法詳解

Java NIO 易百教程

歡迎關注我的微信公眾號:"Java面試通關手冊"(一個有溫度的微信公眾號,期待與你共同進步~~~堅持原創,分享美文,分享各種Java學習資源):

Java NIO 之 Buffer(緩衝區)

相關文章