Java NIO學習系列一:Buffer

木瓜芒果發表於2019-06-24

  前面三篇文章中分別總結了標準Java IO系統中的File、RandomAccessFile、I/O流系統,對於I/O系統從其繼承體系入手,力求對類數量繁多的的I/O系統有一個清晰的認識,然後結合一些I/O的常規用法來加深對標準I/O系統的掌握,感興趣的同學可以看一下:

  <<Java I/O系統學習系列一:File和RandomAccessFile>>

  <<Java I/O系統學習系列二:輸入和輸出>>

  <<Java I/O系統學習系列三:I/O流的典型使用方式>>

  從本文開始我會開始總結NIO部分,Java NIO(注意,這裡的NIO其實叫New IO)是用來替換標準Java IO以及Java 網路API的,其提供了一系列不同與標準IO API的方式來處理IO,從JDK1.4開始引入,其目的在於提高速度。

  之所以能夠提高速度是因為其所使用的結構更接近於作業系統執行I/O的方式:通道和緩衝器。我們可以把它想象成一個煤礦,通道是一個包含煤層(資料)的礦藏,而緩衝器則是派送到礦藏的卡車。卡車滿載煤炭而歸,我們再從卡車上獲得煤炭。也就是說,我們並沒有直接和通道互動,而是和緩衝器互動,並把緩衝器派送到通道。通道要麼從緩衝器獲得資料,要麼向緩衝器傳送資料。

   在標準IO的API中,使用位元組流和字元流。而在Java NIO中是使用Channel(通道)和Buffer(緩衝區),資料從channel中讀取到buffer中,或從buffer寫入到channel中。Java NIO類庫中的核心元件為:

  • Buffer
  • Channel
  • Selector

  本文中我們會著重總結Buffer相關的知識點(後面的文章中會繼續介紹Channel即Selector),本文主要會圍繞如下幾個方面展開:

  Buffer簡介

  Buffer的內部結構  

  Buffer的主要API

  ByteBuffer

  Buffer型別

  總結

 

1. Buffer簡介

  Java NIO中的Buffer一般和Channel配對使用。可以從Channel中讀取資料到Buffer,或者寫資料到Channel中。一個Buffer其實就是代表一個記憶體塊,你可以往裡面寫資料或者從中讀取資料。這個記憶體塊被包裝成一個Buffer物件,並且提供了一系列方法使得操作記憶體塊更便捷。

  通過Buffer來讀寫資料通常包括如下4步:

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

  當往Buffer中寫資料時,Buffer能夠記錄寫了多少資料。當要從Buffer中讀取資料時,就需要通過呼叫flip()方法將Buffer從寫模式切換到讀模式。一旦讀完所有資料,需要清空Buffer,讓它再次處於寫狀態。可以通過呼叫clear()或compact()方法來完成這一步:

  • clear()方法會清空整個Buffer;
  • compact()方法僅僅清空你已經從Buffer中讀取的資料,未讀資料會被移動到Buffer起始位置,可以緊接著未讀的資料寫入新的資料;

  如下是一個簡單的使用例子,通過FileChannel和ByteBuffer讀取pom.xml檔案,並逐位元組輸出:

public class BufferDemo {

    public static void main(String[] args) {
        try {
            RandomAccessFile raf = new RandomAccessFile("pom.xml","r");
            FileChannel channel = raf.getChannel();
            ByteBuffer buffer = ByteBuffer.allocate(48);
            int byteReaded = channel.read(buffer);
            while(byteReaded != -1) {
                buffer.flip();
                while(buffer.hasRemaining()) {
                    System.out.print((char)buffer.get());
                }
                buffer.clear();
                byteReaded = channel.read(buffer);
            }
            raf.close();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }    
}

 

2. Buffer的內部結構

  上面說到Buffer封裝了一塊記憶體塊,並提供了一系列的方法使得可以方便地操縱記憶體中的資料。至於如何操縱?Buffer提供了4個索引。要理解Buffer的工作原理,就需要從這些索引說起:

  • capacity(容量);
  • position(位置);
  • limit(界限);
  • mark(標記);

   其中position和limit的含義取決於Buffer是處於什麼模式(讀或者寫模式),capacity的含義則和模式無關,而mark則只是一個標記,可以通過mark()方法進行設定。下圖描述了讀寫模式下三種屬性分別代表的含義,詳細解釋見下文:

2.1 Capacity

  Buffer代表一個記憶體塊,所以其是有確定大小的,也叫“容量”。可以往buffer中寫入各種資料如byte、long、chars等,當Buffer被寫滿了則需要將其清空(可以通過讀取資料或者清空資料)之後才能繼續寫入資料。

2.2 Position

