這次是關于java nio,有一些重複的發的地方。本文中的源代碼可以在此處下載下傳,下載下傳連結為:http://115.com/file/cltlj10i#nio-src.zip
本文簡介: jdk 1.4 中引入的新輸入輸出 (nio) 庫在标準 java 代碼中提供了高速的、面向塊的 i/o。本實用教程從進階概念到底層的程式設計細節,非常詳細地介紹了 nio 庫。您将學到諸如緩沖區和通道這樣的關鍵 i/o 元素的知識,并考察更新後的庫中的标準 i/o 是如何工作的。您還将了解隻能通過 nio 來完成的工作,如異步 i/o 和直接緩沖區。
輸入/輸出:概念性描述
i/o ? 或者輸入/輸出 ? 指的是計算機與外部世界或者一個程式與計算機的其餘部分的之間的接口。它對于任何計算機系統都非常關鍵,因而所有 i/o 的主體實際上是内置在作業系統中的。單獨的程式一般是讓系統為它們完成大部分的工作。
在 java 程式設計中,直到最近一直使用 流 的方式完成 i/o。所有 i/o 都被視為單個的位元組的移動,通過一個稱為 stream 的對象一次移動一個位元組。流 i/o 用于與外部世界接觸。它也在内部使用,用于将對象轉換為位元組,然後再轉換回對象。
nio 與原來的 i/o 有同樣的作用和目的,但是它使用不同的方式? 塊 i/o。正如您将在本教程中學到的,塊 i/o 的效率可以比流 i/o 高許多。
nio 的建立目的是為了讓 java 程式員可以實作高速 i/o 而無需編寫自定義的本機代碼。nio 将最耗時的 i/o 操作(即填充和提取緩沖區)轉移回作業系統,因而可以極大地提高速度。
原來的 i/o 庫(在 <code>java.io.*</code>中) 與 nio 最重要的差別是資料打包和傳輸的方式。正如前面提到的,原來的 i/o 以流的方式處理資料,而 nio 以塊的方式處理資料。
面向流 的 i/o 系統一次一個位元組地處理資料。一個輸入流産生一個位元組的資料,一個輸出流消費一個位元組的資料。為流式資料建立過濾器非常容易。連結幾個過濾器,以便每個過濾器隻負責單個複雜處理機制的一部分,這樣也是相對簡單的。不利的一面是,面向流的 i/o 通常相當慢。
一個 面向塊 的 i/o 系統以塊的形式處理資料。每一個操作都在一步中産生或者消費一個資料塊。按塊處理資料比按(流式的)位元組處理資料要快得多。但是面向塊的 i/o 缺少一些面向流的 i/o 所具有的優雅性和簡單性。
在 jdk 1.4 中原來的 i/o 包和 nio 已經很好地內建了。 <code>java.io.*</code> 已經以 nio 為基礎重新實作了,是以現在它可以利用 nio 的一些特性。例如, <code>java.io.*</code> 包中的一些類包含以塊的形式讀寫資料的方法,這使得即使在更面向流的系統中,處理速度也會更快。
也可以用 nio 庫實作标準 i/o 功能。例如,可以容易地使用塊 i/o 一次一個位元組地移動資料。但是正如您會看到的,nio 還提供了原 i/o 包中所沒有的許多好處。
通道和緩沖區
<code>通道 </code>和 <code>緩沖區 </code>是 nio 中的核心對象,幾乎在每一個 i/o 操作中都要使用它們。
通道是對原 i/o 包中的流的模拟。到任何目的地(或來自任何地方)的所有資料都必須通過一個 channel 對象。一個 buffer 實質上是一個容器對象。發送給一個通道的所有對象都必須首先放到緩沖區中;同樣地,從通道中讀取的任何資料都要讀到緩沖區中。
在本節中,您會了解到 nio 中通道和緩沖區是如何工作的。
<code>buffer</code> 是一個對象, 它包含一些要寫入或者剛讀出的資料。 在 nio 中加入 <code>buffer</code> 對象,展現了新庫與原 i/o 的一個重要差別。在面向流的 i/o 中,您将資料直接寫入或者将資料直接讀到 <code>stream</code> 對象中。
在 nio 庫中,所有資料都是用緩沖區處理的。在讀取資料時,它是直接讀到緩沖區中的。在寫入資料時,它是寫入到緩沖區中的。任何時候通路 nio 中的資料,您都是将它放到緩沖區中。
緩沖區實質上是一個數組。通常它是一個位元組數組,但是也可以使用其他種類的數組。但是一個緩沖區不 僅僅 是一個數組。緩沖區提供了對資料的結構化通路,而且還可以跟蹤系統的讀/寫程序。
最常用的緩沖區類型是 <code>bytebuffer</code>。一個 <code>bytebuffer</code> 可以在其底層位元組數組上進行 get/set 操作(即位元組的擷取和設定)。
<code>bytebuffer</code> 不是 nio 中唯一的緩沖區類型。事實上,對于每一種基本 java 類型都有一種緩沖區類型:
<code>bytebuffer</code>
<code>charbuffer</code>
<code>shortbuffer</code>
<code>intbuffer</code>
<code>longbuffer</code>
<code>floatbuffer</code>
<code>doublebuffer</code>
每一個 <code>buffer</code> 類都是 <code>buffer</code> 接口的一個執行個體。 除了 <code>bytebuffer</code>,每一個 buffer 類都有完全一樣的操作,隻是它們所處理的資料類型不一樣。因為大多數标準 i/o 操作都使用 <code>bytebuffer</code>,是以它具有所有共享的緩沖區操作以及一些特有的操作。
現在您可以花一點時間運作 usefloatbuffer.java,它包含了類型化的緩沖區的一個應用例子。
<code>channel</code>是一個對象,可以通過它讀取和寫入資料。拿 nio 與原來的 i/o 做個比較,通道就像是流。
正如前面提到的,所有資料都通過 <code>buffer</code> 對象來處理。您永遠不會将位元組直接寫入通道中,相反,您是将資料寫入包含一個或者多個位元組的緩沖區。同樣,您不會直接從通道中讀取位元組,而是将資料從通道讀入緩沖區,再從緩沖區擷取這個位元組。
通道與流的不同之處在于通道是雙向的。而流隻是在一個方向上移動(一個流必須是 <code>inputstream</code> 或者 <code>outputstream</code> 的子類), 而 <code>通道 </code>可以用于讀、寫或者同時用于讀寫。
因為它們是雙向的,是以通道可以比流更好地反映底層作業系統的真實情況。特别是在 unix 模型中,底層作業系統通道是雙向的。
從理論到實踐:nio 中的讀和寫
讀和寫是 i/o 的基本過程。從一個通道中讀取很簡單:隻需建立一個緩沖區,然後讓通道将資料讀到這個緩沖區中。寫入也相當簡單:建立一個緩沖區,用資料填充它,然後讓通道用這些資料來執行寫入操作。
在本節中,我們将學習有關在 java 程式中讀取和寫入資料的一些知識。我們将回顧 nio 的主要元件(緩沖區、通道和一些相關的方法),看看它們是如何互動以進行讀寫的。在接下來的幾節中,我們将更詳細地分析這其中的每個元件以及其互動。
在我們第一個練習中,我們将從一個檔案中讀取一些資料。如果使用原來的 i/o,那麼我們隻需建立一個 <code>fileinputstream</code> 并從它那裡讀取。而在 nio 中,情況稍有不同:我們首先從 <code>fileinputstream</code> 擷取一個 <code>fileinputstream</code> 對象,然後使用這個通道來讀取資料。
在 nio 系統中,任何時候執行一個讀操作,您都是從通道中讀取,但是您不是 直接 從通道讀取。因為所有資料最終都駐留在緩沖區中,是以您是從通道讀到緩沖區中。
是以讀取檔案涉及三個步驟:(1) 從 <code>fileinputstream</code> 擷取 <code>channel</code>,(2) 建立 <code>buffer</code>,(3) 将資料從 <code>channel</code> 讀到 <code>buffer </code>中。
現在,讓我們看一下這個過程。
第一步是擷取通道。我們從 <code>fileinputstream</code> 擷取通道:
<code>fileinputstream fin =</code><code>new</code> <code>fileinputstream(</code><code>"readandshow.txt"</code> <code>);</code>
<code>filechannel fc = fin.getchannel();</code>
下一步是建立緩沖區:
1
<code>bytebuffer buffer = bytebuffer.allocate( </code><code>1024</code> <code>);</code>
最後,需要将資料從通道讀到緩沖區中,如下所示:
<code>fc.read( buffer );</code>
您會注意到,我們不需要告訴通道要讀 多少資料 到緩沖區中。每一個緩沖區都有複雜的内部統計機制,它會跟蹤已經讀了多少資料以及還有多少空間可以容納更多的資料
在 nio 中寫入檔案類似于從檔案中讀取。首先從 <code>fileoutputstream</code> 擷取一個通道:
2
<code>fileoutputstream fout = </code><code>new</code> <code>fileoutputstream( </code><code>"writesomebytes.txt"</code> <code>);</code>
<code>filechannel fc = fout.getchannel();</code>
下一步是建立一個緩沖區并在其中放入一些資料 - 在這裡,資料将從一個名為 <code>message</code> 的數組中取出,這個數組包含字元串 "some bytes" 的 ascii 位元組(本教程後面将會解釋 <code>buffer.flip()</code> 和 <code>buffer.put()</code> 調用)。
<code>bytebuffer buffer = bytebuffer.allocate(</code><code>1024</code> <code>);</code>
<code>for</code> <code>(</code><code>int</code> <code>i=</code><code>0</code><code>; i<message.length; ++i) {</code>
<code> </code><code>buffer.put( message[i] );</code>
<code>}</code>
<code>buffer.flip();</code>
最後一步是寫入緩沖區中
<code>fc.write( buffer );</code>
注意在這裡同樣不需要告訴通道要寫入多資料。緩沖區的内部統計機制會跟蹤它包含多少資料以及還有多少資料要寫入。
下面我們将看一下在結合讀和寫時會有什麼情況。我們以一個名為 copyfile.java 的簡單程式作為這個練習的基礎,它将一個檔案的所有内容拷貝到另一個檔案中。copyfile.java 執行三個基本操作:首先建立一個 <code>buffer</code>,然後從源檔案中将資料讀到這個緩沖區中,然後将緩沖區寫入目标檔案。這個程式不斷重複 ― 讀、寫、讀、寫 ― 直到源檔案結束。
copyfile 程式讓您看到我們如何檢查操作的狀态,以及如何使用 <code>clear()</code> 和 <code>flip()</code> 方法重設緩沖區,并準備緩沖區以便将新讀取的資料寫到另一個通道中。
因為緩沖區會跟蹤它自己的資料,是以 copyfile 程式的内部循環 (inner loop) 非常簡單,如下所示:
<code>fcin.read( buffer );</code>
<code>fcout.write( buffer );</code>
第一行将資料從輸入通道 <code>fcin</code> 中讀入緩沖區,第二行将這些資料寫到輸出通道 <code>fcout</code> 。
下一步是檢查拷貝何時完成。當沒有更多的資料時,拷貝就算完成,并且可以在 <code>read()</code> 方法傳回 -1 是判斷這一點,如下所示:
<code>int</code> <code>r = fcin.read( buffer );</code>
<code>if</code> <code>(r==-</code><code>1</code><code>) {</code>
<code> </code><code>break</code><code>;</code>
最後,在從輸入通道讀入緩沖區之前,我們調用 <code>clear()</code> 方法。同樣,在将緩沖區寫入輸出通道之前,我們調用 <code>flip()</code> 方法,如下所示
3
4
5
6
7
8
9
<code>buffer.clear();</code>
<code>clear()</code> 方法重設緩沖區,使它可以接受讀入的資料。 <code>flip()</code> 方法讓緩沖區可以将新讀入的資料寫入另一個通道。
緩沖區内部細節
<a>概述</a>
本節将介紹 nio 中兩個重要的緩沖區元件:狀态變量和通路方法 (accessor)。
狀态變量是前一節中提到的"内部統計機制"的關鍵。每一個讀/寫操作都會改變緩沖區的狀态。通過記錄和跟蹤這些變化,緩沖區就可能夠内部地管理自己的資源。
在從通道讀取資料時,資料被放入到緩沖區。在有些情況下,可以将這個緩沖區直接寫入另一個通道,但是在一般情況下,您還需要檢視資料。這是使用 通路方法 <code>get()</code> 來完成的。同樣,如果要将原始資料放入緩沖區中,就要使用通路方法 <code>put()</code>。
在本節中,您将學習關于 nio 中的狀态變量和通路方法的内容。我們将描述每一個元件,并讓您有機會看到它的實際應用。雖然 nio 的内部統計機制初看起來可能很複雜,但是您很快就會看到大部分的實際工作都已經替您完成了。您可能習慣于通過手工編碼進行簿記 ― 即使用位元組數組和索引變量,現在它已在 nio 中内部地處理了。
<a>狀态變量</a>
可以用三個值指定緩沖區在任意時刻的狀态:
<code>position</code>
<code>limit</code>
<code>capacity</code>
這三個變量一起可以跟蹤緩沖區的狀态和它所包含的資料。我們将在下面的小節中詳細分析每一個變量,還要介紹它們如何适應典型的讀/寫(輸入/輸出)程序。在這個例子中,我們假定要将資料從一個輸入通道拷貝到一個輸出通道。
<a>position</a>
您可以回想一下,緩沖區實際上就是美化了的數組。在從通道讀取時,您将所讀取的資料放到底層的數組中。 <code>position</code> 變量跟蹤已經寫了多少資料。更準确地說,它指定了下一個位元組将放到數組的哪一個元素中。是以,如果您從通道中讀三個位元組到緩沖區中,那麼緩沖區的 <code>position</code> 将會設定為3,指向數組中第四個元素。
同樣,在寫入通道時,您是從緩沖區中擷取資料。 <code>position</code> 值跟蹤從緩沖區中擷取了多少資料。更準确地說,它指定下一個位元組來自數組的哪一個元素。是以如果從緩沖區寫了5個位元組到通道中,那麼緩沖區的 <code>position</code> 将被設定為5,指向數組的第六個元素。
<a>limit</a>
<code>limit</code> 變量表明還有多少資料需要取出(在從緩沖區寫入通道時),或者還有多少空間可以放入資料(在從通道讀入緩沖區時)。
<code>position</code> 總是小于或者等于 <code>limit</code>。
<a>capacity</a>
緩沖區的 <code>capacity</code> 表明可以儲存在緩沖區中的最大資料容量。實際上,它指定了底層數組的大小 ― 或者至少是指定了準許我們使用的底層數組的容量。
<code>limit</code> 決不能大于 <code>capacity</code>。
<a>觀察變量</a>
我們首先觀察一個新建立的緩沖區。出于本例子的需要,我們假設這個緩沖區的 <code>總容量 </code>為8個位元組。 <code>buffer</code> 的狀态如下所示:

回想一下 ,<code>limit</code> 決不能大于 <code>capacity</code>,此例中這兩個值都被設定為 8。我們通過将它們指向數組的尾部之後(如果有第8個槽,則是第8個槽所在的位置)來說明這點。
<code>position</code> 設定為0。如果我們讀一些資料到緩沖區中,那麼下一個讀取的資料就進入 slot 0 。如果我們從緩沖區寫一些資料,從緩沖區讀取的下一個位元組就來自 slot 0 。 <code>position</code> 設定如下所示:
由于 <code>capacity</code> 不會改變,是以我們在下面的讨論中可以忽略它。
<a>第一次讀取</a>
現在我們可以開始在新建立的緩沖區上進行讀/寫操作。首先從輸入通道中讀一些資料到緩沖區中。第一次讀取得到三個位元組。它們被放到數組中從 <code>position</code> 開始的位置,這時 position 被設定為 0。讀完之後,position 就增加到 3,如下所示:
<code>limit</code> 沒有改變。
<a>第二次讀取</a>
在第二次讀取時,我們從輸入通道讀取另外兩個位元組到緩沖區中。這兩個位元組儲存在由 <code>position</code> 所指定的位置上, <code>position</code> 因而增加 2:
<a>flip</a>
現在我們要将資料寫到輸出通道中。在這之前,我們必須調用 <code>flip()</code> 方法。這個方法做兩件非常重要的事:
它将 <code>limit</code> 設定為目前 <code>position</code>。
它将 <code>position</code> 設定為 0。
前一小節中的圖顯示了在 flip 之前緩沖區的情況。下面是在 flip 之後的緩沖區:
我們現在可以将資料從緩沖區寫入通道了。 <code>position</code> 被設定為 0,這意味着我們得到的下一個位元組是第一個位元組。 <code>limit</code> 已被設定為原來的 <code>position</code>,這意味着它包括以前讀到的所有位元組,并且一個位元組也不多。
<a>第一次寫入</a>
在第一次寫入時,我們從緩沖區中取四個位元組并将它們寫入輸出通道。這使得 <code>position</code> 增加到 4,而 <code>limit</code> 不變,如下所示:
<a>第二次寫入</a>
我們隻剩下一個位元組可寫了。 <code>limit</code>在我們調用 <code>flip()</code> 時被設定為 5,并且 <code>position</code> 不能超過 <code>limit</code>。是以最後一次寫入操作從緩沖區取出一個位元組并将它寫入輸出通道。這使得 <code>position</code> 增加到 5,并保持 <code>limit</code> 不變,如下所示:
最後一步是調用緩沖區的 <code>clear()</code> 方法。這個方法重設緩沖區以便接收更多的位元組。 <code>clear</code> 做兩種非常重要的事情:
它将 <code>limit</code> 設定為與 <code>capacity</code> 相同。
它設定 <code>position</code> 為 0。
下圖顯示了在調用 <code>clear()</code> 後緩沖區的狀态:
緩沖區現在可以接收新的資料了。
到目前為止,我們隻是使用緩沖區将資料從一個通道轉移到另一個通道。然而,程式經常需要直接處理資料。例如,您可能需要将使用者資料儲存到磁盤。在這種情況下,您必須将這些資料直接放入緩沖區,然後用通道将緩沖區寫入磁盤。
或者,您可能想要從磁盤讀取使用者資料。在這種情況下,您要将資料從通道讀到緩沖區中,然後檢查緩沖區中的資料。
在本節的最後,我們将詳細分析如何使用 <code>bytebuffer</code> 類的 <code>get()</code> 和 <code>put()</code> 方法直接通路緩沖區中的資料。
<code>bytebuffer</code> 類中有四個 <code>get()</code> 方法:
<code>byte get();</code>
<code>bytebuffer get( byte dst[] );</code>
<code>bytebuffer get( byte dst[], int offset, int length );</code>
<code>byte get( int index );</code>
第一個方法擷取單個位元組。第二和第三個方法将一組位元組讀到一個數組中。第四個方法從緩沖區中的特定位置擷取位元組。那些傳回<code>bytebuffer</code> 的方法隻是傳回調用它們的緩沖區的 <code>this</code> 值。
此外,我們認為前三個 <code>get()</code> 方法是相對的,而最後一個方法是絕對的。 相對 意味着 <code>get()</code> 操作服從 <code>limit</code> 和 <code>position</code> 值 ― 更明确地說,位元組是從目前 <code>position</code> 讀取的,而 <code>position</code> 在 <code>get</code> 之後會增加。另一方面,一個 絕對 方法會忽略 <code>limit</code> 和 <code>position</code> 值,也不會影響它們。事實上,它完全繞過了緩沖區的統計方法。
上面列出的方法對應于 <code>bytebuffer</code> 類。其他類有等價的 <code>get()</code> 方法,這些方法除了不是處理位元組外,其它方面是是完全一樣的,它們處理的是與該緩沖區類相适應的類型。
<code>bytebuffer</code> 類中有五個 <code>put()</code> 方法:
<code>bytebuffer put( byte b );</code>
<code>bytebuffer put( byte src[] );</code>
<code>bytebuffer put( byte src[], int offset, int length );</code>
<code>bytebuffer put( bytebuffer src );</code>
<code>bytebuffer put( int index, byte b );</code>
第一個方法 <code>寫入(put) </code>單個位元組。第二和第三個方法寫入來自一個數組的一組位元組。第四個方法将資料從一個給定的源<code>bytebuffer</code> 寫入這個 <code>bytebuffer</code>。第五個方法将位元組寫入緩沖區中特定的 <code>位置 </code>。那些傳回 <code>bytebuffer</code> 的方法隻是傳回調用它們的緩沖區的 <code>this</code> 值。
與 <code>get()</code> 方法一樣,我們将把 <code>put()</code> 方法劃分為 相對 或者 絕對 的。前四個方法是相對的,而第五個方法是絕對的。
上面顯示的方法對應于 <code>bytebuffer</code> 類。其他類有等價的 <code>put()</code> 方法,這些方法除了不是處理位元組之外,其它方面是完全一樣的。它們處理的是與該緩沖區類相适應的類型。
除了前些小節中描述的 <code>get()</code> 和 <code>put()</code> 方法, <code>bytebuffer</code> 還有用于讀寫不同類型的值的其他方法,如下所示:
<code>getbyte()</code>
<code>getchar()</code>
<code>getshort()</code>
<code>getint()</code>
<code>getlong()</code>
<code>getfloat()</code>
<code>getdouble()</code>
<code>putbyte()</code>
<code>putchar()</code>
<code>putshort()</code>
<code>putint()</code>
<code>putlong()</code>
<code>putfloat()</code>
<code>putdouble()</code>
事實上,這其中的每個方法都有兩種類型 ― 一種是相對的,另一種是絕對的。它們對于讀取格式化的二進制資料(如圖像檔案的頭部)很有用。
您可以在例子程式 typesinbytebuffer.java 中看到這些方法的實際應用。
下面的内部循環概括了使用緩沖區将資料從輸入通道拷貝到輸出通道的過程。
<code>while</code> <code>(</code><code>true</code><code>) {</code>
<code> </code><code>buffer.clear();</code>
<code> </code><code>int</code> <code>r = fcin.read( buffer );</code>
<code> </code><code>if</code> <code>(r==-</code><code>1</code><code>) {</code>
<code> </code><code>break</code><code>;</code>
<code> </code><code>}</code>
<code> </code><code>buffer.flip();</code>
<code> </code><code>fcout.write( buffer );</code>
<code>read()</code> 和 <code>write()</code> 調用得到了極大的簡化,因為許多工作細節都由緩沖區完成了。 <code>clear()</code> 和 <code>flip()</code> 方法用于讓緩沖區在讀和寫之間切換。
關于緩沖區的更多内容
到目前為止,您已經學習了使用緩沖區進行日常工作所需要掌握的大部分内容。我們的例子沒怎麼超出标準的讀/寫過程種類,在原來的 i/o 中可以像在 nio 中一樣容易地實作這樣的标準讀寫過程。
本節将讨論使用緩沖區的一些更複雜的方面,比如緩沖區配置設定、包裝和分片。我們還會讨論 nio 帶給 java 平台的一些新功能。您将學到如何建立不同類型的緩沖區以達到不同的目的,如可保護資料不被修改的 隻讀 緩沖區,和直接映射到底層作業系統緩沖區的 直接 緩沖區。我們将在本節的最後介紹如何在 nio 中建立記憶體映射檔案。
在能夠讀和寫之前,必須有一個緩沖區。要建立緩沖區,您必須 配置設定 它。我們使用靜态方法 <code>allocate()</code> 來配置設定緩沖區:
<code>allocate()</code> 方法配置設定一個具有指定大小的底層數組,并将它包裝到一個緩沖區對象中 ― 在本例中是一個 <code>bytebuffer</code>。
您還可以将一個現有的數組轉換為緩沖區,如下所示:
<code>byte</code> <code>array[] =</code><code>new</code> <code>byte</code><code>[</code><code>1024</code><code>];</code>
<code>bytebuffer buffer = bytebuffer.wrap( array );</code>
本例使用了 <code>wrap()</code> 方法将一個數組包裝為緩沖區。必須非常小心地進行這類操作。一旦完成包裝,底層資料就可以通過緩沖區或者直接通路。
<code>slice()</code> 方法根據現有的緩沖區建立一種 子緩沖區 。也就是說,它建立一個新的緩沖區,新緩沖區與原來的緩沖區的一部分共享資料。
使用例子可以最好地說明這點。讓我們首先建立一個長度為 10 的 <code>bytebuffer</code>:
<code>bytebuffer buffer = bytebuffer.allocate(</code><code>10</code> <code>)</code>
然後使用資料來填充這個緩沖區,在第 n 個槽中放入數字 n:
<code>for</code> <code>(</code><code>int</code> <code>i=</code><code>0</code><code>; i<buffer.capacity(); ++i) {</code>
<code> </code><code>buffer.put( (</code><code>byte</code><code>)i );</code>
現在我們對這個緩沖區 分片 ,以建立一個包含槽 3 到槽 6 的子緩沖區。在某種意義上,子緩沖區就像原來的緩沖區中的一個 視窗 。
視窗的起始和結束位置通過設定 <code>position</code> 和 <code>limit</code> 值來指定,然後調用 <code>buffer</code> 的 <code>slice()</code> 方法:
<code>buffer.position(</code><code>3</code> <code>);</code>
<code>buffer.limit(</code><code>7</code> <code>);</code>
<code>bytebuffer slice = buffer.slice();</code>
<code>片 </code>是緩沖區的 <code>子緩沖區 </code>。不過, <code>片段 </code>和 <code>緩沖區 </code>共享同一個底層資料數組,我們在下一節将會看到這一點。
我們已經建立了原緩沖區的子緩沖區,并且我們知道緩沖區和子緩沖區共享同一個底層資料數組。讓我們看看這意味着什麼。
我們周遊子緩沖區,将每一個元素乘以 11 來改變它。例如,5 會變成 55。
<code>for</code> <code>(</code><code>int</code> <code>i=</code><code>0</code><code>; i<slice.capacity(); ++i) {</code>
<code> </code><code>byte</code> <code>b = slice.get( i );</code>
<code> </code><code>b *=</code><code>11</code><code>;</code>
<code> </code><code>slice.put( i, b );</code>
最後,再看一下原緩沖區中的内容:
<code>buffer.position(</code><code>0</code> <code>);</code>
<code>buffer.limit( buffer.capacity() );</code>
<code>while</code> <code>(buffer.remaining()></code><code>0</code><code>) {</code>
<code> </code><code>system.out.println( buffer.get() );</code>
結果表明隻有在子緩沖區視窗中的元素被改變了:
緩沖區片對于促進抽象非常有幫助。可以編寫自己的函數處理整個緩沖區,而且如果想要将這個過程應用于子緩沖區上,您隻需取主緩沖區的一個片,并将它傳遞給您的函數。這比編寫自己的函數來取額外的參數以指定要對緩沖區的哪一部分進行操作更容易。
隻讀緩沖區非常簡單 ― 您可以讀取它們,但是不能向它們寫入。可以通過調用緩沖區的 <code>asreadonlybuffer()</code> 方法,将任何正常緩沖區轉換為隻讀緩沖區,這個方法傳回一個與原緩沖區完全相同的緩沖區(并與其共享資料),隻不過它是隻讀的。
隻讀緩沖區對于保護資料很有用。在将緩沖區傳遞給某個對象的方法時,您無法知道這個方法是否會修改緩沖區中的資料。建立一個隻讀的緩沖區可以 保證 該緩沖區不會被修改。
不能将隻讀的緩沖區轉換為可寫的緩沖區。
另一種有用的 <code>bytebuffer</code> 是直接緩沖區。 直接緩沖區 是為加快 i/o 速度,而以一種特殊的方式配置設定其記憶體的緩沖區。
實際上,直接緩沖區的準确定義是與實作相關的。sun 的文檔是這樣描述直接緩沖區的:
給定一個直接位元組緩沖區,java 虛拟機将盡最大努力直接對它執行本機 i/o 操作。也就是說,它會在每一次調用底層作業系統的本機 i/o 操作之前(或之後),嘗試避免将緩沖區的内容拷貝到一個中間緩沖區中(或者從一個中間緩沖區中拷貝資料)。
您可以在例子程式 fastcopyfile.java 中看到直接緩沖區的實際應用,這個程式是 copyfile.java 的另一個版本,它使用了直接緩沖區以提高速度。
還可以用記憶體映射檔案建立直接緩沖區。
記憶體映射檔案 i/o 是一種讀和寫檔案資料的方法,它可以比正常的基于流或者基于通道的 i/o 快得多。
記憶體映射檔案 i/o 是通過使檔案中的資料神奇般地出現為記憶體數組的内容來完成的。這其初聽起來似乎不過就是将整個檔案讀到記憶體中,但是事實上并不是這樣。一般來說,隻有檔案中實際讀取或者寫入的部分才會送入(或者 映射 )到記憶體中。
記憶體映射并不真的神奇或者多麼不尋常。現代作業系統一般根據需要将檔案的部分映射為記憶體的部分,進而實作檔案系統。java 記憶體映射機制不過是在底層作業系統中可以采用這種機制時,提供了對該機制的通路。
盡管建立記憶體映射檔案相當簡單,但是向它寫入可能是危險的。僅隻是改變數組的單個元素這樣的簡單操作,就可能會直接修改磁盤上的檔案。修改資料與将資料儲存到磁盤是沒有分開的。
了解記憶體映射的最好方法是使用例子。在下面的例子中,我們要将一個 <code>filechannel</code> (它的全部或者部分)映射到記憶體中。為此我們将使用 <code>filechannel.map()</code> 方法。下面代碼行将檔案的前 1024 個位元組映射到記憶體中:
<code>mappedbytebuffer mbb = fc.map( filechannel.mapmode.read_write,</code>
<code> </code><code>0</code><code>,</code><code>1024</code> <code>);</code>
<code>map()</code> 方法傳回一個 <code>mappedbytebuffer</code>,它是 <code>bytebuffer</code> 的子類。是以,您可以像使用其他任何 <code>bytebuffer</code> 一樣使用新映射的緩沖區,作業系統會在需要時負責執行行映射。
分散和聚集
分散/聚集 i/o 是使用多個而不是單個緩沖區來儲存資料的讀寫方法。
一個分散的讀取就像一個正常通道讀取,隻不過它是将資料讀到一個緩沖區數組中而不是讀到單個緩沖區中。同樣地,一個聚集寫入是向緩沖區數組而不是向單個緩沖區寫入資料。
分散/聚集 i/o 對于将資料流劃分為單獨的部分很有用,這有助于實作複雜的資料格式。
通道可以有選擇地實作兩個新的接口: <code>scatteringbytechannel</code> 和 <code>gatheringbytechannel</code>。一個 <code>scatteringbytechannel</code> 是一個具有兩個附加讀方法的通道:
<code>long read( bytebuffer[] dsts );</code>
<code>long read( bytebuffer[] dsts, int offset, int length );</code>
這些 <code>long read()</code> 方法很像标準的 <code>read</code> 方法,隻不過它們不是取單個緩沖區而是取一個緩沖區數組。
在 分散讀取 中,通道依次填充每個緩沖區。填滿一個緩沖區後,它就開始填充下一個。在某種意義上,緩沖區數組就像一個大緩沖區。
分散/聚集 i/o 對于将資料劃分為幾個部分很有用。例如,您可能在編寫一個使用消息對象的網絡應用程式,每一個消息被劃分為固定長度的頭部和固定長度的正文。您可以建立一個剛好可以容納頭部的緩沖區和另一個剛好可以容難正文的緩沖區。當您将它們放入一個數組中并使用分散讀取來向它們讀入消息時,頭部和正文将整齊地劃分到這兩個緩沖區中。
我們從緩沖區所得到的友善性對于緩沖區數組同樣有效。因為每一個緩沖區都跟蹤自己還可以接受多少資料,是以分散讀取會自動找到有空間接受資料的第一個緩沖區。在這個緩沖區填滿後,它就會移動到下一個緩沖區。
聚集寫入 類似于分散讀取,隻不過是用來寫入。它也有接受緩沖區數組的方法:
<code>long write( bytebuffer[] srcs );</code>
<code>long write( bytebuffer[] srcs, int offset, int length );</code>
聚集寫對于把一組單獨的緩沖區中組成單個資料流很有用。為了與上面的消息例子保持一緻,您可以使用聚集寫入來自動将網絡消息的各個部分組裝為單個資料流,以便跨越網絡傳輸消息。
從例子程式 usescattergather.java 中可以看到分散讀取和聚集寫入的實際應用。
檔案鎖定
檔案鎖定初看起來可能讓人迷惑。它 似乎 指的是防止程式或者使用者通路特定檔案。事實上,檔案鎖就像正常的 java 對象鎖 ― 它們是 勸告式的(advisory) 鎖。它們不阻止任何形式的資料通路,相反,它們通過鎖的共享和擷取賴允許系統的不同部分互相協調。
您可以鎖定整個檔案或者檔案的一部分。如果您擷取一個排它鎖,那麼其他人就不能獲得同一個檔案或者檔案的一部分上的鎖。如果您獲得一個共享鎖,那麼其他人可以獲得同一個檔案或者檔案一部分上的共享鎖,但是不能獲得排它鎖。檔案鎖定并不總是出于保護資料的目的。例如,您可能臨時鎖定一個檔案以保證特定的寫操作成為原子的,而不會有其他程式的幹擾。
大多數作業系統提供了檔案系統鎖,但是它們并不都是采用同樣的方式。有些實作提供了共享鎖,而另一些僅提供了排它鎖。事實上,有些實作使得檔案的鎖定部分不可通路,盡管大多數實作不是這樣的。
在本節中,您将學習如何在 nio 中執行簡單的檔案鎖過程,我們還将探讨一些保證被鎖定的檔案盡可能可移植的方法。
要擷取檔案的一部分上的鎖,您要調用一個打開的 <code>filechannel</code> 上的 <code>lock()</code> 方法。注意,如果要擷取一個排它鎖,您必須以寫方式打開檔案。
<code>randomaccessfile raf =</code><code>new</code> <code>randomaccessfile(</code><code>"usefilelocks.txt"</code><code>,</code><code>"rw"</code> <code>);</code>
<code>filechannel fc = raf.getchannel();</code>
<code>filelock lock = fc.lock( start, end,</code><code>false</code> <code>);</code>
在擁有鎖之後,您可以執行需要的任何敏感操作,然後再釋放鎖:
<code>lock.release();</code>
在釋放鎖後,嘗試獲得鎖的其他任何程式都有機會獲得它。
本小節的例子程式 usefilelocks.java 必須與它自己并行運作。這個程式擷取一個檔案上的鎖,持有三秒鐘,然後釋放它。如果同時運作這個程式的多個執行個體,您會看到每個執行個體依次獲得鎖。
檔案鎖定可能是一個複雜的操作,特别是考慮到不同的作業系統是以不同的方式實作鎖這一事實。下面的指導原則将幫助您盡可能保持代碼的可移植性:
隻使用排它鎖。
将所有的鎖視為勸告式的(advisory)。
連網和異步 i/o
連網是學習異步 i/o 的很好基礎,而異步 i/o 對于在 java 語言中執行任何輸入/輸出過程的人來說,無疑都是必須具備的知識。nio 中的連網與 nio 中的其他任何操作沒有什麼不同 ― 它依賴通道和緩沖區,而您通常使用 <code>inputstream</code> 和 <code>outputstream</code> 來獲得通道。
本節首先介紹異步 i/o 的基礎 ― 它是什麼以及它不是什麼,然後轉向更實用的、程式性的例子。
異步 i/o 是一種 沒有阻塞地 讀寫資料的方法。通常,在代碼進行 <code>read()</code> 調用時,代碼會阻塞直至有可供讀取的資料。同樣,<code>write()</code> 調用将會阻塞直至資料能夠寫入。
另一方面,異步 i/o 調用不會阻塞。相反,您将注冊對特定 i/o 事件的興趣 ― 可讀的資料的到達、新的套接字連接配接,等等,而在發生這樣的事件時,系統将會告訴您。
異步 i/o 的一個優勢在于,它允許您同時根據大量的輸入和輸出執行 i/o。同步程式常常要求助于輪詢,或者建立許許多多的線程以處理大量的連接配接。使用異步 i/o,您可以監聽任何數量的通道上的事件,不用輪詢,也不用額外的線程。
我們将通過研究一個名為 multiportecho.java 的例子程式來檢視異步 i/o 的實際應用。這個程式就像傳統的 echo server,它接受網絡連接配接并向它們回響它們可能發送的資料。不過它有一個附加的特性,就是它能同時監聽多個端口,并處理來自所有這些端口的連接配接。并且它隻在單個線程中完成所有這些工作。
本節的闡述對應于 <code>multiportecho</code> 的源代碼中的 <code>go()</code> 方法的實作,是以應該看一下源代碼,以便對所發生的事情有個更全面的了解。
異步 i/o 中的核心對象名為 <code>selector</code>。<code>selector</code> 就是您注冊對各種 i/o 事件的興趣的地方,而且當那些事件發生時,就是這個對象告訴您所發生的事件。
是以,我們需要做的第一件事就是建立一個 <code>selector</code>:
<code>selector selector = selector.open();</code>
然後,我們将對不同的通道對象調用 <code>register()</code> 方法,以便注冊我們對這些對象中發生的 i/o 事件的興趣。<code>register()</code> 的第一個參數總是這個 <code>selector</code>。
為了接收連接配接,我們需要一個 <code>serversocketchannel</code>。事實上,我們要監聽的每一個端口都需要有一個 <code>serversocketchannel</code> 。對于每一個端口,我們打開一個 <code>serversocketchannel</code>,如下所示:
<code>serversocketchannel ssc = serversocketchannel.open();</code>
<code>ssc.configureblocking(</code><code>false</code> <code>);</code>
<code>serversocket ss = ssc.socket();</code>
<code>inetsocketaddress address =</code><code>new</code> <code>inetsocketaddress( ports[i] );</code>
<code>ss.bind( address );</code>
第一行建立一個新的 <code>serversocketchannel</code> ,最後三行将它綁定到給定的端口。第二行将 <code>serversocketchannel</code> 設定為 非阻塞的 。我們必須對每一個要使用的套接字通道調用這個方法,否則異步 i/o 就不能工作。
下一步是将新打開的 <code>serversocketchannels</code> 注冊到 <code>selector</code>上。為此我們使用 serversocketchannel.register() 方法,如下所示
<code>selectionkey key = ssc.register( selector, selectionkey.op_accept );</code>
<code>register()</code> 的第一個參數總是這個 <code>selector</code>。第二個參數是 <code>op_accept</code>,這裡它指定我們想要監聽 accept 事件,也就是在新的連接配接建立時所發生的事件。這是适用于 <code>serversocketchannel</code> 的唯一事件類型。
請注意對 <code>register()</code> 的調用的傳回值。 <code>selectionkey</code> 代表這個通道在此 <code>selector</code> 上的這個注冊。當某個 <code>selector</code> 通知您某個傳入事件時,它是通過提供對應于該事件的 <code>selectionkey</code> 來進行的。<code>selectionkey</code> 還可以用于取消通道的注冊。
現在已經注冊了我們對一些 i/o 事件的興趣,下面将進入主循環。使用 <code>selectors</code> 的幾乎每個程式都像下面這樣使用内部循環:
<code>int</code> <code>num = selector.select();</code>
<code>set selectedkeys = selector.selectedkeys();</code>
<code>iterator it = selectedkeys.iterator();</code>
<code>while</code> <code>(it.hasnext()) {</code>
<code> </code><code>selectionkey key = (selectionkey)it.next();</code>
<code> </code><code>// ... deal with i/o event ...</code>
首先,我們調用 <code>selector</code> 的 <code>select()</code> 方法。這個方法會阻塞,直到至少有一個已注冊的事件發生。當一個或者更多的事件發生時,<code>select()</code> 方法将傳回所發生的事件的數量。
接下來,我們調用 <code>selector</code> 的 <code>selectedkeys()</code> 方法,它傳回發生了事件的 <code>selectionkey</code> 對象的一個 <code>集合 </code>。
我們通過疊代 <code>selectionkeys</code> 并依次處理每個 <code>selectionkey</code> 來處理事件。對于每一個 <code>selectionkey</code>,您必須确定發生的是什麼 i/o 事件,以及這個事件影響哪些 i/o 對象。
程式執行到這裡,我們僅注冊了 <code>serversocketchannel</code>,并且僅注冊它們“接收”事件。為确認這一點,我們對 <code>selectionkey</code> 調用<code>readyops()</code> 方法,并檢查發生了什麼類型的事件:
<code>if</code> <code>((key.readyops() & selectionkey.op_accept)</code>
<code> </code><code>== selectionkey.op_accept) {</code>
<code> </code><code>// accept the new connection</code>
<code> </code><code>// ...</code>
可以肯定地說, <code>readops()</code> 方法告訴我們該事件是新的連接配接。
接受新的連接配接
因為我們知道這個伺服器套接字上有一個傳入連接配接在等待,是以可以安全地接受它;也就是說,不用擔心 <code>accept()</code> 操作會阻塞:
<code>serversocketchannel ssc = (serversocketchannel)key.channel();</code>
<code>socketchannel sc = ssc.accept();</code>
下一步是将新連接配接的 <code>socketchannel</code> 配置為非阻塞的。而且由于接受這個連接配接的目的是為了讀取來自套接字的資料,是以我們還必須将 <code>socketchannel</code> 注冊到 <code>selector</code>上,如下所示:
<code>sc.configureblocking(</code><code>false</code> <code>);</code>
<code>selectionkey newkey = sc.register( selector, selectionkey.op_read );</code>
注意我們使用 <code>register()</code> 的 <code>op_read</code> 參數,将 <code>socketchannel</code> 注冊用于 讀取 而不是 接受 新連接配接。
在處理 <code>selectionkey</code> 之後,我們幾乎可以傳回主循環了。但是我們必須首先将處理過的 <code>selectionkey</code> 從標明的鍵集合中删除。如果我們沒有删除處理過的鍵,那麼它仍然會在主集合中以一個激活的鍵出現,這會導緻我們嘗試再次處理它。我們調用疊代器的<code>remove()</code> 方法來删除處理過的 <code>selectionkey</code>:
<code>it.remove();</code>
現在我們可以傳回主循環并接受從一個套接字中傳入的資料(或者一個傳入的 i/o 事件)了。
當來自一個套接字的資料到達時,它會觸發一個 i/o 事件。這會導緻在主循環中調用 <code>selector.select()</code>,并傳回一個或者多個 i/o 事件。這一次, <code>selectionkey</code> 将被标記為 <code>op_read</code> 事件,如下所示:
<code>}</code><code>else</code> <code>if</code> <code>((key.readyops() & selectionkey.op_read)</code>
<code> </code><code>== selectionkey.op_read) {</code>
<code> </code><code>// read the data</code>
<code> </code><code>socketchannel sc = (socketchannel)key.channel();</code>
與以前一樣,我們取得發生 i/o 事件的通道并處理它。在本例中,由于這是一個 echo server,我們隻希望從套接字中讀取資料并馬上将它發送回去。
每次傳回主循環,我們都要調用 <code>select</code> 的 <code>selector()</code>方法,并取得一組 <code>selectionkey</code>。每個鍵代表一個 i/o 事件。我們處理事件,從標明的鍵集中删除 <code>selectionkey</code>,然後傳回主循環的頂部。
這個程式有點過于簡單,因為它的目的隻是展示異步 i/o 所涉及的技術。在現實的應用程式中,您需要通過将通道從 <code>selector</code> 中删除來處理關閉的通道。而且您可能要使用多個線程。這個程式可以僅使用一個線程,因為它隻是一個示範,但是在現實場景中,建立一個線程池來負責 i/o 事件進行中的耗時部分會更有意義。
字元集
根據 sun 的文檔,一個 <code>charset</code> 是“十六位 unicode 字元序列與位元組序列之間的一個命名的映射”。實際上,一個 <code>charset</code> 允許您以盡可能最具可移植性的方式讀寫字元序列。
java 語言被定義為基于 unicode。然而在實際上,許多人編寫代碼時都假設一個字元在磁盤上或者在網絡流中用一個位元組表示。這種假設在許多情況下成立,但是并不是在所有情況下都成立,而且随着計算機變得對 unicode 越來越友好,這個假設就日益變得不能成立了。
在本節中,我們将看一下如何使用 <code>charsets</code> 以适合現代文本格式的方式處理文本資料。這裡将使用的示例程式相當簡單,不過,它觸及了使用 <code>charset</code> 的所有關鍵方面:為給定的字元編碼建立 <code>charset</code>,以及使用該 <code>charset</code> 解碼和編碼文本資料。
要讀和寫文本,我們要分别使用 <code>charsetdecoder</code> 和 <code>charsetencoder</code>。将它們稱為 編碼器 和 解碼器 是有道理的。一個 字元 不再表示一個特定的位模式,而是表示字元系統中的一個實體。是以,由某個實際的位模式表示的字元必須以某種特定的 編碼 來表示。
<code>charsetdecoder</code> 用于将逐位表示的一串字元轉換為具體的 <code>char</code> 值。同樣,一個 <code>charsetencoder</code> 用于将字元轉換回位。
在下一個小節中,我們将考察一個使用這些對象來讀寫資料的程式。
現在我們将分析這個例子程式 usecharsets.java。這個程式非常簡單 ― 它從一個檔案中讀取一些文本,并将該文本寫入另一個檔案。但是它把該資料當作文本資料,并使用 <code>charbuffer</code> 來将該數句讀入一個 <code>charsetdecoder</code> 中。同樣,它使用 <code>charsetencoder</code> 來寫回該資料。
我們将假設字元以 iso-8859-1(latin1) 字元集(這是 ascii 的标準擴充)的形式儲存在磁盤上。盡管我們必須為使用 unicode 做好準備,但是也必須認識到不同的檔案是以不同的格式儲存的,而 ascii 無疑是非常普遍的一種格式。事實上,每種 java 實作都要求對以下字元編碼提供完全的支援:
us-ascii
iso-8859-1
utf-8
utf-16be
utf-16le
utf-16
在打開相應的檔案、将輸入資料讀入名為 <code>inputdata</code> 的 <code>bytebuffer</code> 之後,我們的程式必須建立 iso-8859-1 (latin1) 字元集的一個執行個體:
<code>charset latin1 = charset.forname(</code><code>"iso-8859-1"</code> <code>);</code>
然後,建立一個解碼器(用于讀取)和一個編碼器 (用于寫入):
<code>charsetdecoder decoder = latin1.newdecoder();</code>
<code>charsetencoder encoder = latin1.newencoder();</code>
為了将位元組資料解碼為一組字元,我們把 <code>bytebuffer</code> 傳遞給 <code>charsetdecoder</code>,結果得到一個 <code>charbuffer</code>
<code>charbuffer cb = decoder.decode( inputdata );</code>
如果想要處理字元,我們可以在程式的此處進行。但是我們隻想無改變地将它寫回,是以沒有什麼要做的。
要寫回資料,我們必須使用 <code>charsetencoder</code> 将它轉換回位元組:
<code>bytebuffer outputdata = encoder.encode( cb );</code>
在轉換完成之後,我們就可以将資料寫到檔案中了。
結束語和參考資料
<a>結束語</a>
正如您所看到的, nio 庫有大量的特性。在一些新特性(例如檔案鎖定和字元集)提供新功能的同時,許多特性在優化方面也非常優秀。
在基礎層次上,通道和緩沖區可以做的事情幾乎都可以用原來的面向流的類來完成。但是通道和緩沖區允許以 快得多 的方式完成這些相同的舊操作 ― 事實上接近系統所允許的最大速度。
不過 nio 最強大的長度之一在于,它提供了一種在 java 語言中執行進行輸入/輸出的新的(也是迫切需要的)結構化方式。随諸如緩沖區、通道和異步 i/o 這些概念性(且可實作的)實體而來的,是我們重新思考 java 程式中的 i/o過程的機會。這樣,nio 甚至為我們最熟悉的 i/o 過程也帶來了新的活力,同時賦予我們通過和以前不同并且更好的方式執行它們的機會。