日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
全面解讀JavaNIO工作原理

本文簡介: JDK 1.4 中引入的新輸入輸出 (NIO) 庫在標準 Java 代碼中提供了高速的、面向塊的 I/O。本實用教程從高級概念到底層的編程細節(jié),非常詳細地介紹了 NIO 庫。您將學到諸如緩沖區(qū)和通道這樣的關(guān)鍵 I/O 元素的知識,并考察更新后的庫中的標準 I/O 是如何工作的。您還將了解只能通過 NIO 來完成的工作,如異步 I/O 和直接緩沖區(qū)。

創(chuàng)新互聯(lián)客戶idc服務中心,提供川西大數(shù)據(jù)中心、成都服務器、成都主機托管、成都雙線服務器等業(yè)務的一站式服務。通過各地的服務中心,我們向成都用戶提供優(yōu)質(zhì)廉價的產(chǎn)品以及開放、透明、穩(wěn)定、高性價比的服務,資深網(wǎng)絡工程師在機房提供7*24小時標準級技術(shù)保障。

◆  輸入/輸出:概念性描述

I/O 簡介

I/O ? 或者輸入/輸出 ? 指的是計算機與外部世界或者一個程序與計算機的其余部分的之間的接口。它對于任何計算機系統(tǒng)都非常關(guān)鍵,因而所有 I/O 的主體實際上是內(nèi)置在操作系統(tǒng)中的。單獨的程序一般是讓系統(tǒng)為它們完成大部分的工作。

在 Java 編程中,直到最近一直使用 流 的方式完成 I/O。所有 I/O 都被視為單個的字節(jié)的移動,通過一個稱為 Stream 的對象一次移動一個字節(jié)。流 I/O 用于與外部世界接觸。它也在內(nèi)部使用,用于將對象轉(zhuǎn)換為字節(jié),然后再轉(zhuǎn)換回對象。

NIO 與原來的 I/O 有同樣的作用和目的,但是它使用不同的方式? 塊 I/O。正如您將在本教程中學到的,塊 I/O 的效率可以比流 I/O 高許多。

為什么要使用 NIO?

NIO 的創(chuàng)建目的是為了讓 Java 程序員可以實現(xiàn)高速 I/O 而無需編寫自定義的本機代碼。NIO 將最耗時的 I/O 操作(即填充和提取緩沖區(qū))轉(zhuǎn)移回操作系統(tǒng),因而可以極大地提高速度。

流與塊的比較

原來的 I/O 庫(在 java.io.*中) 與 NIO 最重要的區(qū)別是數(shù)據(jù)打包和傳輸?shù)姆绞?。正如前面提到的,原來?I/O 以流的方式處理數(shù)據(jù),而 NIO 以塊的方式處理數(shù)據(jù)。

面向流 的 I/O 系統(tǒng)一次一個字節(jié)地處理數(shù)據(jù)。一個輸入流產(chǎn)生一個字節(jié)的數(shù)據(jù),一個輸出流消費一個字節(jié)的數(shù)據(jù)。為流式數(shù)據(jù)創(chuàng)建過濾器非常容易。鏈接幾個過濾器,以便每個過濾器只負責單個復雜處理機制的一部分,這樣也是相對簡單的。不利的一面是,面向流的 I/O 通常相當慢。

一個 面向塊 的 I/O 系統(tǒng)以塊的形式處理數(shù)據(jù)。每一個操作都在一步中產(chǎn)生或者消費一個數(shù)據(jù)塊。按塊處理數(shù)據(jù)比按(流式的)字節(jié)處理數(shù)據(jù)要快得多。但是面向塊的 I/O 缺少一些面向流的 I/O 所具有的優(yōu)雅性和簡單性。

集成的 I/O

在 JDK 1.4 中原來的 I/O 包和 NIO 已經(jīng)很好地集成了。 java.io.* 已經(jīng)以 NIO 為基礎(chǔ)重新實現(xiàn)了,所以現(xiàn)在它可以利用 NIO 的一些特性。例如, java.io.* 包中的一些類包含以塊的形式讀寫數(shù)據(jù)的方法,這使得即使在更面向流的系統(tǒng)中,處理速度也會更快。

也可以用 NIO 庫實現(xiàn)標準 I/O 功能。例如,可以容易地使用塊 I/O 一次一個字節(jié)地移動數(shù)據(jù)。但是正如您會看到的,NIO 還提供了原 I/O 包中所沒有的許多好處。

◆ 通道和緩沖區(qū)

概  述

通道 和 緩沖區(qū) 是 NIO 中的核心對象,幾乎在每一個 I/O 操作中都要使用它們。

通道是對原 I/O 包中的流的模擬。到任何目的地(或來自任何地方)的所有數(shù)據(jù)都必須通過一個 Channel 對象。一個 Buffer 實質(zhì)上是一個容器對象。發(fā)送給一個通道的所有對象都必須首先放到緩沖區(qū)中;同樣地,從通道中讀取的任何數(shù)據(jù)都要讀到緩沖區(qū)中。

在本節(jié)中,您會了解到 NIO 中通道和緩沖區(qū)是如何工作的。

什么是緩沖區(qū)?

Buffer 是一個對象, 它包含一些要寫入或者剛讀出的數(shù)據(jù)。 在 NIO 中加入 Buffer 對象,體現(xiàn)了新庫與原 I/O 的一個重要區(qū)別。在面向流的 I/O 中,您將數(shù)據(jù)直接寫入或者將數(shù)據(jù)直接讀到 Stream 對象中。

在 NIO 庫中,所有數(shù)據(jù)都是用緩沖區(qū)處理的。在讀取數(shù)據(jù)時,它是直接讀到緩沖區(qū)中的。在寫入數(shù)據(jù)時,它是寫入到緩沖區(qū)中的。任何時候訪問 NIO 中的數(shù)據(jù),您都是將它放到緩沖區(qū)中。

緩沖區(qū)實質(zhì)上是一個數(shù)組。通常它是一個字節(jié)數(shù)組,但是也可以使用其他種類的數(shù)組。但是一個緩沖區(qū)不 僅僅 是一個數(shù)組。緩沖區(qū)提供了對數(shù)據(jù)的結(jié)構(gòu)化訪問,而且還可以跟蹤系統(tǒng)的讀/寫進程。

緩沖區(qū)類型

最常用的緩沖區(qū)類型是 ByteBuffer。一個 ByteBuffer 可以在其底層字節(jié)數(shù)組上進行 get/set 操作(即字節(jié)的獲取和設(shè)置)。

ByteBuffer 不是 NIO 中唯一的緩沖區(qū)類型。事實上,對于每一種基本 Java 類型都有一種緩沖區(qū)類型:

? ByteBuffer

? CharBuffer

? ShortBuffer

? IntBuffer

? LongBuffer

? FloatBuffer

? DoubleBuffer