  當往Buffer中寫資料時,寫入的地方就是所謂的position,其初始值為0,最大值為capacity-1。當往Buffer中寫入一個byte或者long的資料時,position會前移以指向下一個即將被插入的位置。

  當從Buffer中讀取資料時,讀取資料的地方就是所謂的position。當執行flip將Buffer從寫模式切換到讀模式時,position會被重置為0。隨著不斷從Buffer讀取資料,position也會不斷後移指向下一個將被讀取的資料。

2.3 Limit

  在寫模式下,Buffer的limit是指能夠往Buffer中寫入多少資料,其值等於Buffer的capacity。

  在讀模式下,Buffer的limit是指能夠從Buffer讀取多少資料出來。因此當從寫模式切換到讀模式下時,limit就被設定為寫模式下的position的值(這很好理解,寫了多少才能讀到多少)。

 2.4 Mark

  mark其實就是一個標記,可以通過mark()方法設定,設定值為當前的position。

 

  下面是用於設定和復位索引以及查詢它們值的方法:


 

  capacity()      返回緩衝區容量
  clear()      清空緩衝區,將position設定為0,limit設定為容量。我們可以呼叫此方法覆寫緩衝區
  flip()       將limit設定為position,position設定為0。此方法用於準備從緩衝區讀取已經寫入的資料
  limit()        返回limit值
  limit(int lim)    設定limit值
  mark()       將mark設定為position
  position()     返回position值
  position(int pos)  設定position值
  remaining()    返回(limit - position)
  hasRemaining()  若有介於position和limit之間的元素,則返回true


 

3. Buffer的主要API

  除了如上和索引相關的方法之外,Buffer還提供了一些其他的方法用於寫入、讀取等操作。

3.1 給Buffer分配空間

  要獲得一個Buffer物件就可以通過Buffer類的allocate()方法來實現,如下分別是分配一個48位元組的ByteBuffer和1024字元的CharBuffer:

ByteBuffer buf = ByteBuffer.allocate(48);
CharBuffer buf = CharBuffer.allocate(1024);

3.2 往Buffer中寫資料

  有兩種方式往Buffer中寫入資料:

  • 從Channel中往Buffer寫資料;
  • 通過Buffer的put()方法寫入資料;
int bytesRead = inChannel.read(buf); // read into buffer
buf.put(127);

  put()方法有多個過載版本,比如從指定位置寫入資料,或寫入位元組陣列等。

3.3 flip()

  flip()方法將Buffer從寫模式切換到讀模式。呼叫flip()方法會將position設為0,limit設為position之前的值。

3.4 從Buffer讀資料

  也有兩種方法從Buffer讀取資料:

  • 從Buffer中讀資料到Channel中;
  • 呼叫Buffer的get()方法讀取資料;
int bytesWritten = inChannel.write(buf); // read from buffer into channel
byte aByte = buf.get();

3.5 rewind()

  rewind()方法將position設定為0,可以從頭開始讀資料。

3.6 clear()和compact()

  當從Buffer讀取資料結束之後要將其切換回寫模式,可以呼叫clear()、compact()這兩個方法,兩者之間的區別如下:

  呼叫clear(),會將position設為0,limit設為capacity,也就是說Buffer被清空了,但是裡面的資料仍然存在,只是這時沒有標記可以告訴你哪些資料是已讀,哪些是未讀。

  如果讀取到一半需要寫入資料,但是未讀的資料稍後還需要讀取,這時可以使用compact(),其會將所有未讀取的資料複製到Buffer的前面,將position設定到這些資料後面,limit設定為capacity,所以此時是從未讀的資料後面開始寫入新的資料。

3.7 mark()和reset()

  呼叫mark()方法可以標誌一個指定的位置(即設定mark值),之後呼叫reset()方法時position又會回到之前標記的位置。

 

4. ByteBuffer

   ByteBuffer是一個比較基礎的緩衝器,繼承自Buffer,是可以儲存未加工位元組的緩衝器,並且也是唯一直接與通道互動的緩衝器。可以通過ByteBuffer的allocate()方法來分配一個固定大小的ByteBuffer,並且其還有一個方法選擇集,用於以原始的位元組形式或基本型別輸出和讀取資料。但是,沒辦法輸出或讀取物件,即使是字串物件也不行。這種處理雖然很低階,但卻正好,因為這是大多數作業系統中更有效的對映方式。

  ByteBuffer也分為直接和非直接緩衝器,通過allocate()建立的就是非直接緩衝器,而通過allocateDirect()方法就可以建立出一個緩衝器直接緩衝器,這是一個與作業系統有更高耦合性的緩衝器,也就意味著它能夠帶來更高的速度,但是分配的開支也會更大。

