Java位元組流和字元流,是時候總結一下IO流了

雙子孤狼發表於2021-04-12

從接收輸入值說起

在日常的開發應用中,有時候需要直接接收外部裝置如鍵盤等的輸入值,而對於這種資料的接收方式,我們一般有三種方法:位元組流讀取,字元流讀取,Scanner 工具類讀取。

位元組流讀取

直接看一個例子:

public class Demo01SystemIn {
    public static void main(String[] args) throws IOException {
        int a = System.in.read();
        System.out.println(a);
        char c = 'a';
        System.out.println((int) c);
    }
}

執行程式之後,會被 read 方法阻塞,這時候在控制檯輸入一個字元 a,那麼上面的程式兩句話都會輸出 97,這個沒問題,因為小寫字母 a 對應的就是 97,那麼假如我們輸入一箇中文會出現什麼結果呢?

把上面示例中的 a 修改為 ,然後執行程式,在控制檯同樣輸入 ,則會得到 22820013,這就說明我們控制檯輸入的 並沒有全部讀取,原因就是 read 只能讀取 1 個位元組,為了進一步驗證結論,我們將上面的例子進行改寫:

public class Demo01SystemIn {
    public static void main(String[] args) throws IOException {
        char a = (char) System.in.read();//讀取一個位元組
        System.out.println(a);
        char c = '中';
        System.out.println(c);
    }
}

執行之後得到如下結果:

可以看到,第一個輸出亂碼了,因為 System.in.read() 一次只能讀取一個位元組,而中文在 utf-8 編碼下佔用了 3 個位元組。正因為 read 方法一次只能讀取一個位元組,所以其範圍只能在 -1~255 之間,-1 表示已經讀取到了結尾。

那麼如果想要完整的讀取中文應該怎麼辦呢?

字元流讀取

我們先看下面一個例子:

public class Demo01SystemIn {
    public static void main(String[] args) throws IOException {
        InputStreamReader inputStreamReader1 = new InputStreamReader(System.in);
        int b = inputStreamReader1.read();//只能讀一個字元
        System.out.println(b);

        InputStreamReader inputStreamReader2 = new InputStreamReader(System.in);
        char[] chars = new char[2];
        int c = inputStreamReader2.read(chars);//讀入到指定char陣列,返回當前讀取到的字元數
        System.out.println("讀取的字元數為:" + c);
        System.out.println(chars[0]);
        System.out.println(chars[1]);
    }
}

執行之後,輸出結果如下所示:

這個時候我們已經能完成的讀取到一個字元了,當然,有時候為了優化,我們需要使用 BufferedReader 進行進一步的包裝:

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

這種方式雖然解決了讀取中文會亂碼問題,但是使用起來也不是很方便,所以一般讀取鍵盤輸入資訊我們都會採用 Scnner 來讀取。

Scanner 讀取

Scanner 實際上還是對 System.in 進行了封裝,並提供了一系列方法來讀取不同的字元型別,比如 nextIntnextFloat,以及 next 等。

public class Demo02Scnner {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextInt()){
            System.out.println(scanner.nextInt());
        }
    }
}

什麼是 IO 流

流是一種抽象概念,它代表了資料的無結構化傳輸(摘自百度百科)。IO 流對應的就是 InPutOutput,也就是輸入和輸出。輸入和輸出這個概念是針對於應用程式而言,比如當前程式中需要讀取檔案中的內容,那麼這就是輸入,而如果需要將應用程式本身的資料傳送到其他應用,就對應了輸出。

位元組流和字元流

根據流的處理方式又可以將流可以分為兩種型別:位元組流和字元流。

位元組流

位元組流讀取的基本單位為位元組,採用的是 ASCII 編碼,通常用來處理二進位制資料,其頂層抽象類為 InputStreamOutputStream,比如上面示例中的 System.in 實際上就是獲取到了一個 InputStream 類。

Java 中的流家族非常龐大,提供了非常多的具有不同功能的流,在實際應用中我們可以選擇不同的組合達到目的。

位元組輸入流

下圖為位元組輸入流家族關係示意圖:

