不學無數——Java中IO和NIO

不學無數的程式設計師發表於2018-09-20

JAVA中的I/O和NIO

I/O 問題是任何程式語言都無法迴避的問題,可以說 I/O 問題是整個人機互動的核心問題,因為 I/O 是機器獲取和交換資訊的主要渠道。在當今這個資料大爆炸時代,I/O 問題尤其突出,很容易成為一個效能瓶頸。

什麼是I/O

I/O ? 或者輸入/輸出 ? 指的是計算機與外部世界或者一個程式與計算機的其餘部分的之間的介面。它對於任何計算機系統都非常關鍵,因而所有 I/O 的主體實際上是內建在作業系統中的。單獨的程式一般是讓系統為它們完成大部分的工作。

  • I:就是從硬碟將內容讀取到記憶體
  • O:就是從記憶體將內容讀取到硬碟

其中有些情況下I/O是沒有和硬碟進行互動的,例如管道流,涉及到兩個執行緒之間的通訊。管道本身是一個分配在記憶體中的迴圈緩衝區,只能被連線它的兩個執行緒使用。

Java中的I/O操作類在包java.io下面,大概將近有80多個類,但是這些類可以分為三組

  • 基於位元組操作的I/O介面:InputStreamOutputStream
  • 基於字元操作的 I/O 介面:WriterReader
  • 基於磁碟操作的 I/O 介面:File

然後在各個介面下還有其各自的包裝類,其運用到了裝飾模式,為其增加一些功能,而Java的I/O複雜也在這,不同的裝飾模式建立類的程式碼也不同。

基於位元組操作

InputStream的作用是用來表示那些從不同資料來源產生輸入的類,這些資料來源包括

  • 位元組陣列
  • String物件
  • 檔案
  • 管道,工作方式和實際中的管道相同,從一端輸入,從另一端輸出
  • 其他的資料來源,例如Internet中的Socket連線

InputStream的類圖,OutputStream類圖和這個類似

不學無數——Java中IO和NIO

功能 構造器引數 如何使用
ByteArrayInputStream 允許將記憶體的緩衝區當做InputStream使用 緩衝區,位元組將其從中取出 作為資料來源:將其與FilterInputStream物件相連以提供有用的介面
StringBufferInputStream 將String轉換成InputStream 字串,底層實現實際使用StringBuffer 作為資料來源:將其與FilterInputStream物件相連提供有用介面
FileInputStream 用於從檔案中讀取資訊 字串,表示檔名,檔案或者FileDescriptor物件 作為一種資料來源,將其與FilterInputStream物件相連提供有用介面
PipedInputStream 產生用於寫入相關PipedOutputStream的額資料,實現管道化的概念 PipedOutputStream 作為多執行緒的資料來源:將其與FilterInputStream物件相連提供有用介面
FilterInputStream 抽象類,作為裝飾器的介面,為其他的InputStream提供有用的功能

使用過濾器新增有用的屬性和有用的介面

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

Reader類圖

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

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

還是寫一個相關的讀取檔案的簡單例子

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相關的實現類

Buffer相關實現類

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

ByteBuffer是唯一一個直接與通道互動的緩衝器——也就說,可以儲存未加工位元組的緩衝器。當我們檢視ByteBuffer原始碼時會發現其通過告知分配多少儲存空間來建立一個ByteBuffer物件,並且還有一個方法選擇集,用於以原始的位元組形式或者基本資料型別輸出和讀取資料。但是,也沒辦法輸出或者讀取物件,即是是字串的物件也不行。這種處理方式雖然很低階,但是正好,因為這是大多數作業系統中更有效的對映方式。

通道(Channel)介紹

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

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

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

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

NIO的使用

我會舉一個簡單的例子來演示如何使用NIO對檔案進行復制的操作。還是上面所說的,NIO中對資料操作的是緩衝器,和緩衝器互動的通道,所以現在需要我們有兩個物件一個是BufferChannel

    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);
    }

複製程式碼

參考文章

相關文章