  儘管ByteBuffer只能儲存位元組型別的資料,但是它具有可以從其所容納的位元組中產生出各種不同基本型別值的方法。下面的例子展示怎樣使用這些方法來插入和抽取各種數值:

public class GetData {    
    private static final int BSIZE = 1024;
    public static void main(String[] args){
        ByteBuffer bb = ByteBuffer.allocate(BSIZE);
        int i = 0;
        while(i++ < bb.limit())
            if(bb.get() != 0)
                System.out.println("nonzero");
        System.out.println("i = " + i);
        bb.rewind();
        // store and read a char array:
        bb.asCharBuffer().put("Howdy!");
        char c;
        while((c = bb.getChar()) != 0)
            System.out.print(c + " ");
        System.out.println();
        bb.rewind();
        // store and read a short:
        bb.asShortBuffer().put((short)471142);
        System.out.println(bb.getShort());
        bb.rewind();
        // sotre and read an int:
        bb.asIntBuffer().put(99471142);
        System.out.println(bb.getInt());
        bb.rewind();
        // store and read a long:
        bb.asLongBuffer().put(99471142);
        System.out.println(bb.getLong());
        bb.rewind();
        // store and read a float:
        bb.asFloatBuffer().put(99471142);
        System.out.println(bb.getFloat());
        bb.rewind();
        // store and read a double:
        bb.asDoubleBuffer().put(99471142);
        System.out.println(bb.getDouble());
        bb.rewind();
    }
}

 

5. Buffer型別

  Java NIO中包含了如下幾種Buffer:

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

  這些Buffer型別代表著不同的資料型別,使得可以通過Buffer直接操作如char、short等型別的資料而不是位元組資料。其中MappedByteBuffer略有不同,後面會專門總結。

  通過ByteBuffer我們只能往Buffer直接寫入或者讀取位元組陣列,但是通過對應型別的Buffer比如CharBuffer、DoubleBuffer等我們可以直接往Buffer寫入char、double等型別的資料。或者利用ByteBuffer的asCharBuffer()、asShorBuffer()等方法獲取其檢視,然後再使用其put()方法即可直接寫入基本資料型別,就像上面的例子。

  這就是檢視緩衝器(view buffer)可以讓我們通過某個特定的基本資料型別的視窗檢視其底層的ByteBuffer。ByteBuffer依然是實際儲存資料的地方,“支援”著前面的檢視,因此對檢視的任何修改都會對映成為對ByteBuffer中資料的修改。這使得我們可以很方便地向ByteBuffer插入資料。檢視還允許我們從ByteBuffer一次一個地(與ByteBuffer所支援的方式相同)或者成批地(通過放入陣列中)讀取基本型別值。在下面的例子中,通過IntBuffer操縱ByteBuffer中的int型資料:

public class IntBufferDemo {    
    private static final int BSIZE = 1024;
    public static void main(String[] args){
        ByteBuffer bb = ByteBuffer.allocate(BSIZE);
        IntBuffer ib = bb.asIntBuffer();
        // store an array of int:
        ib.put(new int[]{11,42,47,99,143,811,1016});
        // absolute location read and write:
        System.out.println(ib.get(3));
        ib.put(3,1811);
        // setting a new limit before rewinding the buffer.
        ib.flip();
        while(ib.hasRemaining()){
            int i = ib.get();
            System.out.println(i);
        }
    }
}

  上例中先用過載後的put()方法儲存一個整數陣列。接著get()和put()方法呼叫直接訪問底層ByteBuffer中的某個整數位置。這些通過直接與ByteBuffer對話訪問絕對位置的方式也同樣適用於基本型別。

 

6. 總結

  本文簡單總結了Java NIO(Java New IO),其目的在於提高速度。Java NIO類庫中主要包括Buffer、Channel、Selector,本文主要總結了Buffer相關的知識點:

  • Buffer叫緩衝器,她是和Channel(通道)互動的,可以從channel中讀資料到buffer中,或者從buffer往channel中寫資料;
  • Buffer內部封裝了一塊記憶體,提供了一系列API使得可以方便地操作記憶體中的資料。其內部是通過capacity、position、limit、mark等變數來跟蹤標記封裝的資料的;
  • ByteBuffer是最基本的Buffer,是唯一可以直接與通道互動的緩衝器,其可以直接操縱位元組資料或位元組陣列;
  • 除了ByteBuffer之外,Buffer還有許多別的型別如:MappedByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer;
  • 雖然只有ByteBuffer能夠直接和通道互動,但是可以從ByteBuffer獲取多種不同的檢視緩衝器,進而同時具備了直接操作基本資料型別和與通道互動的能力;

  基礎知識的總結也許是比較枯燥的,但是如果你已經看到這裡說明你很有耐心,如果覺得對你有幫助的話,不妨點個贊關注一下吧^_^

 

相關文章