Java NIO核心概念及基本讀寫
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所具有的優雅性和簡單性。
/** * 使用IO讀取指定文件的前1024個字節的內容。 * @param file 指定文件名稱。 * @throws java.io.IOException IO異常。 */ 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 庫的核心概念以及從高級的特性到底層編程細節的幾乎所有方面。
2. 核心概念:通道和緩沖區
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,所以它具有所有共享的緩沖區操作以及一些特有的操作。
/** * 使用 float 緩沖區。 * @since 1.5 */ 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,然后從源文件中將數據讀到這個緩沖區中,然后將緩沖區寫入目標文件。 * 程序不斷重復 — 讀、寫、讀、寫 — 直到源文件結束。 * */ 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); } } }