Java NIO源碼剖析及使用實例(一):Buffer

現在越來越多的公司開始使用NIO,面試中也經常被問到NIO的知識,這裏給大家介紹下,包括基本使用方法以及一些實現原理等,因爲NIO知識較多,會分多篇介紹。

首先來說NIO是做什麼的,Java中NIO大家可以理解爲new io,即新出的一個處理io流的包,它最重要的幾個特性就是Channel(管道)、Buffer(緩衝區)、Selector(選擇器)。相較與IO,NIO新增了緩衝區以及非阻塞讀取等特效,今天首先來看看Buffer。

首先我們直接通過讀源碼來學習下Buffer,後面再舉出具體使用的例子,首先我們看下Buffer的幾個屬性:

// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;

position
首先我們看position,有助於後面理解mark。position故名思議就是記錄當前的位置,他的幾個重要方法是:

final int nextGetIndex() {                          // package-private
    if (position >= limit)
        throw new BufferUnderflowException();
    return position++;
}

final int nextGetIndex(int nb) {                    // package-private
    if (limit - position < nb)
        throw new BufferUnderflowException();
    int p = position;
    position += nb;
    return p;
}
final int nextPutIndex() {                          // package-private
    if (position >= limit)
        throw new BufferOverflowException();
    return position++;
}

final int nextPutIndex(int nb) {                    // package-private
    if (limit - position < nb)
        throw new BufferOverflowException();
    int p = position;
    position += nb;
    return p;
}

nextGetIndex方法其實在讀取buffer裏數據的時候取出position的值之後自增或者增加所需步長,即取出當前位置,再由具體的Buffer實現取出position對應的具體值來達到一個流式的讀取效果。nextPutIndex是同樣的原理,只是其應用於寫數據的時候

mark
mark及後面幾個參數都是繼承自Buffer類裏的,要了解mark,我們可以看一下Buffer源碼裏有關於mark值變化的部分:

public final Buffer mark() {
    mark = position;
    return this;
}
public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

上面兩個方法可以看出來,mark的作用,其實就跟它的名字一樣,就是通過mark()方法在當前位置做一個標記,然後需要時通過reset()來回到標記的位置,未標記時默認值爲-1

limit
limit其實就是記錄具體的讀寫數量,取值時受限於limit。我們在使用Buffer時會先指定Buffer緩衝區大小,比如我們指定5個byte,但是讀取時實際只有3個byte,此時limit值就爲3,我們取值時就只會取3個值,而不是總容量大小5個。其重要使用的地方就是上面介紹過的nextGetIndex和nextPutIndex,大家可以看到取下一個位置時是要跟limit做比較的,它還有個重要方法是:

public final boolean hasRemaining() {
    return position < limit;
}

這個方法其實就是獲取是否還有可讀取的值,即通過當前讀取到的位置和limit的值來判斷

capacity
capacity就是我們緩衝區的總大小。

接下來我們看個具體實例來看下如何使用Buffer,且順帶看下它的常用方法:

private static void testChannel() throws IOException {
    RandomAccessFile accessFile = new RandomAccessFile("d://testNIO.txt" , "rw");
    FileChannel channel = accessFile.getChannel();
    ByteBuffer byteBuffer = ByteBuffer.allocate(3);
    int readBytes = channel.read(byteBuffer);
    while (readBytes != -1){
        byteBuffer.flip();
        while (byteBuffer.hasRemaining()){
            System.out.printf((char)byteBuffer.get() + "");
        }
        byteBuffer.clear();
        readBytes = channel.read(byteBuffer);

    }
    accessFile.close();
}

testNIO.txt文件內容:

aaacc
bba
ccca
dbac

執行結果如下:

aaacc
bba
ccca
dbac

這就是一個簡單的讀取文件的例子,對於其中幾個大家可能不太瞭解的方法下面我們通過源碼來分析下具體實現以瞭解上述代碼是如何工作的:
首先此處用到的是ByteBuffer,我們來看下代碼中大家沒使用過的一些方法:

ByteBuffer.allocate(3)

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer(int cap, int lim) {            // package-private
    super(-1, 0, lim, cap, new byte[cap], 0);
}
ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
             byte[] hb, int offset)
{
    super(mark, pos, lim, cap);
    this.hb = hb;
    this.offset = offset;
}

通過上面可以看到,allocate(3)方法其實就是new了一個mark=-1,pos=0,limit=3,capacity=3,hb=new Byte[3]的這麼一個初始HeapByteBuff緩衝區。

channel.read(byteBuffer)
這裏只能看到read方法,再裏面的具體實現看不到,這裏就不多講了,這個方法一看也就知道其實就是通過管道將數據讀取到緩衝區。

byteBuffer.flip()

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

從源碼可以看出來,flip方法其實就是將limit置爲當前position值,即表示讀取了多少數據,以及將position歸0,這樣後面我們才能讀取數據。

byteBuffer.get()

public byte get() {
    return hb[ix(nextGetIndex())];
}
final int nextGetIndex() {                          // package-private
    if (position >= limit)
        throw new BufferUnderflowException();
    return position++;
}
protected int ix(int i) {
    return i + offset;
}

從源碼可以看出,get方法其實就是去除hb數組中當前position的值,且將position加1以讀取後續值,這就是爲什麼讀取數據時需要先調用flip方法將position值歸0的原因。

byteBuffer.clear()

public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

看源碼可以看出clear方法其實就是將Buffer中的幾個屬性重新初始化了,此處並沒有清空緩存區的值,網上有人說此處會清空緩存區的值是錯誤且不負責任的!我們通過以下一個例子可以看出來其實是沒有清空值的:

將testNIO.txt內容改爲如下:

aaaccdd

然後去掉上面代碼中的byteBuffer.flip(),執行原方法,結果如下:

cd

這是爲什麼呢?是因爲緩衝區第一次讀取數據時存放的是aaa,第二次覆蓋掉第一次的,值爲ccd,第三次讀取時只有一個d值覆蓋掉原來的ccd中的第一個c,此時緩衝區的值爲dcd,我們去掉flip方法後再來讀取數據,此時position爲1,limit爲3,因此會讀出後面的cd值打印到控制檯,所以說clear方法是不會清除緩存區的值的

我的博客:blog.scarlettbai.com
我的公衆號:讀書健身編程

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