1. 引言
I/O流或者輸入/輸出流指的是計算機與外部世界或者一個程序與計算機的其他部分的之間的接口。新的輸入/輸出(NIO)庫是在JDK 1.4中引入的。NIO彌補了原來的I/O的不足,它在標準Java代碼中提供了高速的、面向塊的I/O。
原來的I/O庫與NIO最重要的區別是數據打包和傳輸的方式的不一樣,原來的 I/O 以流 的方式處理數據,而 NIO 以塊 的方式處理數據。
面向流的I/O系統一次一個字節地處理數據。一個輸入流產生一個字節的數據,一個輸出流消費一個字節的數據。爲流式數據建立過濾器很是容易。連接幾個過濾器,以便每一個過濾器只負責單個複雜處理機制的一部分,這樣也是相對簡單的。不利的一面是,面向流的I/O一般至關慢。
NIO與原來的I/O有一樣的做用和目的,可是它使用塊I/O的處理方式。每個操做都在一步中產生或者消費一個數據塊。按塊處理數據比按(流式的)字節處理數據要快得多。可是面向塊的I/O缺乏一些面向流的I/O所具備的優雅性和簡單性。java
2. 從一個例子開始
下面咱們從一個簡單的使用IO和NIO讀取一個文件中的內容爲例,來進入NIO的學習之旅。
使用IO來讀取指定文件中的前1024字節並打印出來:sql
/** 數組
* 使用IO讀取指定文件的前1024個字節的內容。 學習
* @param file 指定文件名稱。 spa
* @throws java.io.IOException IO異常。 操作系統
*/ orm
public void ioRead(String file) throws IOException { 對象
FileInputStream in = new FileInputStream(file); 接口
byte[] b = new byte[1024];
in.read(b);
System.out.println(new String(b));
}
/**
* 使用NIO讀取指定文件的前1024個字節的內容。
* @param file 指定文件名稱。
* @throws java.io.IOException IO異常。
*/
public void nioRead(String file) throws IOException {
FileInputStream in = new FileInputStream(file);
FileChannel channel = in.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
byte[] b = buffer.array();
System.out.println(new String(b));
}
從上面的例子中能夠看出,NIO以通道Channel和緩衝區Buffer爲基礎來實現面向塊的IO數據處理。下面將討論並學習NIO 庫的核心概念以及從高級的特性到底層編程細節的幾乎全部方面。
3. 核心概念:通道和緩衝區
1) 概述:
通道和緩衝區是NIO中的核心對象,幾乎在每個I/O操做中都要使用它們。
通道Channel是對原I/O包中的流的模擬。到任何目的地(或來自任何地方)的全部數據都必須經過一個Channel對象。
緩衝區Buffer實質上是一個容器對象。發送給一個通道的全部對象都必須首先放到緩衝區中;一樣地,從通道中讀取的任何數據都要讀到緩衝區中。
2) 緩衝區:
Buffer是一個容器對象,它包含一些要寫入或者剛讀出的數據。在NIO中加入Buffer對象,體現了新庫與原I/O的一個重要區別。在面向流的I/O中,您將數據直接寫入或者將數據直接讀到Stream對象中。
在NIO庫中,全部數據都是用緩衝區處理的。在讀取數據時,它是直接讀到緩衝區中的。在寫入數據時,它是寫入到緩衝區中的。任什麼時候候訪問NIO中的數據,您都是將它放到緩衝區中。
緩衝區實質上是一個數組。一般它是一個字節數組,可是也可使用其餘種類的數組。可是一個緩衝區不只僅是一個數組。緩衝區提供了對數據的結構化訪問,並且還能夠跟蹤系統的讀/寫進程。
最經常使用的緩衝區類型是ByteBuffer。 一個ByteBuffer能夠在其底層字節數組上進行get/set操做(即字節的獲取和設置)。
ByteBuffer不是NIO中惟一的緩衝區類型。事實上,對於每一種基本Java類型都有一種緩衝區類型:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
每個Buffer類都是Buffer接口的一個實例。 除了ByteBuffer, 每個Buffer類都有徹底同樣的操做,只是它們所處理的數據類型不同。由於大多數標準I/O操做都使用ByteBuffer,因此它具備全部共享的緩衝區操做以及一些特有的操做。
下面的UseFloatBuffer列舉了使用類型化的緩衝區FloatBuffer的一個應用例子:
/**
* 使用 float 緩衝區。
* @version 1.00 2010-5-19, 10:30:59
* @since 1.5
* @author ZhangShixi
*/
public class UseFloatBuffer {
public static void main(String[] args) {
// 分配一個容量爲10的新的 float 緩衝區
FloatBuffer buffer = FloatBuffer.allocate(10);
for (int i = 0; i < buffer.capacity(); i++) {
float f = (float) Math.sin((((float) i) / 10) * (2 * Math.PI));
buffer.put(f);
}
// 反轉此緩衝區
buffer.flip();
// 告知在當前位置和限制之間是否有元素
while (buffer.hasRemaining()) {
float f = buffer.get();
System.out.println(f);
}
}
}
3) 通道:
Channel是對原I/O包中的流的模擬,能夠經過它讀取和寫入數據。拿NIO與原來的I/O作個比較,通道就像是流。
正如前面提到的,全部數據都經過Buffer對象來處理。您永遠不會將字節直接寫入通道中,相反,您是將數據寫入包含一個或者多個字節的緩衝區。一樣,您不會直接從通道中讀取字節,而是將數據從通道 讀入緩衝區,再從緩衝區獲取這個字節。
通道與流的不一樣之處在於通道是雙向的。而流只是在一個方向上移動(一個流必須是InputStream或者OutputStream的子類), 而通道能夠用於讀、寫或者同時用於讀寫。
由於它們是雙向的,因此通道能夠比流更好地反映底層操做系統的真實狀況。特別是在UNIX模型中,底層操做系統通道是雙向的。
4. 從理論到實踐:NIO中的讀和寫
1) 概述:
讀和寫是I/O的基本過程。從一個通道中讀取很簡單:只需建立一個緩衝區,而後讓通道將數據讀到這個緩衝區中。寫入也至關簡單:建立一個緩衝區,用數據填充它,而後讓通 道用這些數據來執行寫入操做。
2) 從文件中讀取:
若是使用原來的I/O,那麼咱們只需建立一個FileInputStream並從它那裏讀取。而在NIO中,狀況稍有不一樣:咱們首先從FileInputStream獲取一個FileChannel對象,而後使用這個通道來讀取數據。
在NIO系統中,任什麼時候候執行一個讀操做,您都是從通道中讀取,可是您不是直接從通道讀取。由於全部數據最終都駐留在緩衝區中,因此您是從通道讀到緩衝區中。
所以讀取文件涉及三個步驟:
(1) 從FileInputStream獲取Channel。
(2) 建立Buffer。
(3) 將數據從Channel讀到Buffer 中。
如今,讓咱們看一下這個過程。
// 第一步是獲取通道。咱們從 FileInputStream 獲取通道:
FileInputStream fin = new FileInputStream( "readandshow.txt" );
FileChannel fc = fin.getChannel();
// 下一步是建立緩衝區:
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
// 最後,須要將數據從通道讀到緩衝區中:
fc.read( buffer );
您會注意到,咱們不須要告訴通道要讀多少數據到緩衝區中。每個緩衝區都有複雜的內部統計機制,它會跟蹤已經讀了多少數據以及還有多少空間能夠容納更多的數據。咱們將在緩衝區內部細節中介紹更多關於緩衝區統計機制的內容。
3) 寫入文件:
在 NIO 中寫入文件相似於從文件中讀取。
// 首先從 FileOutputStream 獲取一個通道:
FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
FileChannel fc = fout.getChannel();
// 下一步是建立一個緩衝區並在其中放入一些數據,這裏,用message來表示一個持有數據的數組。
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
for (int i=0; i<message.length; ++i) {
buffer.put( message[i] );
}
buffer.flip();
// 最後一步是寫入緩衝區中:
fc.write( buffer );
注意在這裏一樣不須要告訴通道要寫入多數據。緩衝區的內部統計機制會跟蹤它包含多少數據以及還有多少數據要寫入。
4) 讀寫結合:
下面的示例將展現使用讀寫結合,將一個文件的全部內容拷貝到另外一個文件中。
/**
* 將一個文件的全部內容拷貝到另外一個文件中。
*
* CopyFile.java 執行三個基本操做:
* 首先建立一個 Buffer,而後從源文件中將數據讀到這個緩衝區中,而後將緩衝區寫入目標文件。
* 程序不斷重複 — 讀、寫、讀、寫 — 直到源文件結束。
*
* @version 1.00 2010-5-19, 10:49:46
* @since 1.5
* @author ZhangShixi
*/
public class CopyFile {
public static void main(String[] args) throws Exception {
String infile = "C:\\copy.sql";
String outfile = "C:\\copy.txt";
// 獲取源文件和目標文件的輸入輸出流
FileInputStream fin = new FileInputStream(infile);
FileOutputStream fout = new FileOutputStream(outfile);
// 獲取輸入輸出通道
FileChannel fcin = fin.getChannel();
FileChannel fcout = fout.getChannel();
// 建立緩衝區
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
// clear方法重設緩衝區,使它能夠接受讀入的數據
buffer.clear();
// 從輸入通道中將數據讀到緩衝區
int r = fcin.read(buffer);
// read方法返回讀取的字節數,可能爲零,若是該通道已到達流的末尾,則返回-1
if (r == -1) {
break;
}
// flip方法讓緩衝區能夠將新讀入的數據寫入另外一個通道
buffer.flip();
// 從輸出通道中將數據寫入緩衝區
fcout.write(buffer);
}
}
}