從上圖可以看出這些結構非常清晰,首先是一個最頂層的介面,其次就是一些不同功能的基礎流,比如我們最常用的 FileInputStream 就是用來讀取檔案的,這其中有一個 FilterInputStream 流,這個流主要是用來擴充套件基礎流功能,其本身只是簡單的覆蓋了父類 InputStream 中的所有方法,並沒有做什麼特殊處理,真正的功能擴充套件需要依賴於其眾多的子類,比如最常用的 BufferedInputStream 提供了資料的緩衝,從而提升讀取流的效率,而 DataInputStream 是可以用來處理二進位制資料等等。

通過這些眾多不同功能的流來組合,可以靈活的讀取我們需要的資料。比如當我們需要讀取一個二進位制檔案,那麼就需要使用 DataInputStream,而 DataInputStream 本身不具備直接讀取檔案內容的功能,所以需要結合 FileInputStream

FileInputStream fin = new FileInputStream("E:\\test.txt");
DataInputStream din = new DataInputStream(fin);
System.out.println(din.readInt());

同時,如果我們想要使用緩衝機制,又可以進一步組裝 BufferedInputStream

FileInputStream fin = new FileInputStream("E:\\test.txt");
DataInputStream din = new DataInputStream(new BufferedInputStream(fin));
System.out.println(din.readInt());

還有一種流比較有意思,那就是 PushbackInputStream,這個流可以將讀出來的資料重新推回到流中:

public class Demo03 {
    public static void main(String[] args) throws IOException {
        FileInputStream fin = new FileInputStream("E:\\test.txt");//文件記憶體儲 abcd
        PushbackInputStream pin = new PushbackInputStream(new BufferedInputStream(fin));

        int a = pin.read();//讀取到a
        System.out.println(a);
        if (a != 'b'){
            pin.unread(a);//將 a 推回流中
        }
        System.out.println(pin.read());//再次讀取到 a
        System.out.println(pin.read());//讀取到 b
        System.out.println(pin.read());// 讀取到 c
    }
}

位元組輸出流

下圖為位元組輸出流家族關係示意圖:

這個結構和輸入流的結構基本類似,同樣的我們也可以通過組合來實現不同的輸出。

比如普通的輸出檔案,可以使用 FileOutputStream 流:

FileOutputStream fout = new FileOutputStream("E:\\test2.txt");
fout.write(1);
fout.write(2);

如果想要輸出二進位制格式,那麼就可以組合 DataOutputStream 流:

FileOutputStream fout = new FileOutputStream("E:\\test2.txt");
DataOutputStream dout = new DataOutputStream(fout);
dout.write(9);
dout.write(10);

緩衝流的原理

IO 操作是一個比較耗時的操作,而位元組流的 read 方法一次只能返回一個位元組,那麼當我們需要讀取多個位元組時就會出現每次讀取都要進行一次 IO 操作,而緩衝流內部定義了一個大小為 8192byte 陣列,當我們使用了緩衝流時,讀取資料的時候則會一次性最多讀取 8192 個位元組放到記憶體,然後一個個依次返回,這樣就大大減少了 IO 次數;同樣的,寫資料時,緩衝流會將資料先寫到記憶體,當我們寫完需要寫的資料時再一次性重新整理到指定位置,如磁碟等。

字元流

字元流讀取的基本單位為字元,採用的是 Unicode 編碼,其 read 方法返回的是一個 Unicode 碼元(0~65535)。

字元流通常用來處理文字資料,其頂層抽象類為 ReaderWrite,比如文中最開始的示例中的 InputStreamReader 就是繼承自 Reader 類。

字元輸入流

下圖為字元輸入流家族關係示意圖:

上圖可以看出,除頂層 Reader 類之外,字元流也提供了一些基本的字元流來處理文字資料,比如我們需要從文字讀取內容:

public class Demo05Reader {
    public static void main(String[] args) throws Exception {
        //位元組流
        FileInputStream fin = new FileInputStream("E:\\test.txt");//文字內容為“雙子孤狼”
        System.out.println(fin.read());//372
        //字元流
        InputStreamReader ir = new InputStreamReader(new FileInputStream("E:\\test.txt"));//文字內容為“雙子孤狼”
        System.out.println(ir.read());//21452
        char s = '雙';
        System.out.println((int)s);//21452
    }
}