每一個 Buffer 類都是 Buffer 接口的一個實例。 除了 ByteBuffer,每一個 Buffer 類都有完全一樣的操作,只是它們所處理的數(shù)據(jù)類型不一樣。因為大多數(shù)標準 I/O 操作都使用 ByteBuffer,所以它具有所有共享的緩沖區(qū)操作以及一些特有的操作。

現(xiàn)在您可以花一點時間運行 UseFloatBuffer.java,它包含了類型化的緩沖區(qū)的一個應用例子。

什么是通道?

Channel是一個對象,可以通過它讀取和寫入數(shù)據(jù)。拿 NIO 與原來的 I/O 做個比較,通道就像是流。

正如前面提到的,所有數(shù)據(jù)都通過 Buffer 對象來處理。您永遠不會將字節(jié)直接寫入通道中,相反,您是將數(shù)據(jù)寫入包含一個或者多個字節(jié)的緩沖區(qū)。同樣,您不會直接從通道中讀取字節(jié),而是將數(shù)據(jù)從通道讀入緩沖區(qū),再從緩沖區(qū)獲取這個字節(jié)。

通道類型

通道與流的不同之處在于通道是雙向的。而流只是在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類), 而 通道 可以用于讀、寫或者同時用于讀寫。

因為它們是雙向的,所以通道可以比流更好地反映底層操作系統(tǒng)的真實情況。特別是在 UNIX 模型中,底層操作系統(tǒng)通道是雙向的。

◆ 從理論到實踐:NIO 中的讀和寫

概  述

讀和寫是 I/O 的基本過程。從一個通道中讀取很簡單:只需創(chuàng)建一個緩沖區(qū),然后讓通道將數(shù)據(jù)讀到這個緩沖區(qū)中。寫入也相當簡單:創(chuàng)建一個緩沖區(qū),用數(shù)據(jù)填充它,然后讓通道用這些數(shù)據(jù)來執(zhí)行寫入操作。

在本節(jié)中,我們將學習有關(guān)在 Java 程序中讀取和寫入數(shù)據(jù)的一些知識。我們將回顧 NIO 的主要組件(緩沖區(qū)、通道和一些相關(guān)的方法),看看它們是如何交互以進行讀寫的。在接下來的幾節(jié)中,我們將更詳細地分析這其中的每個組件以及其交互。

從文件中讀取

在我們第一個練習中,我們將從一個文件中讀取一些數(shù)據(jù)。如果使用原來的 I/O,那么我們只需創(chuàng)建一個 FileInputStream 并從它那里讀取。而在 NIO 中,情況稍有不同:我們首先從 FileInputStream 獲取一個 FileInputStream 對象,然后使用這個通道來讀取數(shù)據(jù)。

在 NIO 系統(tǒng)中,任何時候執(zhí)行一個讀操作,您都是從通道中讀取,但是您不是 直接 從通道讀取。因為所有數(shù)據(jù)最終都駐留在緩沖區(qū)中,所以您是從通道讀到緩沖區(qū)中。

因此讀取文件涉及三個步驟:(1) 從 FileInputStream 獲取 Channel,(2) 創(chuàng)建 Buffer,(3) 將數(shù)據(jù)從 Channel 讀到 Buffer 中。

現(xiàn)在,讓我們看一下這個過程。

三個容易的步驟

第一步是獲取通道。我們從 FileInputStream 獲取通道:

 
 
 
 
  1. FileInputStream fin = new FileInputStream( "readandshow.txt" );  
  2. FileChannel fc = fin.getChannel(); 

下一步是創(chuàng)建緩沖區(qū):

 
 
 
 
  1. ByteBuffer buffer = ByteBuffer.allocate( 1024 ); 

最后,需要將數(shù)據(jù)從通道讀到緩沖區(qū)中,如下所示:

 
 
 
 
  1. fc.read( buffer ); 

您會注意到,我們不需要告訴通道要讀 多少數(shù)據(jù) 到緩沖區(qū)中。每一個緩沖區(qū)都有復雜的內(nèi)部統(tǒng)計機制,它會跟蹤已經(jīng)讀了多少數(shù)據(jù)以及還有多少空間可以容納更多的數(shù)據(jù)

寫入文件

在 NIO 中寫入文件類似于從文件中讀取。首先從 FileOutputStream 獲取一個通道:

 
 
 
 
  1. FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );  
  2. FileChannel fc = fout.getChannel(); 

下一步是創(chuàng)建一個緩沖區(qū)并在其中放入一些數(shù)據(jù) - 在這里,數(shù)據(jù)將從一個名為 message 的數(shù)組中取出,這個數(shù)組包含字符串 "Some bytes" 的 ASCII 字節(jié)(本教程后面將會解釋 buffer.flip() 和 buffer.put() 調(diào)用)。

 
 
 
 
  1. ByteBuffer buffer = ByteBuffer.allocate( 1024 );  
  2.  for (int i=0; i
  3.      buffer.put( message[i] );  
  4. }  
  5. buffer.flip(); 

最后一步是寫入緩沖區(qū)中

 
 
 
 
  1. fc.write( buffer ); 

注意在這里同樣不需要告訴通道要寫入多數(shù)據(jù)。緩沖區(qū)的內(nèi)部統(tǒng)計機制會跟蹤它包含多少數(shù)據(jù)以及還有多少數(shù)據(jù)要寫入。

讀寫結(jié)合

下面我們將看一下在結(jié)合讀和寫時會有什么情況。我們以一個名為 CopyFile.java 的簡單程序作為這個練習的基礎(chǔ),它將一個文件的所有內(nèi)容拷貝到另一個文件中。CopyFile.java 執(zhí)行三個基本操作:首先創(chuàng)建一個 Buffer,然后從源文件中將數(shù)據(jù)讀到這個緩沖區(qū)中,然后將緩沖區(qū)寫入目標文件。這個程序不斷重復 ― 讀、寫、讀、寫 ― 直到源文件結(jié)束。

CopyFile 程序讓您看到我們?nèi)绾螜z查操作的狀態(tài),以及如何使用 clear() 和 flip() 方法重設(shè)緩沖區(qū),并準備緩沖區(qū)以便將新讀取的數(shù)據(jù)寫到另一個通道中。

運行 CopyFile 例子

因為緩沖區(qū)會跟蹤它自己的數(shù)據(jù),所以 CopyFile 程序的內(nèi)部循環(huán) (inner loop) 非常簡單,如下所示:

 
 
 
 
  1. fcin.read( buffer );  
  2. fcout.write( buffer ); 

第一行將數(shù)據(jù)從輸入通道 fcin 中讀入緩沖區(qū),第二行將這些數(shù)據(jù)寫到輸出通道 fcout 。

檢查狀態(tài)

下一步是檢查拷貝何時完成。當沒有更多的數(shù)據(jù)時,拷貝就算完成,并且可以在 read() 方法返回 -1 是判斷這一點,如下所示:

 
 
 
 
  1. int r = fcin.read( buffer );  
  2.  if (r==-1) {  
  3.      break;  

重設(shè)緩沖區(qū)

最后,在從輸入通道讀入緩沖區(qū)之前,我們調(diào)用 clear() 方法。同樣,在將緩沖區(qū)寫入輸出通道之前,我們調(diào)用 flip() 方法,如下所示

 
 
 
 
  1. buffer.clear();int r = fcin.read( buffer );  
  2.  if (r==-1) {  
  3.      break;  
  4. }  
  5.  buffer.flip();  
  6. fcout.write( buffer ); 

clear() 方法重設(shè)緩沖區(qū),使它可以接受讀入的數(shù)據(jù)。 flip() 方法讓緩沖區(qū)可以將新讀入的數(shù)據(jù)寫入另一個通道。

#p#

◆  緩沖區(qū)內(nèi)部細節(jié)

概  述

本節(jié)將介紹 NIO 中兩個重要的緩沖區(qū)組件:狀態(tài)變量和訪問方法 (accessor)。

狀態(tài)變量是前一節(jié)中提到的"內(nèi)部統(tǒng)計機制"的關(guān)鍵。每一個讀/寫操作都會改變緩沖區(qū)的狀態(tài)。通過記錄和跟蹤這些變化,緩沖區(qū)就可能夠內(nèi)部地管理自己的資源。

在從通道讀取數(shù)據(jù)時,數(shù)據(jù)被放入到緩沖區(qū)。在有些情況下,可以將這個緩沖區(qū)直接寫入另一個通道,但是在一般情況下,您還需要查看數(shù)據(jù)。這是使用 訪問方法 get() 來完成的。同樣,如果要將原始數(shù)據(jù)放入緩沖區(qū)中,就要使用訪問方法 put()。

在本節(jié)中,您將學習關(guān)于 NIO 中的狀態(tài)變量和訪問方法的內(nèi)容。我們將描述每一個組件,并讓您有機會看到它的實際應用。雖然 NIO 的內(nèi)部統(tǒng)計機制初看起來可能很復雜,但是您很快就會看到大部分的實際工作都已經(jīng)替您完成了。您可能習慣于通過手工編碼進行簿記 ― 即使用字節(jié)數(shù)組和索引變量,現(xiàn)在它已在 NIO 中內(nèi)部地處理了。

狀態(tài)變量

可以用三個值指定緩沖區(qū)在任意時刻的狀態(tài):

? position

? limit

? capacity

這三個變量一起可以跟蹤緩沖區(qū)的狀態(tài)和它所包含的數(shù)據(jù)。我們將在下面的小節(jié)中詳細分析每一個變量,還要介紹它們?nèi)绾芜m應典型的讀/寫(輸入/輸出)進程。在這個例子中,我們假定要將數(shù)據(jù)從一個輸入通道拷貝到一個輸出通道。

Position

您可以回想一下,緩沖區(qū)實際上就是美化了的數(shù)組。在從通道讀取時,您將所讀取的數(shù)據(jù)放到底層的數(shù)組中。 position 變量跟蹤已經(jīng)寫了多少數(shù)據(jù)。更準確地說,它指定了下一個字節(jié)將放到數(shù)組的哪一個元素中。因此,如果您從通道中讀三個字節(jié)到緩沖區(qū)中,那么緩沖區(qū)的 position 將會設(shè)置為3,指向數(shù)組中第四個元素。

同樣,在寫入通道時,您是從緩沖區(qū)中獲取數(shù)據(jù)。 position 值跟蹤從緩沖區(qū)中獲取了多少數(shù)據(jù)。更準確地說,它指定下一個字節(jié)來自數(shù)組的哪一個元素。因此如果從緩沖區(qū)寫了5個字節(jié)到通道中,那么緩沖區(qū)的 position 將被設(shè)置為5,指向數(shù)組的第六個元素。

Limit

limit 變量表明還有多少數(shù)據(jù)需要取出(在從緩沖區(qū)寫入通道時),或者還有多少空間可以放入數(shù)據(jù)(在從通道讀入緩沖區(qū)時)。

position 總是小于或者等于 limit。

Capacity

緩沖區(qū)的 capacity 表明可以儲存在緩沖區(qū)中的最大數(shù)據(jù)容量。實際上,它指定了底層數(shù)組的大小 ― 或者至少是指定了準許我們使用的底層數(shù)組的容量。

limit 決不能大于 capacity。

觀察變量

我們首先觀察一個新創(chuàng)建的緩沖區(qū)。出于本例子的需要,我們假設(shè)這個緩沖區(qū)的 總?cè)萘?為8個字節(jié)。 Buffer 的狀態(tài)如下所示:

回想一下 ,limit 決不能大于 capacity,此例中這兩個值都被設(shè)置為 8。我們通過將它們指向數(shù)組的尾部之后(如果有第8個槽,則是第8個槽所在的位置)來說明這點。

position 設(shè)置為0。如果我們讀一些數(shù)據(jù)到緩沖區(qū)中,那么下一個讀取的數(shù)據(jù)就進入 slot 0 。如果我們從緩沖區(qū)寫一些數(shù)據(jù),從緩沖區(qū)讀取的下一個字節(jié)就來自 slot 0 。 position 設(shè)置如下所示:

由于 capacity 不會改變,所以我們在下面的討論中可以忽略它。

第一次讀取

現(xiàn)在我們可以開始在新創(chuàng)建的緩沖區(qū)上進行讀/寫操作。首先從輸入通道中讀一些數(shù)據(jù)到緩沖區(qū)中。第一次讀取得到三個字節(jié)。它們被放到數(shù)組中從 position 開始的位置,這時 position 被設(shè)置為 0。讀完之后,position 就增加到 3,如下所示:

limit 沒有改變。

第二次讀取

在第二次讀取時,我們從輸入通道讀取另外兩個字節(jié)到緩沖區(qū)中。這兩個字節(jié)儲存在由 position 所指定的位置上, position 因而增加 2:

limit 沒有改變。

flip

現(xiàn)在我們要將數(shù)據(jù)寫到輸出通道中。在這之前,我們必須調(diào)用 flip() 方法。這個方法做兩件非常重要的事:

1.它將 limit 設(shè)置為當前 position。

2.它將 position 設(shè)置為 0。

前一小節(jié)中的圖顯示了在 flip 之前緩沖區(qū)的情況。下面是在 flip 之后的緩沖區(qū):

我們現(xiàn)在可以將數(shù)據(jù)從緩沖區(qū)寫入通道了。 position 被設(shè)置為 0,這意味著我們得到的下一個字節(jié)是第一個字節(jié)。 limit 已被設(shè)置為原來的 position,這意味著它包括以前讀到的所有字節(jié),并且一個字節(jié)也不多。

第一次寫入

在第一次寫入時,我們從緩沖區(qū)中取四個字節(jié)并將它們寫入輸出通道。這使得 position 增加到 4,而 limit 不變,如下所示:

第二次寫入

我們只剩下一個字節(jié)可寫了。 limit在我們調(diào)用 flip() 時被設(shè)置為 5,并且 position 不能超過 limit。所以最后一次寫入操作從緩沖區(qū)取出一個字節(jié)并將它寫入輸出通道。這使得 position 增加到 5,并保持 limit 不變,如下所示:

