Java NIO 中Buffer 緩沖區解析

一、Buffer 簡介

Java NIO 中的 Buffer 用於和 NIO 通道進行交互。數據是通道讀取到緩沖區

緩沖區本質上是一塊可以寫入數據,然後可以從中讀取數據的內存。這塊內存被包裝 NIO Buffer 對象,並且提供瞭一組方法,用來方便的訪問這塊內存。緩沖區世紀上一個容器對象,更直接的說,其實就是一個數組,在 NIO 庫中,所有數據都是用緩沖區處理的。 在讀取數據時,它直接讀到緩沖區中;在寫數據時,它也是寫入到緩沖區中的;任何時候訪問 NIO 中的數據,都是將它放到緩沖區中。而在面向流 I/O 系統中,所有數據都是直接寫入或者直接將數據讀取到 Stream 對象中。

在 NIO 中,所有的緩沖區類型都是繼承於抽象類 Buffer, 最常用的就是 ByteBuffer. 對於 Java 中國呢的基本類型,金額都有一個具體 Buffer 類型與之對應,他們之間的繼承關系如下圖所示:

二、Buffer 的基本方法

1、使用 Buffer 讀寫數據

使用 Buffer 讀寫數據,一般遵循以下四個步驟:

(1)寫數據到 Buffer

(2)調用flip() 方法

(3)從 Buffer 中讀取數據

(4)調用 clear() 方法或者 compact() 方法

當向 buffer 寫數據時,buffer 會記錄下寫瞭多少數據。一旦要讀數據,需要通過 flip() 方法將 buffer 從寫模式切換到讀模式。在讀模式下,可以讀取到之前寫入到 buffer 的所有數據。一旦讀完瞭所有的數據,就需要清空緩沖區,讓它可以再次被寫入。有兩種方式能清空緩沖區:調用 clear() 或者 compact() 方法。clear() 方法會清空整個緩沖區。compact() 方法隻會清除已經度過的數據。任何未讀取的數據都被移動到瞭緩沖區的起始處,新寫的數據將放到緩沖區未讀數據的後面。

2、使用 Buffer 的例子

@Test
public void buffer01() throws IOException {
    // FileChannel
    String pathName = "/Users/zhengsh/sourcecode.io/zhengsh-vvip/nio/src/main/resources/01.txt";
    RandomAccessFile accessFile =
        new RandomAccessFile(pathName, "rw");
    FileChannel channel = accessFile.getChannel();

    // 創建 buffer , 大小
    ByteBuffer buffer = ByteBuffer.allocate(1024);

    // 讀
    int bytesRead = channel.read(buffer);
    while (bytesRead != -1) {
        // read 模式
        buffer.flip();

        while (buffer.hasRemaining()) {
            System.out.println((char) buffer.get());
        }
        buffer.clear();

        channel.read(buffer);
    }

    accessFile.close();
}


@Test
public void buffer02() {

    // 創建 buffer
    IntBuffer buffer = IntBuffer.allocate(8);
    for (int i = 0; i < buffer.capacity(); i++) {
        int j = 2 * (i + 1);
        buffer.put(j);
    }

    // 重置緩沖區
    buffer.flip();

    while (buffer.hasRemaining()) {
        int value = buffer.get();
        System.out.println(value + "  ");
    }
}

三、Buffer 的 capactity、posittion 和limit

為瞭理解 Buffer 的工作原理,需要熟悉它的三個屬性:

  • capacity
  • ponstition
  • limit

position limit 的含義取決於 Buffer 處在讀模式還是寫模式。不管 buffer 處於什麼模式,capactity 的含義總是一樣的。

這裡有一個關於 capacity, postition 和 limit 在讀模式中的說明:

(1) capactiy

作為一個內存塊,Buffer 有一個固定的大小值,也叫做 “capactiy” . 你隻能往裡面寫 capacity 個 byte

long、 char等類型。一旦 buffer 滿瞭,需要將其清空(通過讀數據或者清除數據)才能繼續寫數據往裡寫數據。

(2) postition

1)寫數據到 Bufer 中時,position 表示寫入數據的當前位置,position 的初始值為 0 。當一個 byte,long,等數據寫入到 buffer 後,position 會向下移動到下一個可插入的元素的 buffer 但願。position 最大可為 capacity -1 (因為 position 的初始值為 0)