輸出之後可以很明顯看出區別,位元組流一次讀入一個位元組,而字元流一次讀入一個字元。

當然,我們也可以採用自由組合的方式來更靈活的進行字元讀取,比如我們結合 BufferedReader 來讀取一整行資料:

public class Demo05Reader {
    public static void main(String[] args) throws Exception {
        InputStreamReader ir = new InputStreamReader(new FileInputStream("E:\\test.txt"));//文字內容為“雙子孤狼”
        BufferedReader br = new BufferedReader(ir);
        String s;
        while (null != (s = br.readLine())){
            System.out.println(s);//輸出雙子孤狼
        }
    }
}

字元輸出流

下圖為字元輸出流家族關係示意圖:

文字輸出,我們用的最多的就是 PrintWriter,這個類我想絕大部分朋友都使用過:

public class Demo06Writer {
    public static void main(String[] args) throws Exception{
        PrintWriter printWriter = new PrintWriter("E:\\test3.txt");
        printWriter.write("雙子孤狼");
        printWriter.flush();
    }
}

這裡和位元組流的區別就是寫完之後需要手動呼叫 flush 方法,否則資料就會丟失,並不會寫到檔案中。

為什麼字元流需要 flush,而位元組流不需要

位元組流不需要 flush 操作是因為位元組流直接操作的是位元組,中途不需要做任何轉換,所以直接就可以操作檔案,而字元流,說到底,其底層還是位元組流,但是字元流幫我們將位元組轉換成了字元,這個轉換需要依賴字元表,所以就需要在字元和位元組完成轉換之後通過 flush 操作刷到磁碟中。

需要注意的是,位元組輸出流最頂層類 OutputStream 中也提供了 flush 方法,但是它是一個空的方法,如果有子類有需要,也可以實現 flush 方法。

RandomAccessFile

RandomAccessFile 是一個隨機訪問檔案類,其可以在檔案中的任意位置查詢或者寫入資料。

public class Demo07RandomAccessFile {
    public static void main(String[] args) throws Exception {
        //文件內容為 lonely wolf
        RandomAccessFile inOut = new RandomAccessFile(new File("E:\\test.txt"),"rw");
        System.out.println("當前指標在:" + inOut.getFilePointer());//預設在0
        System.out.println((char) inOut.read());//讀到 l
        System.out.println("當前指標在:" + inOut.getFilePointer());
        inOut.seek(7L);//指標跳轉到7的位置
        System.out.println((char) inOut.read());//讀到 w
        inOut.seek(7);//跳回到 7
        inOut.write(new byte[]{'c','h','i','n','a'});//寫入 china,此時 wolf被覆蓋
        inOut.seek(7);//繼續跳回到 7
        System.out.println((char) inOut.read());//此時因為 wolf 被 china覆蓋,所以讀到 c
    }
}

根據上面的示例中的輸出結果,可以看到 RandomAccessFile 類可以隨機指定指標,並隨機進行讀寫,功能非常強大。

另外需要說明的是,構造 RandomAccessFile 時需要傳入一個模式,模式主要有 4 種:

  • r:只讀模式。此時呼叫任何 write 相關方法,會丟擲 IOException
  • rw:讀寫模式。支援讀寫,如果檔案不存在,則會建立。
  • rws:讀寫模式。每當進行寫操作,會將內容或者後設資料同步重新整理到磁碟。
  • rwd:讀寫模式。每當進行寫操作時,會將變動的內容用同步重新整理到磁碟。

總結

本文主要將 Java 中的 IO 流進行了梳理,通過將其分成位元組流和字元流,以及輸入流和輸出流分別統計,來建立一個對 JavaIO 流全域性的概念,最後通過一些例項來演示瞭如何通過不同型別的流來組合實現強大靈活的輸入和輸出,最後,介紹了同時支援輸入和輸出的 RandomAccessFile

相關文章