clear

最后一步是調(diào)用緩沖區(qū)的 clear() 方法。這個方法重設(shè)緩沖區(qū)以便接收更多的字節(jié)。 Clear 做兩種非常重要的事情:

1.它將 limit 設(shè)置為與 capacity 相同。

2.它設(shè)置 position 為 0。

下圖顯示了在調(diào)用 clear() 后緩沖區(qū)的狀態(tài):

緩沖區(qū)現(xiàn)在可以接收新的數(shù)據(jù)了。

訪問方法

到目前為止,我們只是使用緩沖區(qū)將數(shù)據(jù)從一個通道轉(zhuǎn)移到另一個通道。然而,程序經(jīng)常需要直接處理數(shù)據(jù)。例如,您可能需要將用戶數(shù)據(jù)保存到磁盤。在這種情況下,您必須將這些數(shù)據(jù)直接放入緩沖區(qū),然后用通道將緩沖區(qū)寫入磁盤。

或者,您可能想要從磁盤讀取用戶數(shù)據(jù)。在這種情況下,您要將數(shù)據(jù)從通道讀到緩沖區(qū)中,然后檢查緩沖區(qū)中的數(shù)據(jù)。

在本節(jié)的最后,我們將詳細分析如何使用 ByteBuffer 類的 get() 和 put() 方法直接訪問緩沖區(qū)中的數(shù)據(jù)。

get() 方法

ByteBuffer 類中有四個 get() 方法:

1.byte get();

2.ByteBuffer get( byte dst[] );

3.ByteBuffer get( byte dst[], int offset, int length );

4.byte get( int index );

第一個方法獲取單個字節(jié)。第二和第三個方法將一組字節(jié)讀到一個數(shù)組中。第四個方法從緩沖區(qū)中的特定位置獲取字節(jié)。那些返回ByteBuffer 的方法只是返回調(diào)用它們的緩沖區(qū)的 this 值。

此外,我們認為前三個 get() 方法是相對的,而最后一個方法是絕對的。 相對 意味著 get() 操作服從 limit 和 position 值 ― 更明確地說,字節(jié)是從當前 position 讀取的,而 position 在 get 之后會增加。另一方面,一個 絕對 方法會忽略 limit 和 position 值,也不會影響它們。事實上,它完全繞過了緩沖區(qū)的統(tǒng)計方法。

上面列出的方法對應于 ByteBuffer 類。其他類有等價的 get() 方法,這些方法除了不是處理字節(jié)外,其它方面是是完全一樣的,它們處理的是與該緩沖區(qū)類相適應的類型。

put()方法

ByteBuffer 類中有五個 put() 方法:

1.ByteBuffer put( byte b );

2.ByteBuffer put( byte src[] );

3.ByteBuffer put( byte src[], int offset, int length );

4.ByteBuffer put( ByteBuffer src );

5.ByteBuffer put( int index, byte b );

第一個方法 寫入(put) 單個字節(jié)。第二和第三個方法寫入來自一個數(shù)組的一組字節(jié)。第四個方法將數(shù)據(jù)從一個給定的源ByteBuffer 寫入這個 ByteBuffer。第五個方法將字節(jié)寫入緩沖區(qū)中特定的 位置 。那些返回 ByteBuffer 的方法只是返回調(diào)用它們的緩沖區(qū)的 this 值。

與 get() 方法一樣,我們將把 put() 方法劃分為 相對 或者 絕對 的。前四個方法是相對的,而第五個方法是絕對的。

上面顯示的方法對應于 ByteBuffer 類。其他類有等價的 put() 方法,這些方法除了不是處理字節(jié)之外,其它方面是完全一樣的。它們處理的是與該緩沖區(qū)類相適應的類型。

類型化的 get() 和 put() 方法

除了前些小節(jié)中描述的 get() 和 put() 方法, ByteBuffer 還有用于讀寫不同類型的值的其他方法,如下所示:

? getByte()

? getChar()

? getShort()

? getInt()

? getLong()

? getFloat()

? getDouble()

? putByte()

? putChar()

? putShort()

? putInt()

? putLong()

? putFloat()

? putDouble()

事實上,這其中的每個方法都有兩種類型 ― 一種是相對的,另一種是絕對的。它們對于讀取格式化的二進制數(shù)據(jù)(如圖像文件的頭部)很有用。

您可以在例子程序 TypesInByteBuffer.java 中看到這些方法的實際應用。

緩沖區(qū)的使用:一個內(nèi)部循環(huán)

下面的內(nèi)部循環(huán)概括了使用緩沖區(qū)將數(shù)據(jù)從輸入通道拷貝到輸出通道的過程。

 
 
 
 
  1. while (true) {  
  2.      buffer.clear();  
  3.      int r = fcin.read( buffer );  
  4.       if (r==-1) {  
  5.        break;  
  6.      }  
  7.       buffer.flip();  
  8.      fcout.write( buffer );} 

read() 和 write() 調(diào)用得到了極大的簡化,因為許多工作細節(jié)都由緩沖區(qū)完成了。 clear() 和 flip() 方法用于讓緩沖區(qū)在讀和寫之間切換。

#p#

◆  關(guān)于緩沖區(qū)的更多內(nèi)容

概  述

到目前為止,您已經(jīng)學習了使用緩沖區(qū)進行日常工作所需要掌握的大部分內(nèi)容。我們的例子沒怎么超出標準的讀/寫過程種類,在原來的 I/O 中可以像在 NIO 中一樣容易地實現(xiàn)這樣的標準讀寫過程。

本節(jié)將討論使用緩沖區(qū)的一些更復雜的方面,比如緩沖區(qū)分配、包裝和分片。我們還會討論 NIO 帶給 Java 平臺的一些新功能。您將學到如何創(chuàng)建不同類型的緩沖區(qū)以達到不同的目的,如可保護數(shù)據(jù)不被修改的 只讀 緩沖區(qū),和直接映射到底層操作系統(tǒng)緩沖區(qū)的 直接 緩沖區(qū)。我們將在本節(jié)的最后介紹如何在 NIO 中創(chuàng)建內(nèi)存映射文件。

緩沖區(qū)分配和包裝

在能夠讀和寫之前,必須有一個緩沖區(qū)。要創(chuàng)建緩沖區(qū),您必須 分配 它。我們使用靜態(tài)方法 allocate() 來分配緩沖區(qū):

 
 
 
 
  1. ByteBuffer buffer = ByteBuffer.allocate( 1024 ); 

allocate() 方法分配一個具有指定大小的底層數(shù)組,并將它包裝到一個緩沖區(qū)對象中 ― 在本例中是一個 ByteBuffer。

您還可以將一個現(xiàn)有的數(shù)組轉(zhuǎn)換為緩沖區(qū),如下所示:

 
 
 
 
  1. byte array[] = new byte[1024];  
  2. ByteBuffer buffer = ByteBuffer.wrap( array ); 

本例使用了 wrap() 方法將一個數(shù)組包裝為緩沖區(qū)。必須非常小心地進行這類操作。一旦完成包裝,底層數(shù)據(jù)就可以通過緩沖區(qū)或者直接訪問。

緩沖區(qū)分片

slice() 方法根據(jù)現(xiàn)有的緩沖區(qū)創(chuàng)建一種 子緩沖區(qū) 。也就是說,它創(chuàng)建一個新的緩沖區(qū),新緩沖區(qū)與原來的緩沖區(qū)的一部分共享數(shù)據(jù)。

使用例子可以最好地說明這點。讓我們首先創(chuàng)建一個長度為 10 的 ByteBuffer:

 
 
 
 
  1. ByteBuffer buffer = ByteBuffer.allocate( 10 ) 

然后使用數(shù)據(jù)來填充這個緩沖區(qū),在第 n 個槽中放入數(shù)字 n:

 
 
 
 
  1. for (int i=0; i
  2.      buffer.put( (byte)i );  

現(xiàn)在我們對這個緩沖區(qū) 分片 ,以創(chuàng)建一個包含槽 3 到槽 6 的子緩沖區(qū)。在某種意義上,子緩沖區(qū)就像原來的緩沖區(qū)中的一個 窗口 。

窗口的起始和結(jié)束位置通過設(shè)置 position 和 limit 值來指定,然后調(diào)用 Buffer 的 slice() 方法:

 
 
 
 
  1. buffer.position( 3 );  
  2. buffer.limit( 7 );  
  3. ByteBuffer slice = buffer.slice(); 

片 是緩沖區(qū)的 子緩沖區(qū) 。不過, 片段 和 緩沖區(qū) 共享同一個底層數(shù)據(jù)數(shù)組,我們在下一節(jié)將會看到這一點。

緩沖區(qū)份片和數(shù)據(jù)共享

我們已經(jīng)創(chuàng)建了原緩沖區(qū)的子緩沖區(qū),并且我們知道緩沖區(qū)和子緩沖區(qū)共享同一個底層數(shù)據(jù)數(shù)組。讓我們看看這意味著什么。

我們遍歷子緩沖區(qū),將每一個元素乘以 11 來改變它。例如,5 會變成 55。

 
 
 
 
  1. for (int i=0; i
  2.      byte b = slice.get( i );  
  3.      b *= 11;  
  4.      slice.put( i, b );  

最后,再看一下原緩沖區(qū)中的內(nèi)容:

 
 
 
 
  1. buffer.position( 0 );  
  2. buffer.limit( buffer.capacity() );  
  3.  while (buffer.remaining()>0) {  
  4.      System.out.println( buffer.get() );  

結(jié)果表明只有在子緩沖區(qū)窗口中的元素被改變了:

$ java SliceBuffer 
0 
1 
2 
33 
44 
55 
66 
7 
8 
9

緩沖區(qū)片對于促進抽象非常有幫助??梢跃帉懽约旱暮瘮?shù)處理整個緩沖區(qū),而且如果想要將這個過程應用于子緩沖區(qū)上,您只需取主緩沖區(qū)的一個片,并將它傳遞給您的函數(shù)。這比編寫自己的函數(shù)來取額外的參數(shù)以指定要對緩沖區(qū)的哪一部分進行操作更容易。

只讀緩沖區(qū)

只讀緩沖區(qū)非常簡單 ― 您可以讀取它們,但是不能向它們寫入??梢酝ㄟ^調(diào)用緩沖區(qū)的 asReadOnlyBuffer() 方法,將任何常規(guī)緩沖區(qū)轉(zhuǎn)換為只讀緩沖區(qū),這個方法返回一個與原緩沖區(qū)完全相同的緩沖區(qū)(并與其共享數(shù)據(jù)),只不過它是只讀的。

只讀緩沖區(qū)對于保護數(shù)據(jù)很有用。在將緩沖區(qū)傳遞給某個對象的方法時,您無法知道這個方法是否會修改緩沖區(qū)中的數(shù)據(jù)。創(chuàng)建一個只讀的緩沖區(qū)可以 保證 該緩沖區(qū)不會被修改。

不能將只讀的緩沖區(qū)轉(zhuǎn)換為可寫的緩沖區(qū)。

直接和間接緩沖區(qū)

另一種有用的 ByteBuffer 是直接緩沖區(qū)。 直接緩沖區(qū) 是為加快 I/O 速度,而以一種特殊的方式分配其內(nèi)存的緩沖區(qū)。

實際上,直接緩沖區(qū)的準確定義是與實現(xiàn)相關(guān)的。Sun 的文檔是這樣描述直接緩沖區(qū)的:

給定一個直接字節(jié)緩沖區(qū),Java 虛擬機將盡最大努力直接對它執(zhí)行本機 I/O 操作。也就是說,它會在每一次調(diào)用底層操作系統(tǒng)的本機 I/O 操作之前(或之后),嘗試避免將緩沖區(qū)的內(nèi)容拷貝到一個中間緩沖區(qū)中(或者從一個中間緩沖區(qū)中拷貝數(shù)據(jù))。

您可以在例子程序 FastCopyFile.java 中看到直接緩沖區(qū)的實際應用,這個程序是 CopyFile.java 的另一個版本,它使用了直接緩沖區(qū)以提高速度。

還可以用內(nèi)存映射文件創(chuàng)建直接緩沖區(qū)。

內(nèi)存映射文件 I/O

內(nèi)存映射文件 I/O 是一種讀和寫文件數(shù)據(jù)的方法,它可以比常規(guī)的基于流或者基于通道的 I/O 快得多。

內(nèi)存映射文件 I/O 是通過使文件中的數(shù)據(jù)神奇般地出現(xiàn)為內(nèi)存數(shù)組的內(nèi)容來完成的。這其初聽起來似乎不過就是將整個文件讀到內(nèi)存中,但是事實上并不是這樣。一般來說,只有文件中實際讀取或者寫入的部分才會送入(或者 映射 )到內(nèi)存中。

內(nèi)存映射并不真的神奇或者多么不尋?!,F(xiàn)代操作系統(tǒng)一般根據(jù)需要將文件的部分映射為內(nèi)存的部分,從而實現(xiàn)文件系統(tǒng)。Java 內(nèi)存映射機制不過是在底層操作系統(tǒng)中可以采用這種機制時,提供了對該機制的訪問。

盡管創(chuàng)建內(nèi)存映射文件相當簡單,但是向它寫入可能是危險的。僅只是改變數(shù)組的單個元素這樣的簡單操作,就可能會直接修改磁盤上的文件。修改數(shù)據(jù)與將數(shù)據(jù)保存到磁盤是沒有分開的。

將文件映射到內(nèi)存

了解內(nèi)存映射的最好方法是使用例子。在下面的例子中,我們要將一個 FileChannel (它的全部或者部分)映射到內(nèi)存中。為此我們將使用 FileChannel.map() 方法。下面代碼行將文件的前 1024 個字節(jié)映射到內(nèi)存中:

 
 
 
 
  1. MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE,     0, 1024 ); 

map() 方法返回一個 MappedByteBuffer,它是 ByteBuffer 的子類。因此,您可以像使用其他任何 ByteBuffer 一樣使用新映射的緩沖區(qū),操作系統(tǒng)會在需要時負責執(zhí)行行映射。

◆  分散和聚集

概  述

分散/聚集 I/O 是使用多個而不是單個緩沖區(qū)來保存數(shù)據(jù)的讀寫方法。

一個分散的讀取就像一個常規(guī)通道讀取,只不過它是將數(shù)據(jù)讀到一個緩沖區(qū)數(shù)組中而不是讀到單個緩沖區(qū)中。同樣地,一個聚集寫入是向緩沖區(qū)數(shù)組而不是向單個緩沖區(qū)寫入數(shù)據(jù)。

分散/聚集 I/O 對于將數(shù)據(jù)流劃分為單獨的部分很有用,這有助于實現(xiàn)復雜的數(shù)據(jù)格式。

分散/聚集 I/O

通道可以有選擇地實現(xiàn)兩個新的接口: ScatteringByteChannel 和 GatheringByteChannel。一個 ScatteringByteChannel 是一個具有兩個附加讀方法的通道:

? long read( ByteBuffer[] dsts );

? long read( ByteBuffer[] dsts, int offset, int length );

這些 long read() 方法很像標準的 read 方法,只不過它們不是取單個緩沖區(qū)而是取一個緩沖區(qū)數(shù)組。

在 分散讀取 中,通道依次填充每個緩沖區(qū)。填滿一個緩沖區(qū)后,它就開始填充下一個。在某種意義上,緩沖區(qū)數(shù)組就像一個大緩沖區(qū)。

分散/聚集的應用

分散/聚集 I/O 對于將數(shù)據(jù)劃分為幾個部分很有用。例如,您可能在編寫一個使用消息對象的網(wǎng)絡應用程序,每一個消息被劃分為固定長度的頭部和固定長度的正文。您可以創(chuàng)建一個剛好可以容納頭部的緩沖區(qū)和另一個剛好可以容難正文的緩沖區(qū)。當您將它們放入一個數(shù)組中并使用分散讀取來向它們讀入消息時,頭部和正文將整齊地劃分到這兩個緩沖區(qū)中。

我們從緩沖區(qū)所得到的方便性對于緩沖區(qū)數(shù)組同樣有效。因為每一個緩沖區(qū)都跟蹤自己還可以接受多少數(shù)據(jù),所以分散讀取會自動找到有空間接受數(shù)據(jù)的第一個緩沖區(qū)。在這個緩沖區(qū)填滿后,它就會移動到下一個緩沖區(qū)。

聚集寫入

聚集寫入 類似于分散讀取,只不過是用來寫入。它也有接受緩沖區(qū)數(shù)組的方法:

? long write( ByteBuffer[] srcs );

? long write( ByteBuffer[] srcs, int offset, int length );

聚集寫對于把一組單獨的緩沖區(qū)中組成單個數(shù)據(jù)流很有用。為了與上面的消息例子保持一致,您可以使用聚集寫入來自動將網(wǎng)絡消息的各個部分組裝為單個數(shù)據(jù)流,以便跨越網(wǎng)絡傳輸消息。

從例子程序 UseScatterGather.java 中可以看到分散讀取和聚集寫入的實際應用。

◆  文件鎖定

概  述

文件鎖定初看起來可能讓人迷惑。它 似乎 指的是防止程序或者用戶訪問特定文件。事實上,文件鎖就像常規(guī)的 Java 對象鎖 ― 它們是 勸告式的(advisory) 鎖。它們不阻止任何形式的數(shù)據(jù)訪問,相反,它們通過鎖的共享和獲取賴允許系統(tǒng)的不同部分相互協(xié)調(diào)。

您可以鎖定整個文件或者文件的一部分。如果您獲取一個排它鎖,那么其他人就不能獲得同一個文件或者文件的一部分上的鎖。如果您獲得一個共享鎖,那么其他人可以獲得同一個文件或者文件一部分上的共享鎖,但是不能獲得排它鎖。文件鎖定并不總是出于保護數(shù)據(jù)的目的。例如,您可能臨時鎖定一個文件以保證特定的寫操作成為原子的,而不會有其他程序的干擾。

大多數(shù)操作系統(tǒng)提供了文件系統(tǒng)鎖,但是它們并不都是采用同樣的方式。有些實現(xiàn)提供了共享鎖,而另一些僅提供了排它鎖。事實上,有些實現(xiàn)使得文件的鎖定部分不可訪問,盡管大多數(shù)實現(xiàn)不是這樣的。

在本節(jié)中,您將學習如何在 NIO 中執(zhí)行簡單的文件鎖過程,我們還將探討一些保證被鎖定的文件盡可能可移植的方法。

鎖定文件

要獲取文件的一部分上的鎖,您要調(diào)用一個打開的 FileChannel 上的 lock() 方法。注意,如果要獲取一個排它鎖,您必須以寫方式打開文件。

 
 
 
 
  1. RandomAccessFile raf = new RandomAccessFile( "usefilelocks.txt", "rw" );  
  2. FileChannel fc = raf.getChannel();  
  3. FileLock lock = fc.lock( start, end, false ); 

在擁有鎖之后,您可以執(zhí)行需要的任何敏感操作,然后再釋放鎖:

 
 
 
 
  1. lock.release(); 

在釋放鎖后,嘗試獲得鎖的其他任何程序都有機會獲得它。

本小節(jié)的例子程序 UseFileLocks.java 必須與它自己并行運行。這個程序獲取一個文件上的鎖,持有三秒鐘,然后釋放它。如果同時運行這個程序的多個實例,您會看到每個實例依次獲得鎖。

文件鎖定可能是一個復雜的操作,特別是考慮到不同的操作系統(tǒng)是以不同的方式實現(xiàn)鎖這一事實。下面的指導原則將幫助您盡可能保持代碼的可移植性:

? 只使用排它鎖。

? 將所有的鎖視為勸告式的(advisory)。

#p#

◆  連網(wǎng)和異步 I/O

概  述

連網(wǎng)是學習異步 I/O 的很好基礎(chǔ),而異步 I/O 對于在 Java 語言中執(zhí)行任何輸入/輸出過程的人來說,無疑都是必須具備的知識。NIO 中的連網(wǎng)與 NIO 中的其他任何操作沒有什么不同 ― 它依賴通道和緩沖區(qū),而您通常使用 InputStream 和 OutputStream 來獲得通道。

本節(jié)首先介紹異步 I/O 的基礎(chǔ) ― 它是什么以及它不是什么,然后轉(zhuǎn)向更實用的、程序性的例子。

異步 I/O

異步 I/O 是一種 沒有阻塞地 讀寫數(shù)據(jù)的方法。通常,在代碼進行 read() 調(diào)用時,代碼會阻塞直至有可供讀取的數(shù)據(jù)。同樣,write() 調(diào)用將會阻塞直至數(shù)據(jù)能夠?qū)懭搿?/p>