2)讀數據到 Buffer 中時,position 表示讀數據的當前位置,如 position = 2 時表示已經開始讀瞭 3 個 byte, 或者從第三個 byte 開始讀取,通過 ByteBuffer.flip() 切換到讀模式 position 會被重置為 0, 當 Buffer 從 position 讀入數據後,position 會下移到下一個可讀入的數據 Buffer 單元。

(3) limit

1)寫數據時, limit 表示可以對 Buffer 最多寫入多少個數據。寫模式下,limit 等於 Buffer 的 capactiy

2)讀數據時, limit 表示 Buffer 裡有多少可讀數據(not null 的數據),因此能讀取到之前寫入的所有數據(limit 被設置為已寫數據的數量,這個值在寫模式下就是 position)

四、Buffer 的類型

Java NIO 有一下 Buffer 的類型

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • LongBuffer
  • ShortBuffer

這些 buffer 類型都代表瞭不同的數據類型。換句話說,就是可以通過 char ,short, int, long , float 或者 double 類型來操作緩沖區的字節。

五、Buffer 分配和寫數據

1、 Buffer 分配

想要獲取一個 Buffer 對象首先要進行分配。每一個 Buffer 類都有一個 allocate 方法。

下面是一個分配 48 字節 capactiy 的 ByteBuffer 的例子。

ByteBuffer buf = ByteBuffer.alloacte(48);

這是分配一個可存儲 1024 個字符的 CharBuffer:

ByteBuffer buf = ByteBuffer.alloacte(1024);

2、向 buffer 中寫數據

寫數據到 Buffer 有兩種方式:

  • (1)從 channel 寫到 Buffer
  • (2)通過 Buffer 的 put 方法寫到 Buffer 裡。

從 Channel 寫到 Buffer 的例子

int byteRead = channel.read(buf); // read into buffer

通過 put 方法寫入 buffer 的例子:

buf.put(100);

put 的方法有很多版本,允許你不同的方式把數據寫入到 buffer 中,例如,寫到一個指定的位置,或者把字節數組寫入到 Buffer .

3、flip() 方法

flip 方法將 Buffer 從寫模式切換到讀模式。調用 flip() 方法將會 position 設置為 0 , 並且將 limt 設置為之前 position 的值。換句話說,position 現在用於標記讀的位置,limit 表示之前寫進瞭多少個 byte, char 等(現在能讀取多少個 byte, char 等)。

六、從 Buffer 中讀取數據

從 Buffer 中讀取數據到 Channel 中:

(1) 從 Buffer 中讀取數據到 Channel

(2)使用 get 方法從 Buffer 中讀取數據

從 Buffer 中讀取數據到 Channnel 到例子:

// read form buffer into channel 
int bytesWritten = inChannel.write(buf);

使用 get() 方法從 Buffer 中讀取數據的例子:

byte aByte = buf.get();

get 方法中有很多版本,允許你以不同的方式 Buffer 中讀取數據。例如,從指定 position 讀取,或者從 Buffer 中讀取到字節數組。

七、Buffer 幾個方法

1、rewind() 方法

Buffer.rewind() 將 position 返回0, 所以你可以重讀 Buffer 中的所有數據。limit 保持不變,仍然表示能從 Buffer 中讀取到多少個元素(byte, char 等)。

2、clear() 與 compact() 方法

一旦讀完 Buffer 中的數據,需要讓 Buffer 準備好再次被寫入。可以通過 clear() 或 compact() 方法來完成

如果調用的是 cleanr () 方法,position 兼備設置為 0 , limit 被設置成 capactiy 的值。換句話說,Buffer 被清空瞭。 Buffer 中的數據並未清除,隻是這些標記高數我們從哪裡開始往 Buffer 中寫數據。

如果 Buffer 中有些數未讀的數據,調用 clear() 方法,數據將 “被遺忘”,意味著不在有任何標記會告訴你那些數據被讀過,那些還沒有。

如果 Buffer 中依然有未讀的數據,且後續還需要這些數據,但是此時想要先寫這些數據,那麼使用 compact() 方法。

compact() 方法將所有未讀的數據拷貝到 Buffer 起始處。然後將 position 設置到最後一個未讀元素正後面。 limit 屬性依然像 clear() 方法一樣。設置成 capacity. 現在 Buffer 準備好寫數據瞭,但是不會覆蓋未讀的數據。

3、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

八、緩沖區操作

1、緩沖區分片

在 NIO 中除瞭可以分配或者包裝一個緩沖區對象外,還可以更具現有的緩沖區對象來創建一個子緩沖區,即現有緩沖區上切出一片來作為一個新的緩沖區,但現有的緩沖區與創建的子緩沖區在底層數組層面上是數據共享的,也就是說,子緩沖區相當於是現有緩沖區的一個視圖窗口。調用 slice() 方法可以創建一個子緩沖區。

