Java NIO的出現旨在提高文件的讀寫速度,當然IO用NIO重新實過,所以我們不用顯示的調用NIO也能享受這種高效的文件讀寫。
Java NIO的高效得益于其兩大"助手":Channel(管道)和Buffer(緩沖器)。當然這兩個"得力助手"的"年齡"遠遠比java大!力求簡單易懂的把知識講解給大家,我舉一個例子來說明一下這"兩元大將"是如何在java NIO中配合工作的。
中國古代有一種傳統的吸煙器具---水煙袋。我想用這個東西來模擬一下Channel和Buffer的工作原理。不求說的好,力求準確無誤。

分析一下水煙袋是如何工作的:

第一步,準備工作,準備好上等煙絲;第二步,將"水斗"中裝入適量的水,煙倉中裝滿煙絲并插入水斗中,然后再將煙管插入水斗中;第三步,點燃煙絲并吸氣。香煙從煙倉產生,經過水的過濾進入水上的空閑區。第四步,享受吸煙的快感.....從這個例子中我們提取出主要對象"煙",來分析一下它的運動軌跡。煙倉把煙生產出來,經過水的過濾飄到水上面的空閑區域,然后通過煙管進入人的體內。
如果上面的過程大家理解了,明白了,那么java NIO你已經了解了50%,至少你已經知道它的工作原理了。因為用NIO處理的數據和用水煙袋中吸煙很相似。我們分析一下NIO的工作原理,非常簡單。
當然和吸煙一樣我們首先必須有要用NIO來處理需求的欲望(這好比你想要吸煙了),比方說我想要將C盤下面的wk.txt文件進行備份,備份文件的名稱為wk-bak.txt。類比剛剛吸煙的那個過程:
步驟一:準備工作,確定文件的位置,并將程序不可直接操作的文件轉換成字符流的形式(這一步和上邊吸煙實例的第一步沒有什么差別,只是進行一些簡單的準備工作)。
String inFile = "C:\\wk.txt";
String outFile = "C:\\wk-bak.txt";
FileInputStream inf = new FileInputStream(inFile);
FileOutputStream outf = new FileOutputStream(outFile);
ByteBuffer buffer = ByteBuffer.allocate(1024);
步驟二:創建文件輸入管道,和文件輸出管道。(這一步與上邊吸煙的第二部稍有差別,因為Channel和Buffer是在讀寫的時候才發生的"連接"動作)
//準備文件讀取的管道-->相當于煙倉和煙管
FileChannel inFc = inf.getChannel();
FileChannel outFc = outf.getChannel();
Charset charSet = Charset.forName("utf-8");
//進行編碼解碼-->相當于水斗中水的過濾作用
CharsetDecoder decoder = charSet.newDecoder();
CharsetEncoder encoder = charSet.newEncoder();
步驟三:開始進行文件備份工作。
while(true) {
//準備向Buffer中寫入數據-->相當于點燃煙絲,完事具備只欠東風
buffer.clear();
//進行字符編碼 -->相當于水的過濾作用
CharBuffer cb = decoder.decode(buffer);
ByteBuffer bb = encoder.encode(cb);
//數據經過編碼以后暫存緩沖區-->相當于經過水過濾后的煙暫停在水斗中
int t = inFc.read(bb);
if(t == -1) {
break;
}
bb.flip();
//將字節碼寫入目標文件-->相當于煙已經進入到嘴里
outFc.write(bb);
}
步驟四:檢查文件是否備份成功。發現C盤下面多了一個wk-bak.txt的文件,內容與wk.txt一摸一樣。接下來享受java帶給你的快感....
上面的例子估計大家已經理解的差不多了,當然如果深究也會有一些不太妥當的地方,但是不要較真,目的是學習NIO,并不是吸煙。如果感覺你可以了那么就請把上面的例子補充完整,運行一下,享受一下NIO的威武(當然字符編碼并不是必須的,只是讓這個例子顯得完整一點)。
好吧如果你理解了上面的東西,并且真正的補全了文件備份的小程序,那么就來進行稍微深入一點的學習吧。
上文我提到了舉吸煙的例子是有欠妥當的,其中之一就是Buffer的內部機制和"水斗"簡單的過濾功能是不一樣的。還有字符編碼那一塊也不是在Buffer內部實現的東西,decoder和encoder是針對Buffer的兩個工具。那我們接下來分析一下Buffer內部機制到底不一樣在哪里呢(主要分析常用的兩個方法;clear(),flip())?
來吧,打開Buffer的源碼(摘取有用的部分):
public abstract class Buffer {
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
首先我們要明確一點,所謂的緩沖器僅僅是一個"多功能"的數組。可能在這個Buffer類中沒有體現,但是如果我們打開ByteBuffer的源碼會有byte[]的數組,打開CharBuffer的源碼會有char[]的數組。因為Buffer是所有緩沖器的父類,所以他它不能預計會有多少種緩沖器,所以索性讓"兒子"們自己實現去吧。
既然知道了緩沖器是一個"多功能的數組",那么我們用畫圖的形式來分析一下上面Buffer的源碼。

假設我們定義了一個8個單位大的緩沖區,如上圖(其實Buffer也就是這么一個東西)。首先告訴大家那三個重要的關于緩沖區狀態的的屬性:
capacity:緩沖區的容量;
limit:緩沖區還有多少數據能夠取出或者緩沖區還有多少容量用于存放數據;
position:相當于一個游標(cursor),記錄我們從哪里開始寫數據,從哪里開始讀數據。
剛還說到flip()和clear()是Buffer的兩個重要的方法,因為它們兩個方法決定了緩沖是否能正常的進行讀寫工作。
當我們要想從緩沖區中寫數據的時候必須先執行flip()方法,當我們要想從緩沖區中讀數據時必須先執行clear()方法。

第一次向Buffer中寫入數據時,執行一次flip()方法以后,Buffer的結構變成了這樣:position指向了第一個可以存取數據的0號位,limit和capacity同時指向最高位。

假如第一次我們向Buffer中寫入了3單位的數據,我們再次執行flip()方法則Buffer的結構會變成上圖的所示。但是經過flip()的改造后position總是指向Buffer中第一個可用的位置。那么,未執行flip()方法以前position在哪里呢?很簡單,指向最后一個數據的位置。

當我們想要從Buffer中讀取數據時,執行clear()方法,Buffer的內部結構變成了上圖所示,position指向了可讀數據的首位,limit指向了原來position的位置。
從上面的幾幅圖中我們看出:capacity代表了Buffer的容量是不變的,limit與position的差總是表示Buffer總可以讀的數據,或者Buffer中可以寫數據的容量。還有position總是小于等于limit,limit總是小于等于capacity。
其實到這里我們已經發現,NIO并不像IO那么復雜,因為IO 中的Decorator模式和Adaptor模式確實讓我們一時間摸不到頭腦,但是熟悉了會感覺到IO的設計之精美。
NIO中還有一個知識點就是無阻塞的Socket編程,這里就不說了,因為比較復雜,但是如果我們真正理解了Selector這個調度者的工作,那么無阻塞的實現機制我們差不多就掌握了,復雜也就是編碼上面的事了。