另一方面,異步 I/O 調(diào)用不會阻塞。相反,您將注冊對特定 I/O 事件的興趣 ― 可讀的數(shù)據(jù)的到達、新的套接字連接,等等,而在發(fā)生這樣的事件時,系統(tǒng)將會告訴您。

異步 I/O 的一個優(yōu)勢在于,它允許您同時根據(jù)大量的輸入和輸出執(zhí)行 I/O。同步程序常常要求助于輪詢,或者創(chuàng)建許許多多的線程以處理大量的連接。使用異步 I/O,您可以監(jiān)聽任何數(shù)量的通道上的事件,不用輪詢,也不用額外的線程。

我們將通過研究一個名為 MultiPortEcho.java 的例子程序來查看異步 I/O 的實際應用。這個程序就像傳統(tǒng)的 echo server,它接受網(wǎng)絡連接并向它們回響它們可能發(fā)送的數(shù)據(jù)。不過它有一個附加的特性,就是它能同時監(jiān)聽多個端口,并處理來自所有這些端口的連接。并且它只在單個線程中完成所有這些工作。

Selectors

本節(jié)的闡述對應于 MultiPortEcho 的源代碼中的 go() 方法的實現(xiàn),因此應該看一下源代碼,以便對所發(fā)生的事情有個更全面的了解。

異步 I/O 中的核心對象名為 Selector。Selector 就是您注冊對各種 I/O 事件的興趣的地方,而且當那些事件發(fā)生時,就是這個對象告訴您所發(fā)生的事件。

所以,我們需要做的第一件事就是創(chuàng)建一個 Selector:

 
 
 
 
  1. Selector selector = Selector.open(); 

然后,我們將對不同的通道對象調(diào)用 register() 方法,以便注冊我們對這些對象中發(fā)生的 I/O 事件的興趣。register() 的第一個參數(shù)總是這個 Selector。

打開一個 ServerSocketChannel

為了接收連接,我們需要一個 ServerSocketChannel。事實上,我們要監(jiān)聽的每一個端口都需要有一個 ServerSocketChannel 。對于每一個端口,我們打開一個 ServerSocketChannel,如下所示:

 
 
 
 
  1. ServerSocketChannel ssc = ServerSocketChannel.open();  
  2. ssc.configureBlocking( false );   
  3. ServerSocket ss = ssc.socket();  
  4. InetSocketAddress address = new InetSocketAddress( ports[i] );  
  5. ss.bind( address ); 

第一行創(chuàng)建一個新的 ServerSocketChannel ,最后三行將它綁定到給定的端口。第二行將 ServerSocketChannel 設(shè)置為 非阻塞的 。我們必須對每一個要使用的套接字通道調(diào)用這個方法,否則異步 I/O 就不能工作。

選擇鍵

下一步是將新打開的 ServerSocketChannels 注冊到 Selector上。為此我們使用 ServerSocketChannel.register() 方法,如下所示

 
 
 
 
  1. SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT ); 

register() 的第一個參數(shù)總是這個 Selector。第二個參數(shù)是 OP_ACCEPT,這里它指定我們想要監(jiān)聽 accept 事件,也就是在新的連接建立時所發(fā)生的事件。這是適用于 ServerSocketChannel 的唯一事件類型。

請注意對 register() 的調(diào)用的返回值。 SelectionKey 代表這個通道在此 Selector 上的這個注冊。當某個 Selector 通知您某個傳入事件時,它是通過提供對應于該事件的 SelectionKey 來進行的。SelectionKey 還可以用于取消通道的注冊。

內(nèi)部循環(huán)

現(xiàn)在已經(jīng)注冊了我們對一些 I/O 事件的興趣,下面將進入主循環(huán)。使用 Selectors 的幾乎每個程序都像下面這樣使用內(nèi)部循環(huán):

 
 
 
 
  1. int num = selector.select();   
  2. Set selectedKeys = selector.selectedKeys();  
  3. Iterator it = selectedKeys.iterator();   
  4. while (it.hasNext()) {  
  5.      SelectionKey key = (SelectionKey)it.next();  
  6.      // ... deal with I/O event ...} 

首先,我們調(diào)用 Selector 的 select() 方法。這個方法會阻塞,直到至少有一個已注冊的事件發(fā)生。當一個或者更多的事件發(fā)生時,select() 方法將返回所發(fā)生的事件的數(shù)量。

接下來,我們調(diào)用 Selector 的 selectedKeys() 方法,它返回發(fā)生了事件的 SelectionKey 對象的一個 集合。

我們通過迭代 SelectionKeys 并依次處理每個 SelectionKey 來處理事件。對于每一個 SelectionKey,您必須確定發(fā)生的是什么 I/O 事件,以及這個事件影響哪些 I/O 對象。

監(jiān)聽新連接

程序執(zhí)行到這里,我們僅注冊了 ServerSocketChannel,并且僅注冊它們“接收”事件。為確認這一點,我們對 SelectionKey 調(diào)用readyOps() 方法,并檢查發(fā)生了什么類型的事件:

 
 
 
 
  1. if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {  
  2.         // Accept the new connection  
  3.        // ...  
  4. }  

可以肯定地說, readOps() 方法告訴我們該事件是新的連接。

接受新的連接

因為我們知道這個服務器套接字上有一個傳入連接在等待,所以可以安全地接受它;也就是說,不用擔心 accept() 操作會阻塞:

 
 
 
 
  1. ServerSocketChannel ssc = (ServerSocketChannel)key.channel();  
  2. SocketChannel sc = ssc.accept(); 

下一步是將新連接的 SocketChannel 配置為非阻塞的。而且由于接受這個連接的目的是為了讀取來自套接字的數(shù)據(jù),所以我們還必須將 SocketChannel 注冊到 Selector上,如下所示:

 
 
 
 
  1. sc.configureBlocking( false );  
  2. SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ ); 

注意我們使用 register() 的 OP_READ 參數(shù),將 SocketChannel 注冊用于 讀取 而不是 接受 新連接。

刪除處理過的 SelectionKey

在處理 SelectionKey 之后,我們幾乎可以返回主循環(huán)了。但是我們必須首先將處理過的 SelectionKey 從選定的鍵集合中刪除。如果我們沒有刪除處理過的鍵,那么它仍然會在主集合中以一個激活的鍵出現(xiàn),這會導致我們嘗試再次處理它。我們調(diào)用迭代器的remove() 方法來刪除處理過的 SelectionKey:

 
 
 
 
  1. it.remove(); 

現(xiàn)在我們可以返回主循環(huán)并接受從一個套接字中傳入的數(shù)據(jù)(或者一個傳入的 I/O 事件)了。

傳入的 I/O

