1. 程式人生 > >Java NIO入門學習(一)

Java NIO入門學習(一)

本文為NIO入門學習的第一篇,將會介紹NIO中幾個重要的概念。

I/O即輸入輸出,指的是計算機和外界的介面,或者是單個程式同計算機其他部分的介面。 在Java1.4之前的I/O系統中,提供的都是面向流的I/O系統,系統每次處理一個位元組,輸入流(input stream)生產一個位元組,輸出流(output stream)消費一個位元組。這種工作模式下,非常容易給流資料建立過濾器(filters),而且也很容易將多個過濾器串起來,每個過濾器針對流過自己的位元組做相應處理。另一方面,在這種工作模式下面向流的IO通常很慢。而在Java 1.4中推出了NIO(New I/O),這是一個面向塊的I/O系統,系統以塊為單位處理資料,每個操作都會生產或者消費一“塊”資料,以塊為單位處理資料會比以位元組(流)為單位處理資料快很多。但是面向塊的IO系統同時也損失了一些優雅而簡單的操作方式。 

在NIO中有幾個核心物件需要掌握:緩衝區(Buffer)、通道(Channel)、選擇器(Selector)。

緩衝區Buffer

Buffer本質上說是一個容器物件。任何傳送到Channel的資料都必須先放進Buffer,類似的,任何從Channel中讀出的資料都先讀進Buffer。

Buffer就是一個裝載資料的容器物件,資料從Buffer中讀出,或者把資料寫入Buffer中。在NIO中添加了Buffer物件,這是NIO和老IO最重要的區別。在面向流的I/O中,你可以把資料直接寫入Stream物件,或者直接把資料從Stream中讀出來,而不需要任何容器。 

Buffer本質上就是一個數組(array)。通常它是一個位元組陣列,或者其他種類的可用陣列。但是Buffer除了是一個數組之外,它還提供了結構化的訪問資料的方法,並且還用來跟蹤系統的讀/寫過程。 

在NIO中,所有的緩衝區型別都繼承於抽象類Buffer,最常用的就是ByteBuffer。對於Java中的基本資料型別,基本上都有一個具體Buffer型別與之相對應,它們之間的繼承關係如下圖所示:


下面是一個簡單的使用IntBuffer的例子:

import java.nio.IntBuffer;

public class TestIntBuffer {

	public static void main(String[] args) {
		// 分配新的int緩衝區,引數為緩衝區容量
		// 新緩衝區的當前位置position將為零,其界限(限制位置)limit將為其容量。
		// 它將具有一個底層實現陣列,其陣列偏移量將為零。
		IntBuffer buffer = IntBuffer.allocate(8);

		for (int i = 0; i < buffer.capacity(); ++i) {
			int j = 2 * (i + 1);
			// 將給定整數寫入buffer的當前位置
			buffer.put(j);
		}

		// 重設buffer,將limit設定為position,position設定為0
		buffer.flip();

		// 檢視在position和limit之間是否有元素
		while (buffer.hasRemaining()) {
			// 讀取buffer當前位置的整數
			int j = buffer.get();
			System.out.print(j + " ");
		}
	}

}

執行後可以看到:


在後面我們還會繼續分析Buffer物件,以及它的幾個重要的屬性。

通道Channel

Channel模擬了老IO包中的流的概念。所有去任何地方(或者來自任何地方)的資料都必須通過Channel物件。可以從Channel中讀取資料,也可以從Channel中寫入資料。NIO與老IO相比而言,Channel就如同Stream。 

所有被NIO處理的資料都必須通過Buffer物件。不能直接將任何位元組寫入Channel,而是必須先將資料寫入Buffer。同樣的,也不能直接從Channel中讀取任何位元組,必須先通過Channel將資料讀入Buffer,然後再從Buffer中獲取資料。 

Channel與Stream的區別在於:Channel是雙向的,而Stream只能是單向的(Stream必須是InputStream或者OutputStream的一個子類,即要麼是輸入流,要麼是輸出流,不能即輸入又輸出)。Channel在被開啟之後,即可以讀,也可以寫,或者同時進行讀寫操作。 因為Channel是雙向的,因此它比Stream更好的反應了底層作業系統IO的實質。特別是在Linux系統中,底層作業系統都是雙向的。

在NIO中,提供了多種通道物件,而所有的通道物件都實現了Channel介面。它們之間的繼承關係如下圖所示:


使用NIO讀取資料

在第一個練習中,我們首先從檔案中讀取一些資料。如果使用老的IO,只需要簡單的建立FileInputStream,然後從中讀取資料。在NIO中,事情變得有些不同了,首先需要從FileInputStream中獲取Channel物件,然後使用這個Channel去讀檔案。 

在NIO系統中任何時刻執行一個讀操作時,都要從Buffer中讀,但不是直接從Channel中讀。由於所有的資料都需要通過Buffer承載,所以需首先從Channel中把資料讀進Buffer。

因此,從檔案中讀資料一共有三步:

1. 從FileInputStream中獲取Channel

2. 建立Buffer

3. 從Channel中把資料讀入Buffer

下面是一個簡單的使用NIO從檔案中讀取資料的例子:

import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class TestChannelRead {

	public static void main(String[] args) throws Exception {
		FileInputStream fileInputStream = new FileInputStream("D:\\test.txt");
		// 獲取通道
		FileChannel fileChannel = fileInputStream.getChannel();

		// 建立緩衝區
		ByteBuffer buffer = ByteBuffer.allocate(1024);

		// 讀取資料到緩衝區
		fileChannel.read(buffer);

		// 重設buffer,將limit設定為position,position設定為0
		buffer.flip();

		// 檢視在position和limit之間是否有元素
		while (buffer.hasRemaining()) {
			// 讀取buffer當前位置的整數
			byte b = buffer.get();
			System.out.print((char) b);
		}

		fileInputStream.close();
	}

}
你一定發現,在這裡,並沒有告訴Channel把多少內容讀進Buffer。在每個Buffer中都有一套完整的內部計數系統來跟蹤已經讀了多少資料了,Buffer中還剩多少空間。關於Buffer的計數系統在隨後的“Buffer內部原理”中講解。

使用NIO寫入資料

使用NIO寫入資料與讀取資料的過程類似,同樣資料不是直接寫入Channel,而是先將資料寫入Buffer,可以分為下面三個步驟:

1. 從FileOutputStream獲取Channel

2. 建立Buffer並且把資料放到Buffer中

3. 將Buffer中的資料寫入Channel

下面是一個簡單的使用NIO向檔案中寫入資料的例子:

import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class TestChannelWrite {

	private static byte message[] = { 83, 111, 109, 101, 32, 98, 121, 116, 101,
			115, 46 };

	public static void main(String[] args) throws Exception {
		FileOutputStream fileOutputStream = new FileOutputStream("D:\\test.txt");
		// 獲取通道
		FileChannel fileChannel = fileOutputStream.getChannel();

		// 建立緩衝區
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		// 資料存入緩衝區
		for (int i = 0; i < message.length; ++i) {
			buffer.put(message[i]);
		}
		// 重設buffer,將limit設定為position,position設定為0
		buffer.flip();

		// 將buffer中的資料寫入
		fileChannel.write(buffer);

		fileOutputStream.close();
	}

}
再次注意,我們沒有必要告訴Channel總共要寫多少資料,Buffer的內部計數系統會跟蹤已經寫了多少資料了,還剩多少空間可以使用。

本文介紹了Java NIO中三個核心概念中的兩個,並且看了兩個簡單的示例,分別是使用NIO進行資料的讀取和寫入,下篇將會介紹Buffer內部實現。

參考:

NIO 入門