1. 程式人生 > 其它 >java基礎:NIO之Buffer(2)

java基礎:NIO之Buffer(2)

Buffer簡介

Java NIO 中的 Buffer 用於和 NIO 通道進行互動。資料是從通道讀入緩衝區,從緩衝區寫入到通道中的。

緩衝區本質上是一塊可以寫入資料,然後可以從中讀取資料的記憶體。這塊記憶體被包裝成 NIO Buffer 物件,並提供了一組方法,用來方便的訪問該塊記憶體。緩衝區實際上是一個容器物件,更直接的說,其實就是一個數組,在 NIO 庫中,所有資料都是用緩衝區處理的。在讀取資料時,它是直接讀到緩衝區中的; 在寫入資料時,它也是寫入到緩衝區中的;任何時候訪問 NIO 中的資料,都是將它放到緩衝區中。而在面向流 I/O系統中,所有資料都是直接寫入或者直接將資料讀取到 Stream 物件中。

在 NIO 中,所有的緩衝區型別都繼承於抽象類Buffer,最常用的就是 ByteBuffer,對於 Java 中的基本型別,基本都有一個具體 Buffer 型別與之相對應,它們之間的繼承關係如下圖所示:

Buffer 的基本用法

1、使用 Buffer 讀寫資料,一般遵循以下四個步驟:

(1)寫入資料到 Buffer

(2)呼叫 flip()方法

(3)從 Buffer 中讀取資料

(4)呼叫 clear()方法或者 compact()方法

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

IntBuffer示例:

        // 分配新的 int 緩衝區,引數為緩衝區容量
        // 新緩衝區的當前位置將為零,其界限(限制位置)將為其容量。
        // 它將具有一個底層實現陣列,其陣列偏移量將為零。
        IntBuffer buffer = IntBuffer.allocate(8);

        for (int i = 0; i < buffer.capacity(); ++i) {
            int j = 2 * (i + 1);
            // 將給定整數寫入此緩衝區的當前位置,當前位置遞增
            buffer.put(j);
        }

        // 重設此緩衝區,將限制設定為當前位置,然後將當前位置設定為 0
        buffer.flip();

        // 檢視在當前位置和限制位置之間是否有元素
        while (buffer.hasRemaining()) {
            // 讀取此緩衝區當前位置的整數,然後當前位置遞增
            int j = buffer.get();
            System.out.print(j + " ");
        }

Buffer 的 capacity、position 和 limit

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

  • capacity
  • position
  • limit

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

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

(1)capacity

作為一個記憶體塊,Buffer 有一個固定的大小值,也叫“capacity”.你只能往裡寫capacity 個 byte、long,char 等型別。一旦 Buffer 滿了,需要將其清空(通過讀資料或者清除資料)才能繼續寫資料往裡寫資料。

(2)position

1)寫資料到 Buffer 中時,position 表示寫入資料的當前位置,position 的初始值為 0。當一個 byte、long 等資料寫到 Buffer 後, position 會向下移動到下一個可插入 資料的 Buffer 單元。position 最大可為 capacity – 1(因為 position 的初始值為 0).

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

(3)limit

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

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

rewind、clear、clear、clear、reset

rewind()方法:

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

clear()與 compact()方法:

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

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

如果 Buffer 中有一些未讀的資料,呼叫 clear()方法,資料將“被遺忘”,意味著不再有任何標記會告訴你哪些資料被讀過,哪些還沒有。

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

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

mark()與 reset()方法:

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

緩衝區操作

緩衝區分片

在 NIO 中,除了可以分配或者包裝一個緩衝區物件外,還可以根據現有的緩衝區物件來建立一個子緩衝區,即在現有緩衝區上切出一片來作為一個新的緩衝區,但現有的緩衝區與建立的子緩衝區在底層陣列層面上是資料共享的,也就是說,子緩衝區相當於是現有緩衝區的一個檢視視窗。呼叫 slice()方法可以建立一個子緩衝區。

示例:

        ByteBuffer buffer = ByteBuffer.allocate(10);
        // 緩衝區中的資料 0-9
        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());
        }

結果:

只讀緩衝區

只讀緩衝區非常簡單,可以讀取它們,但是不能向它們寫入資料。可以通過呼叫緩衝區的 asReadOnlyBuffer()方法,將任何常規緩衝區轉換為只讀緩衝區,這個方法返回一個與原緩衝區完全相同的緩衝區,並與原緩衝區共享資料,只不過它是隻讀的。 如果原緩衝區的內容發生了變化,只讀緩衝區的內容也隨之發生變化:

        ByteBuffer buffer = ByteBuffer.allocate(10);
        // 緩衝區中的資料 0-9
        for (int i = 0; i < buffer.capacity(); ++i) {
            buffer.put((byte) i);
        }
        // 建立只讀緩衝區
        ByteBuffer readonly = buffer.asReadOnlyBuffer();
        // 改變原緩衝區的內容
        for (int i = 0; i < buffer.capacity(); ++i) {
            byte b = buffer.get(i);
            b *= 10;
            buffer.put(i, b);
        }
        readonly.position(0);
        readonly.limit(buffer.capacity());
        // 只讀緩衝區的內容也隨之改變
        while (readonly.remaining() > 0) {
            System.out.println(readonly.get());
        }

結果:

如果嘗試修改只讀緩衝區的內容,則會報 ReadOnlyBufferException 異常。只讀緩衝區對於保護資料很有用。在將緩衝區傳遞給某個物件的方法時,無法知道這個方法是否會修改緩衝區中的資料。建立一個只讀的緩衝區可以保證該緩衝區不會被修改。只可以把常規緩衝區轉換為只讀緩衝區,而不能將只讀的緩衝區轉換為可寫的緩衝區。

直接緩衝區

直接緩衝區是為加快 I/O 速度,使用一種特殊方式為其分配記憶體的緩衝區,JDK 文件中的描述為:給定一個直接位元組緩衝區,Java 虛擬機器將盡最大努力直接對它執行本機 I/O 操作。也就是說,它會在每一次呼叫底層作業系統的本機 I/O 操作之前(或之後), 嘗試避免將緩衝區的內容拷貝到一箇中間緩衝區中或者從一箇中間緩衝區中拷貝資料。要分配直接緩衝區,需要呼叫 allocateDirect()方法,而不是 allocate()方法,使用方式與普通緩衝區並無區別。

拷貝檔案示例:

        String infile = "1.txt";
        FileInputStream fin = new FileInputStream(infile);
        FileChannel fcin = fin.getChannel();
        String outfile = "2.txt";
        FileOutputStream fout = new FileOutputStream(outfile);
        FileChannel fcout = fout.getChannel();
        // 使用 allocateDirect,而不是 allocate
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        while (true) {
            buffer.clear();
            int r = fcin.read(buffer);
            if (r == -1) {
                break;
            }
            buffer.flip();
            fcout.write(buffer);
        }

記憶體對映檔案 I/O

記憶體對映檔案 I/O 是一種讀和寫檔案資料的方法,它可以比常規的基於流或者基於通道的 I/O 快的多。記憶體對映檔案 I/O 是通過使檔案中的資料出現為記憶體陣列的內容來完成的,這其初聽起來似乎不過就是將整個檔案讀到記憶體中,但是事實上並不是這樣。一般來說,只有檔案中實際讀取或者寫入的部分才會對映到記憶體中。

示例:

        RandomAccessFile raf = new RandomAccessFile("1.txt", "rw");
        FileChannel fc = raf.getChannel();
        MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 2);
        mbb.put(0, (byte) 97);
        mbb.put(1, (byte) 122);
        raf.close();