當來自一個套接字的數(shù)據(jù)到達時,它會觸發(fā)一個 I/O 事件。這會導致在主循環(huán)中調(diào)用 Selector.select(),并返回一個或者多個 I/O 事件。這一次, SelectionKey 將被標記為 OP_READ 事件,如下所示:

 
 
 
 
  1. } else if ((key.readyOps() & SelectionKey.OP_READ)     == SelectionKey.OP_READ) {  
  2.      // Read the data  
  3.      SocketChannel sc = (SocketChannel)key.channel();  
  4.      // ...} 

與以前一樣,我們?nèi)〉冒l(fā)生 I/O 事件的通道并處理它。在本例中,由于這是一個 echo server,我們只希望從套接字中讀取數(shù)據(jù)并馬上將它發(fā)送回去。

每次返回主循環(huán),我們都要調(diào)用 select 的 Selector()方法,并取得一組 SelectionKey。每個鍵代表一個 I/O 事件。我們處理事件,從選定的鍵集中刪除 SelectionKey,然后返回主循環(huán)的頂部。

這個程序有點過于簡單,因為它的目的只是展示異步 I/O 所涉及的技術(shù)。在現(xiàn)實的應用程序中,您需要通過將通道從 Selector 中刪除來處理關(guān)閉的通道。而且您可能要使用多個線程。這個程序可以僅使用一個線程,因為它只是一個演示,但是在現(xiàn)實場景中,創(chuàng)建一個線程池來負責 I/O 事件處理中的耗時部分會更有意義。

字符集

根據(jù) Sun 的文檔,一個 Charset 是“十六位 Unicode 字符序列與字節(jié)序列之間的一個命名的映射”。實際上,一個 Charset 允許您以盡可能最具可移植性的方式讀寫字符序列。

Java 語言被定義為基于 Unicode。然而在實際上,許多人編寫代碼時都假設(shè)一個字符在磁盤上或者在網(wǎng)絡流中用一個字節(jié)表示。這種假設(shè)在許多情況下成立,但是并不是在所有情況下都成立,而且隨著計算機變得對 Unicode 越來越友好,這個假設(shè)就日益變得不能成立了。

在本節(jié)中,我們將看一下如何使用 Charsets 以適合現(xiàn)代文本格式的方式處理文本數(shù)據(jù)。這里將使用的示例程序相當簡單,不過,它觸及了使用 Charset 的所有關(guān)鍵方面:為給定的字符編碼創(chuàng)建 Charset,以及使用該 Charset 解碼和編碼文本數(shù)據(jù)。

編碼/解碼

要讀和寫文本,我們要分別使用 CharsetDecoder 和 CharsetEncoder。將它們稱為 編碼器 和 解碼器 是有道理的。一個 字符 不再表示一個特定的位模式,而是表示字符系統(tǒng)中的一個實體。因此,由某個實際的位模式表示的字符必須以某種特定的 編碼 來表示。

CharsetDecoder 用于將逐位表示的一串字符轉(zhuǎn)換為具體的 char 值。同樣,一個 CharsetEncoder 用于將字符轉(zhuǎn)換回位。

在下一個小節(jié)中,我們將考察一個使用這些對象來讀寫數(shù)據(jù)的程序。

處理文本的正確方式

現(xiàn)在我們將分析這個例子程序 UseCharsets.java。這個程序非常簡單 ― 它從一個文件中讀取一些文本,并將該文本寫入另一個文件。但是它把該數(shù)據(jù)當作文本數(shù)據(jù),并使用 CharBuffer 來將該數(shù)句讀入一個 CharsetDecoder 中。同樣,它使用 CharsetEncoder 來寫回該數(shù)據(jù)。

我們將假設(shè)字符以 ISO-8859-1(Latin1) 字符集(這是 ASCII 的標準擴展)的形式儲存在磁盤上。盡管我們必須為使用 Unicode 做好準備,但是也必須認識到不同的文件是以不同的格式儲存的,而 ASCII 無疑是非常普遍的一種格式。事實上,每種 Java 實現(xiàn)都要求對以下字符編碼提供完全的支持:

? US-ASCII

? ISO-8859-1

? UTF-8

? UTF-16BE

? UTF-16LE

? UTF-16

示例程序

在打開相應的文件、將輸入數(shù)據(jù)讀入名為 inputData 的 ByteBuffer 之后,我們的程序必須創(chuàng)建 ISO-8859-1 (Latin1) 字符集的一個實例:

 
 
 
 
  1. Charset latin1 = Charset.forName( "ISO-8859-1" ); 

然后,創(chuàng)建一個解碼器(用于讀?。┖鸵粋€編碼器 (用于寫入):

 
 
 
 
  1. CharsetDecoder decoder = latin1.newDecoder();  
  2. CharsetEncoder encoder = latin1.newEncoder(); 

為了將字節(jié)數(shù)據(jù)解碼為一組字符,我們把 ByteBuffer 傳遞給 CharsetDecoder,結(jié)果得到一個 CharBuffer

 
 
 
 
  1. CharBuffer cb = decoder.decode( inputData ); 

如果想要處理字符,我們可以在程序的此處進行。但是我們只想無改變地將它寫回,所以沒有什么要做的。

要寫回數(shù)據(jù),我們必須使用 CharsetEncoder 將它轉(zhuǎn)換回字節(jié):

 
 
 
 
  1. ByteBuffer outputData = encoder.encode( cb ); 

在轉(zhuǎn)換完成之后,我們就可以將數(shù)據(jù)寫到文件中了。

結(jié)束語和參考資料

結(jié)束語

正如您所看到的, NIO 庫有大量的特性。在一些新特性(例如文件鎖定和字符集)提供新功能的同時,許多特性在優(yōu)化方面也非常優(yōu)秀。

在基礎(chǔ)層次上,通道和緩沖區(qū)可以做的事情幾乎都可以用原來的面向流的類來完成。但是通道和緩沖區(qū)允許以 快得多 的方式完成這些相同的舊操作 ― 事實上接近系統(tǒng)所允許的最大速度。

不過 NIO 最強大的長度之一在于,它提供了一種在 Java 語言中執(zhí)行進行輸入/輸出的新的(也是迫切需要的)結(jié)構(gòu)化方式。隨諸如緩沖區(qū)、通道和異步 I/O 這些概念性(且可實現(xiàn)的)實體而來的,是我們重新思考 Java 程序中的 I/O過程的機會。這樣,NIO 甚至為我們最熟悉的 I/O 過程也帶來了新的活力,同時賦予我們通過和以前不同并且更好的方式執(zhí)行它們的機會。

原文鏈接:http://www.cnblogs.com/rollenholt/archive/2011/09/29/2195730.html

【編輯推薦】

  1. Java NIO 異步讀取網(wǎng)絡數(shù)據(jù)
  2. Java NIO(異步IO)Socket通信例子
  3. 影響Java NIO框架性能的因數(shù)
  4. Java的NIO以及線程并發(fā)
  5. 基于事件的NIO多線程服務器

網(wǎng)站名稱:全面解讀JavaNIO工作原理
當前路徑:http://m.5511xx.com/article/dhshdhp.html