// 緩沖區分片
@Test
public void b01() {
    ByteBuffer buffer = ByteBuffer.allocate(10);
    // 放入數據
    for (int i = 0; i < buffer.capacity(); i++) {
        buffer.put((byte) i);
    }

    // 創建子緩沖區
    buffer.position(3);
    buffer.limit(7);
    ByteBuffer slice = buffer.slice();

    // 改變子緩沖區中的內容
    for (int i = 0; i < slice.capacity(); i++) {
        byte b = slice.get(i);
        b *= 10;
        slice.put(i, b);
    }

    // 復位
    buffer.position(0);
    buffer.limit(buffer.capacity());

    while (buffer.remaining() > 0) {
        System.out.println(buffer.get());
    }
}

輸出結果如下:

2、隻讀緩沖區

隻讀緩沖區非常簡單,可以讀取他們,但是不能向他們寫入數據。可以通過調用緩沖區的 asReadOnlyBufer() 方法,將任何常規緩沖區轉換為隻讀緩沖區,這個方法返回一個與原緩沖區完全相同的緩沖區,並與原緩沖區共享數據,隻不過它是隻讀的。如果原緩沖區的內容發生瞭變化,隻讀緩沖區的內容隨之發生變化:

// 隻讀緩沖區
@Test
public void b02() {
    ByteBuffer buffer = ByteBuffer.allocate(10);

    // 放入數據
    for (int i = 0; i < buffer.capacity(); i++) {
        buffer.put((byte) i);
    }

    // 創建一個隻讀緩沖區
    ByteBuffer readOnlyBuf = buffer.asReadOnlyBuffer();
    for (int i = 0; i < buffer.capacity(); i++) {
        byte b = buffer.get(i);
        b *= 10;
        buffer.put(i, b);
    }

    readOnlyBuf.position(0);
    readOnlyBuf.limit(readOnlyBuf.capacity());

    while (readOnlyBuf.remaining() > 0) {
        System.out.println(readOnlyBuf.get());
    }
}

3、直接緩沖區

直接緩沖區為瞭加快 I/O 速度,使用一種特殊的方式為其分配內存的緩沖區, JDK 文檔的描述為:給定一個直接字節緩沖區,Java 虛擬機將盡最大努力直接對它執行本機 I/O 操作之前(或之後),嘗試避免將緩沖區的內容拷貝到一個中間緩沖區中或者從一個中間緩沖區中拷貝數據。要分配直接緩沖區,需要調 allocatieDirect() 方法,而不是 alloacte() 方法,使用方式與普通緩沖區並無區別。

// 直接緩沖區,文件拷貝
@Test
public void b03() throws IOException {
    String filePath = "/xx/01.txt";
    FileInputStream inputStream = new FileInputStream(filePath);
    FileChannel fileInChannel = inputStream.getChannel();

    String outPath = "/xx/02.txt";
    FileOutputStream outputStream = new FileOutputStream(outPath);
    FileChannel fileOutChannel1 = outputStream.getChannel();

    //  使用 allocateDirect , 而不是 allocate
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

    while (true) {
        buffer.clear();
        int r = fileInChannel.read(buffer);
        if (r == -1) {
            break;
        }
        buffer.flip();
        fileOutChannel1.write(buffer);
    }
    fileInChannel.close();
    fileOutChannel1.close();
}

4、內存映射文件 I/O

內存映射文件 I/O 是一種讀和寫文件數據的方法,它可以比常規的基於流或者通道的 I/O 快得多。內存映射 I/O 是通過使文件中的數據出現為內存數組的內容來完成的,這起初聽起來師傅不過就是為瞭將整個文件讀取到內容中,但是事實上並不是這樣的。一般來說隻有文件實際讀取或者寫入的部分才會映射到內存中。

// 內存映射文件 I/O 
@Test
public void b04() throws IOException {
    String filePath = "/xxx/01.txt";
    RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");
    FileChannel fileChannel = randomAccessFile.getChannel();
    MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
    mappedByteBuffer.put(0, (byte) 97);
    mappedByteBuffer.put(1023, (byte) 122);
    fileChannel.close();
}

到此這篇關於Java NIO 中Buffer 緩沖區解析的文章就介紹到這瞭,更多相關Java NIO 中Buffer 緩沖區內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: