不學無數—Java 中 IO 和 NIO

使用過濾器添加有用的屬性和有用的接口

Java的I/O類庫需要多種不同功能的組合,這正是裝飾模式的理由所在。而這也是java的I/O類庫中存在Filter(過濾器)類的原因所在,Filter作為所有裝飾類的基類。

類功能BufferedInputStream使用它可以防止每次讀取都進行與磁盤的交互,使用緩衝區進行一次性讀取固定值的以後再向磁盤中執行寫操作,減少了與磁盤的交互次數。提高速度DataInputStream允許應用程序以與機器無關方式從底層輸入流中讀取基本 Java 數據類型

舉個簡單使用過濾器進行讀取一個文件的內容並輸出,例子如下:

public static void main(String[] args) throws IOException {
InputStream inputStream = new BufferedInputStream(new FileInputStream("/Users/hupengfei/Downloads/a.sql"));
byte[] buffer = new byte[1024];
while ( inputStream.read(buffer)!=-1){
System.out.println(new String(buffer));
}
inputStream.close();
}
複製代碼

複製一個文件的例子:

public static void main(String[] args) throws IOException {
InputStream inputStream =new BufferedInputStream(new FileInputStream("/Users/hupengfei/Downloads/leijicheng.png"));
OutputStream outputStream =new BufferedOutputStream(new FileOutputStream("/Users/hupengfei/Downloads/fuzhi.png"));
byte [] buffer = new byte[1024];
while (inputStream.read(buffer)!=-1){
outputStream.write(buffer);
}
outputStream.flush();

inputStream.close();
outputStream.close();
}
複製代碼

如果要使用 BufferedOutputStream 進行在文件中寫入的話,那麼在緩衝區寫完之後要記得調用 flush() 清空緩衝區。強行將緩衝區中的數據寫出。否則可能無法寫出數據。

基於字符的操作

不管是磁盤還是網絡傳輸,最小的存儲單元都是字節,而不是字符,所以 I/O 操作的都是字節而不是字符,但是為啥有操作字符的 I/O 接口呢?這是因為我們的程序中通常操作的數據都是以字符形式,為了操作方便當然要提供一個直接寫字符的 I/O 接口。

還是老規矩,我們先來看一下關於 Reader 的類圖,對應的字節流是 InputStream

不學無數—Java 中 IO 和 NIO

其中的 InputStreamReader 是可以將 InputStream 轉換為 Reader 即將字節翻譯為字符。其中為什麼要設計 Reader 和 Writer ,主要是為了國際化,之前的字節流僅僅支持8位的字節流,不能很好的處理16位的Unicode字符,由於Unicode用於字符國際化,所以添加了 Reader 和 Writer 是為了在所有的I/O操作中都支持Unicode。

在某些場合,面向字節流 InputStream 和 OutputStream 才是正確的解決方案,特別是在 java.util.zip 類庫就是面向字節流而不是面向字符的。因此,最明智的做法就是儘量優先使用 Reader 和 Writer ,一旦程序無法編譯,那麼我們就會發現自己不得不使用面向字節類庫。

還是寫一個相關的讀取文件的簡單例子

public static void main(String[] args) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new FileReader("/Users/hupengfei/Downloads/a.sql"));
String date;
StringBuilder stringBuilder = new StringBuilder();
while ((date = bufferedReader.readLine()) != null){
stringBuilder.append(date +"\n");
}
bufferedReader.close();
System.out.println(stringBuilder.toString());
}
複製代碼

調用 readLine() 方法時要添加換行符,因為 readLine() 自動將換行符給刪除了

NIO又是什麼

在 JDK1.4 中添加了NIO類,我們也可以稱之為新I/O。NIO 的創建目的是為了讓 Java 程序員可以實現高速 I/O 而無需編寫自定義的本機代碼。NIO 將最耗時的 I/O 操作(即填充和提取緩衝區)轉移回操作系統,因而可以極大地提高速度。

速度的提高來自於所使用的結構更接近於操作系統執行I/O的方式:通道(Channel)和緩衝器(Buffer)

通道和緩衝器是NIO中的核心對象,幾乎每一個I/O操作中都會使用它們。通道是對原I/O包中的流的模擬。到任何地方(來自任何地方)的數據都得必須通過一個Channel對象。一個Buffer實質上是一個容器對象。發送給一個通道的所有對象都必須首先放到Buffer緩衝器中。

我們可以將它們想象成一個煤礦,通道就是一個包含煤礦(數據)的礦藏,而緩衝器就是派送到礦藏中的礦車,礦車載滿煤炭而歸,我們再從礦車上獲取煤炭。也就是說,我們並沒有直接和通道交互,我們只是和緩衝器進行交互。

緩衝器(Buffer)介紹

Buffer是一個對象,它包含著一些需要讀取的數據或者是要傳輸的數據。在NIO中加入了Buffer對象,體現了和之前的I/O的一個重要的區別。在面向流的I/O中我們直接通過流對象直接和數據進行交互的,但是在NIO中我們和數據的交互必須通過Buffer了。

緩衝器實質上是一個數組。通常它是一個字節的數組,但是也可以使用其他種類的數組。但是一個緩衝器不僅僅是一個數組,緩衝器提供了對數據結構化的訪問,而且還可以跟蹤系統的讀寫進程。

接下來我們可以看一下 Buffer 相關的實現類

不學無數—Java 中 IO 和 NIO

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

ByteBuffer 是唯一一個直接與通道交互的緩衝器——也就說,可以存儲未加工字節的緩衝器。當我們查看 ByteBuffer 源碼時會發現其通過告知分配多少存儲空間來創建一個 ByteBuffer 對象,並且還有一個方法選擇集,用於以原始的字節形式或者基本數據類型輸出和讀取數據。但是,也沒辦法輸出或者讀取對象,即是是字符串的對象也不行。這種處理方式雖然很低級,但是正好,因為這是大多數操作系統中更有效的映射方式。

通道(Channel)介紹

Channel 是一個對象,緩衝器可以通過它進行讀取和寫入數據。和原來的I/O做個比較,通道就像個流。正如前面所提到的, Channel 是不和數據進行交互。但是它和流有一點不同,就是通道是雙向的,而流只能是單向的(只能是InputStream或者OutputStream),但是通道可以用於讀、寫或者是同時用於讀寫。

在之前的I/O中有三個類被修改,可以用來產生 FileChannel 對象。這三個類是 FileInputStream 、 FileOutputStream 以及既用於讀也用於寫的 RandomAccessFile 。

下面就舉個創建 FileChannel 的例子。

FileChannel in = new FileInputStream("fileName").getChannel();
複製代碼

NIO的使用

我會舉一個簡單的例子來演示如何使用NIO對文件進行復制的操作。還是上面所說的,NIO中對數據操作的是緩衝器,和緩衝器交互的通道,所以現在需要我們有兩個對象一個是 Buffer和 Channel 。

public static void main(String[] args) throws IOException {
//獲取讀通道
FileChannel in = new FileInputStream("/Users/hupengfei/Downloads/hu.sql").getChannel();
//獲取寫通道
FileChannel out = new FileOutputStream("/Users/hupengfei/Downloads/a.sql").getChannel();
//為緩衝器進行初始化大小
ByteBuffer byteBuffer =ByteBuffer.allocate(1024);
while (in.read(byteBuffer)!=-1){
//做好讓人讀的準備
byteBuffer.flip();
out.write(byteBuffer);
//清除數據
byteBuffer.clear();
}
}
複製代碼

一旦要用從緩衝器中讀取數據的話,那麼就要調用緩衝器的 flip() 方法,讓它做好讓別人讀取字節的準備。那麼寫完數據以後就要調用緩存器的 clear() 方法對所有的內部的指針重新安排,以便緩衝器在另一個 read() 操作期間能夠做好接受數據的準備。然後數據就會從源文件中源源不斷的讀到了目標文件中。

clear() 方法在源碼中有介紹,此方法不會實際的清除在緩衝器的數據。

當然上面的方法也可以簡便,直接將兩個通道進行相連只需要調用 transferTo() 方法,這個也是複製文件的效果。

public static void main(String[] args) throws IOException {
FileChannel in = new FileInputStream("/Users/hupengfei/Downloads/hu.sql").getChannel();
FileChannel out = new FileOutputStream("/Users/hupengfei/Downloads/a.sql").getChannel();
in.transferTo(0,in.size(),out);
}
複製代碼


分享到:


相關文章: