Try   HackMD

深入理解 Java IO 操作

Overview of Content

如有引用請標明出處

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

以下部分使 Jave7 新語法 try-with-resources,它會協助 IO 自動 Close

資料操作 - 概述

程式說白了,大部分的時間都在操作資料,只是這些資料可能存在不同地方,像是 Register, Memory, Disk 等等,而每個區域的資料都有不同特性

資料儲存地方的特性,並非本章要探討的,可以參考 另一篇文章 CPU、記憶體、快取分頁快取

資料

Register

Memory

Disk

流概念 / 設計

  • 在 Java 中,負責處理裡 I/O 的模塊分配在 java.iojava.nio Package 之下,並且會 將一組有序的資料序列稱為「流」

    java.nio 是 JDK 1.4 版本之後導入的新 I/O 庫,為了 IO 增高效率

    而流的輸出、輸出導向是站在應用的角度來觀察

    • 流入應用的數據代表 input

    • 流出應用的數據代表 output

      資料

      input

      鍵盤

      應用

      output

      控制台

      記憶體

      文件

    • 在 Java IO 流中,最常見的設計有兩種

      • 適配器 Adapter

      • 裝飾器 decorate

位元組流 / 字元流

  • 我們知道流是一種有序資料的傳遞方式,而其中 Java 又把流為兩個大類別,他們也代表了不同的資料流動方案

    流類型 流動的最小單位 特點 IO 庫關鍵字
    位元組流 Byte 沒有分檔案類型,皆是一系列的 Byte 數組 InputStream, OutputStream
    字元流 字元 字元,會依照「解碼」方式而有不同的切分方式 Reader, Writer

    Java

    位元組流

    字元流

InputStream / OutputStream 流 - 概述

  • InputStream 是一個抽象類,它底下分別有繼承幾個類,如下圖,之後會依序介紹一些常見的類、如何使用

    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

    其中 FilterInputStream 較為特別,它屬於一個裝飾類,用來加強某些 IO 類

    InputStream

    FileInputStream

    ByteArrayInputStream

    StringBufferInputStream

    PipedInputStream

    FilterInputStream

    ObjectInputStream

    BufferedInputStream

    DataInputStream

    LinenumberInputStream

    PushbackInputStream

  • OutputStream 也是一個抽象類,如下圖,之後會依序介紹一些常見的類、如何使用

    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

    其中 FilterOutputStream 較為特別,它屬於一個裝飾類,用來加強某些 IO 類

    OutputStream

    FileOutputStream

    ByteArrayOutputStream

    PipedOutputStream

    FilterOutputStream

    ObjectOutputStream

    BufferedOutputStream

    DataOutputStream

    PrintInputStream

ByteArrayInputStream / ByteArrayOutputStream 位元組流

  • ByteArrayInputStreamByteArrayOutputStream使用 Adapter 模式,組裝 byte[] 將其轉換為流,繼承、組合關係如下圖

    InputStream

    ByteArrayInputStream

    byte_array

    • ByteArrayInputStream 範例如下

      ​​​​​​​​class ByteArrayUsage { ​​​​​​​​ public static void main(String[] args) { ​​​​​​​​ byte[] source = new byte[] { 1, 2, 3, 4, 5 }; ​​​​​​​​ try(ByteArrayInputStream bais = new ByteArrayInputStream(source)) { ​​​​​​​​ int readData; ​​​​​​​​ // read 返回的是結果 ​​​​​​​​ // 如果返回 `-1` 則代表,沒有數據可讀 ​​​​​​​​ while ((readData = bais.read()) != -1) { ​​​​​​​​ System.out.println("Read Data: " + readData); ​​​​​​​​ } ​​​​​​​​ } catch (IOException e) { ​​​​​​​​ throw new RuntimeException(e); ​​​​​​​​ } ​​​​​​​​ } ​​​​​​​​}

    • ByteArrayOutputStream 範例如下

      ​​​​​​​​class ByteArrayUsage { ​​​​​​​​ public static void main(String[] args) { ​​​​​​​​ byte[] data; ​​​​​​​​ try(ByteArrayOutputStream bais = new ByteArrayOutputStream()) { ​​​​​​​​ // 以 UTF-8 來說,這代表了「安」字 ​​​​​​​​ bais.write(229); ​​​​​​​​ bais.write(174); ​​​​​​​​ bais.write(137); ​​​​​​​​ // 65 代表 ASCII 「A」 ​​​​​​​​ bais.write(65); ​​​​​​​​ data = bais.toByteArray(); ​​​​​​​​ } catch (IOException e) { ​​​​​​​​ throw new RuntimeException(e); ​​​​​​​​ } ​​​​​​​​ // 使用 UTF-8 解碼 ​​​​​​​​System.out.println("The data from byte array output stream(UTF-8): " + ​​​​​​​​ new String(data, StandardCharsets.UTF_8)); ​​​​​​​​ // 使用 ASCII 解碼 ​​​​​​​​System.out.println("The data from byte array output stream(ASCII): " + ​​​​​​​​ new String(data, StandardCharsets.US_ASCII)); ​​​​​​​​ } ​​​​​​​​}
      • 輸出得資料是以 Byte 為單位,所以如果資料轉為 Byte 後超過 255 就會換到下一個 Byte 輸出

FileInputStream 讀取 / FileOutputStream 輸出檔案資料

  • Byte 的方式,讀取、輸出檔案資料到到應用中(記憶體中)

    • FileInputStream 讀取範例

      原檔案資料如下(等等將要讀取的檔案)

      ​​​​​​​​ABC 安安
      • 補充知識,用 Shell 創建隨機檔案

        ​​​​​​​​​​​​mktemp -t InputSteamFile
      • 這裡要特別注意,由於檔案是 透過 Byte 讀取進應用的記憶體中,所以沒有辦法正確顯示超過 Byte 大小的字元,像是上述的「安」這個字就超過 Byte 大小,在使用 Byte 讀取時就會分開表示

        ​​​​​​​​​​​​class FileIOSteamUsage { ​​​​​​​​​​​​ public static void main(String[] args) { ​​​​​​​​​​​​ String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/InputSteamFile.hP28SECT"; ​​​​​​​​​​​​ try(FileInputStream fis = new FileInputStream(targetFile)) { ​​​​​​​​​​​​ int readData; ​​​​​​​​​​​​ while ((readData = fis.read()) != -1) { ​​​​​​​​​​​​ System.out.println("Read Data: " + readData); ​​​​​​​​​​​​ } ​​​​​​​​​​​​ } catch (IOException e) { ​​​​​​​​​​​​ throw new RuntimeException(e); ​​​​​​​​​​​​ } ​​​​​​​​​​​​ } ​​​​​​​​​​​​}
        1. "ABC "-> (65, 66, 67, 32)

        2. "安" -> (229, 174, 137):字符「安」的 UTF-8 表示形式由三個字節組成,分別是0xE5、0xB0 和 0x89,轉為 10 進位就是 (229, 174, 137)

        3. "安" -> (229, 174, 137)

        4. 最後的 10 代表的是換行

          • 如果要高效讀取檔案,就應該使用 read(byte[]) 讀取檔案,才不會平凡操作 IO

            ​​​​​​​​​​​​​​​​​​​​class FileInputSteamUsage { ​​​​​​​​​​​​​​​​​​​​ public static void main(String[] args) { ​​​​​​​​​​​​​​​​​​​​ String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/InputSteamFile.hP28SECT"; ​​​​​​​​​​​​​​​​​​​​ try(FileInputStream fis = new FileInputStream(targetFile)) { ​​​​​​​​​​​​​​​​​​​​ int len; ​​​​​​​​​​​​​​​​​​​​ byte[] cache = new byte[1024]; ​​​​​​​​​​​​​​​​​​​​ while ((len = fis.read(cache)) != -1) { ​​​​​​​​​​​​​​​​​​​​ System.out.println(new String(cache, 0, len)); ​​​​​​​​​​​​​​​​​​​​ } ​​​​​​​​​​​​​​​​​​​​ } catch (IOException e) { ​​​​​​​​​​​​​​​​​​​​ throw new RuntimeException(e); ​​​​​​​​​​​​​​​​​​​​ } ​​​​​​​​​​​​​​​​​​​​ } ​​​​​​​​​​​​​​​​​​​​}
    • FileOutputStream 輸出範例

      將資料以 Byte 為單位輸出到檔案中

      ​​​​​​​​class FileIOSteamUsage { ​​​​​​​​ public static void main(String[] args) { ​​​​​​​​ String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Apple.txt"; ​​​​​​​​ try(FileOutputStream fos = new FileOutputStream(targetFile)) { ​​​​​​​​ fos.write('A'); ​​​​​​​​ fos.write('P'); ​​​​​​​​ fos.write('P'); ​​​​​​​​ fos.write('L'); ​​​​​​​​ fos.write('E'); ​​​​​​​​ } catch (IOException e) { ​​​​​​​​ throw new RuntimeException(e); ​​​​​​​​ } ​​​​​​​​ } ​​​​​​​​}

被遺棄的 StringBufferInputStream

  • StringBufInputStreamUsage使用 Adapter 模式,組裝 String 將其轉換為流,繼承、組合關係如下圖

    InputStream

    StringBufInputStreamUsage

    String

    範例如下

    ​​​​class StringBufInputStreamUsage { ​​​​ public static void main(String[] args) { ​​​​ StringBufferInputStream sbis = new StringBufferInputStream("Hello 安"); ​​​​ int readData; ​​​​ while ((readData = sbis.read()) != -1) { ​​​​ System.out.println("Read Data: " + readData); ​​​​ } ​​​​ } ​​​​}
    • 被遺棄的原因:

      由於 它只使用字元編碼的低 8 位元,所以假如字元編碼超過 8 位元,就無法正常被讀取

    在上面範例我們知道「安」的組成是 (229, 174, 137) 才對,但以結果來看,它只讀取了低 8 位元,所以顯示錯誤(只顯示了最後的 137)!

多執行序的管道 - PipedInputStream / PipedOutputStream

  • 執行序除了可以使用 wait/notify 通訊,也可以使用 I/O 的方式通訊;它的概念就像不同 Thread 對 同一個 文件(管道)進行塞資料

    ThreadA_輸入

    管道_Piped

    ThreadB_讀取

    範例如下:

    1. 對 Piped 輸入資料方:使用 PipedOutputStream (想像成將資料輸出到應用以外)

      ​​​​​​​​static class Sender extends Thread { ​​​​​​​​ public PipedOutputStream pos; ​​​​​​​​ @Override ​​​​​​​​ public void run() { ​​​​​​​​ // Thread 啟動後,創建 PipedOutputStream ​​​​​​​​ try(PipedOutputStream pos = new PipedOutputStream()) { ​​​​​​​​ this.pos = pos; ​​​​​​​​ System.out.println("Sender write: " + i); ​​​​​​​​ for (int i = -3; i <= 3; i++) { ​​​​​​​​ System.out.println("Sender write: " + i); ​​​​​​​​ // 對其輸出資料 ​​​​​​​​ pos.write(i); ​​​​​​​​ Thread.sleep(1000); ​​​​​​​​ } ​​​​​​​​ } catch (IOException | InterruptedException e) { ​​​​​​​​ throw new RuntimeException(e); ​​​​​​​​ } ​​​​​​​​ } ​​​​​​​​}
    2. 讀取特定 Piped 方:使用 PipedInputStream(想像成將資料讀到應用中)

      ​​​​​​​​static class Receiver extends Thread { ​​​​​​​​ private final Sender sender; ​​​​​​​​ Receiver(Sender sender) { ​​​​​​​​ this.sender = sender; ​​​​​​​​ } ​​​​​​​​ @Override ​​​​​​​​ public void run() { ​​​​​​​​ try(PipedInputStream pis = new PipedInputStream(sender.pos)) { ​​​​​​​​ System.out.println("Receiver running."); ​​​​​​​​ ​​​​​​​​ int data; ​​​​​​​​ // 讀取管道中的流 ​​​​​​​​ while ((data = pis.read()) != -1) { ​​​​​​​​ System.out.println("Receiver get: " + data); ​​​​​​​​ } ​​​​​​​​ } catch (IOException e) { ​​​​​​​​ throw new RuntimeException(e); ​​​​​​​​ } ​​​​​​​​ } ​​​​​​​​}
    • 使用範例:

      這裡測試時特別將發送、接收方得時間錯開(使用 Thread#sleep 不同時間),用來證明 Piped 中是可以儲存資料的

      ​​​​​​​​class PipedInputStreamUsage { ​​​​​​​​ public static void main(String[] args) { ​​​​​​​​ Sender sender = new Sender(); ​​​​​​​​ Receiver receiver = new Receiver(sender); ​​​​​​​​ sender.start(); ​​​​​​​​ receiver.start(); ​​​​​​​​ } ​​​​​​​​}

流的串連 - SequenceInputStream

  • 內部是使用 Vector 結構來串連兩個 Stream

    Vector 是有安全保護的 ArrayList,但又由於它的同步安全機制導致,效能較差

    StreamA

    SequenceInputStream

    StreamB

    順序輸出_Stream_A_B

    使用範例

    ​​​​class SequenceInputStreamUsage { ​​​​ public static void main(String[] args) throws IOException { ​​​​ byte[] sourceA = new byte[] { 1, 2, 3, 4, 5 }; ​​​​ byte[] sourceB = new byte[] { -1, -2, -3, -4, -5 }; ​​​​ ByteArrayInputStream baisA = new ByteArrayInputStream(sourceA); ​​​​ ByteArrayInputStream baisB = new ByteArrayInputStream(sourceB); ​​​​ SequenceInputStream sis = new SequenceInputStream(baisA, baisB); ​​​​ int readData; ​​​​ while ((readData = sis.read()) != -1) { ​​​​ System.out.println("Read Data: " + readData); ​​​​ } ​​​​ } ​​​​}

裝飾類型的 FilterInputStream / FilterOutputStream 概述

  • 設計模式中的「裝飾模式」特點在於 對原先類型的加強、減弱,而在 IO 中使用,則是 基於基礎的 ByteStream 用來體現(加強)各個 File 不同的讀寫方式

    這有利於提高程式碼的重用性

    InputStream

    FilterInputStream

    InputStream_準備被加強的類

    OutputStream

    FilterOutputStream

    OutputStream_準備被加強的類

DataInputStream 讀取 / DataOutputStream 寫入基本類型 & UTF-8

  • DataInputStream 實作 DataInput 介面,用於 讀取基礎資料類型(int, float, long, double 等等);

    另外它還提供兩個特別方法: 1. 能讀取 UTF-8 編碼的 DataInputStream#readUTF 方法2. 能寫入 UTF-8 編碼的 DataOutputStream#writeUTF 方法

    • UTF-8Unicode 編碼

      • Unicode 是 Java 大多數預設的編碼方式,而 UTF-8 則是它的變體(特點是它支援作業系統)

      • Unicode 預設佔用 2 Byte 空間,有時候會浪費空間,而 UTF-8 則會針對不同字元進行編碼,給予適合的空間

        eg. UTF-8 對於 ASCII 就採用 1 Byte 空間,而非 ASCII 則給予 2 Byte 空間

    1. DataOutputStream 範例:

      輸出一般資料類型、UTF8 資料類型

      ​​​​​​​​public static void main(String[] args) { ​​​​​​​​ String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello.txt"; ​​​​​​​​ try(FileOutputStream fos = new FileOutputStream(targetFile); ​​​​​​​​ DataOutputStream dos = new DataOutputStream(fos)) { ​​​​​​​​ dos.writeChar('A'); ​​​​​​​​ dos.writeChar('B'); ​​​​​​​​ dos.writeChar('C'); ​​​​​​​​ dos.writeChar(' '); ​​​​​​​​ // 使用 UTF8 輸出資料 ​​​​​​​​ dos.writeUTF("安安"); ​​​​​​​​ } catch (IOException e) { ​​​​​​​​ throw new RuntimeException(e); ​​​​​​​​ } ​​​​​​​​}
    2. DataOutputStream 範例:

      按照順序讀取相同檔案

      ​​​​​​​​public static void main(String[] args) { ​​​​​​​​ String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello.txt"; ​​​​​​​​ try(FileInputStream fis = new FileInputStream(targetFile); ​​​​​​​​ DataInputStream dis = new DataInputStream(fis)) { ​​​​​​​​ System.out.println("Read Data: " + dis.readChar()); ​​​​​​​​ System.out.println("Read Data: " + dis.readChar()); ​​​​​​​​ System.out.println("Read Data: " + dis.readChar()); ​​​​​​​​ System.out.println("Read Data: " + dis.readChar()); ​​​​​​​​ // 使用 UTF8 讀取資料 ​​​​​​​​ System.out.println("Read Data: " + dis.readUTF()); ​​​​​​​​ } catch (IOException e) { ​​​​​​​​ throw new RuntimeException(e); ​​​​​​​​ } ​​​​​​​​}

      • 如果不按照順序讀取,或是使用錯誤方式讀取會拋出 EOFException 異常

BufferedInputStream / BufferedOutputStream 減少密集 IO

  • 使用 BufferedInputStream / BufferedOutputStream 這兩個類都可以有效的減少密集讀寫 IO,可以 加快 IO 效率,提升 IO 品質

    以也可依照需求,在建構函數時傳入緩衝區大小的設定

    使用上述範例進行修改

    ​​​​class BufferedIOStreamUsage { ​​​​ public static void main(String[] args) { ​​​​ String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello.txt"; ​​​​ try(FileOutputStream fos = new FileOutputStream(targetFile); ​​​​ // 新增 ​​​​ BufferedOutputStream bos = new BufferedOutputStream(fos); ​​​​ DataOutputStream dos = new DataOutputStream(bos)) { ​​​​ dos.writeChar('A'); ​​​​ dos.writeChar('B'); ​​​​ dos.writeChar('C'); ​​​​ dos.writeChar(' '); ​​​​ dos.writeUTF("安安"); ​​​​ } catch (IOException e) { ​​​​ throw new RuntimeException(e); ​​​​ } ​​​​ try(FileInputStream fis = new FileInputStream(targetFile); ​​​​ // 新增 ​​​​ BufferedInputStream bis = new BufferedInputStream(fis); ​​​​ DataInputStream dis = new DataInputStream(bis)) { ​​​​ System.out.println("Read Data: " + dis.readChar()); ​​​​ System.out.println("Read Data: " + dis.readChar()); ​​​​ System.out.println("Read Data: " + dis.readChar()); ​​​​ System.out.println("Read Data: " + dis.readChar()); ​​​​ System.out.println("Read Data: " + dis.readUTF()); ​​​​ } catch (IOException e) { ​​​​ throw new RuntimeException(e); ​​​​ } ​​​​ } ​​​​}

PrintStream 格式化輸出

  • PrintStream 是輸出流(與 DataOutputStream 一樣),可以輸出格式化的資料;PrintStream 特點如下

    • PrintStream & DataOutputStream 差異?

      1. PrintStream 不會拋出異常

        須透過 PrintStream#checkError 函數來檢查判斷是否輸出成功

      2. 緩衝輸出的控制

        PrintStream 可以透過設定來自動輸出,而輸出時機如下表

        輸出的時機
        PrintStream 完整輸出一個 byte[]、換行字元 \n(或是 println
        DataOutputStream Buffer 滿、呼叫 flush 強制刷新

      3. 輸出編碼差異

        PrintStream#println 等同於 DataOutputStream#writeUTF,但兩者 對於資料的編碼方式不同

        方法 編碼方式
        PrintStream#println 本地作業系統預設的編碼(有可能是 BIG5, UTF-8, GBK 等等)
        DataOutputStream#writeUTF 預設採用 UTF-8
        ​​​​​​​​​​​​class PrintStreamUsage { ​​​​​​​​​​​​ public static void main(String[] args) throws IOException { ​​​​​​​​​​​​ usePrintStream(); ​​​​​​​​​​​​ System.out.println(); ​​​​​​​​​​​​ useDataOutputSteam(); ​​​​​​​​​​​​ } ​​​​​​​​​​​​ static void readBuff(byte[] bytes) { ​​​​​​​​​​​​ ByteArrayInputStream bais = new ByteArrayInputStream(bytes); ​​​​​​​​​​​​ int data; ​​​​​​​​​​​​ while ((data = bais.read()) != -1) { ​​​​​​​​​​​​ System.out.println("Data: " + data); ​​​​​​​​​​​​ } ​​​​​​​​​​​​ } ​​​​​​​​​​​​ static void usePrintStream() { ​​​​​​​​​​​​ System.out.println("Use PrintStream ---------"); ​​​​​​​​​​​​ ByteArrayOutputStream baos = new ByteArrayOutputStream(); ​​​​​​​​​​​​ // PrintStream 包裝 ByteArray ​​​​​​​​​​​​ PrintStream ps = new PrintStream(baos, true); ​​​​​​​​​​​​ // 對 PrintStream 寫入「安」,其實最終還是填充到 ByteArray 中 ​​​​​​​​​​​​ ps.print("安"); ​​​​​​​​​​​​ readBuff(baos.toByteArray()); ​​​​​​​​​​​​ } ​​​​​​​​​​​​ static void useDataOutputSteam() throws IOException { ​​​​​​​​​​​​ System.out.println("Use DataOutputStream ---------"); ​​​​​​​​​​​​ ByteArrayOutputStream baos = new ByteArrayOutputStream(); ​​​​​​​​​​​​ // DataOutputStream 包裝 ByteArray ​​​​​​​​​​​​ DataOutputStream ps = new DataOutputStream(baos); ​​​​​​​​​​​​ // 對 DataOutputStream 寫入「安」,其實最終還是填充到 ByteArray 中 ​​​​​​​​​​​​ ps.writeUTF("安"); ​​​​​​​​​​​​ readBuff(baos.toByteArray()); ​​​​​​​​​​​​ } ​​​​​​​​​​​​}

        從結果可以看出,我目前使用的作業系統 (PrintStream) 就是 UTF-8 (與 DataOutputStream 差不多)

Reader / Writer 概述

在程式中作資料輸出較為單純,一般來說只需要輸出 Byte 即可,但是當 在應用場合中 Java 需要注意每個文字檔中的編碼方式,如果使用不對的方式編碼

  1. 文字編輯器無法正常閱讀 Java 程式輸出的資料

    無法正常訪問

    Java_app

    輸出_UTF8

    文件

    文字編輯器

    使用_BIG5_閱讀

  2. Java 程式不知道如何正確的解碼程式

    無法正常訪問

    文字編輯器

    輸出_BIG5

    文件

    Java_app

    使用_UTF8_閱讀

自動、指定編碼

  • String#getBytes 方法,預設(不帶參數的狀況下)是使用「本地作業系統編碼」… 而本地作業系統的編碼就可能有多種

    可能有 UTF-8, UTF-16, GBK, BIG5 等等

    • Java 大多預設儲存的編碼方式是 Unicode

    使用 Java 查看本地作業系統編碼的方式如下

    ​​​​class LocalSystemEncode { ​​​​ public static void main(String[] args) { ​​​​ // 方法一 ​​​​ String localEncode = System.getProperty("file.encoding"); ​​​​ System.out.println("Get by property: " + localEncode); ​​​​ // 方法二 ​​​​ Charset cs = Charset.defaultCharset(); ​​​​ System.out.println("Get by Charset: " + cs); ​​​​ } ​​​​}

    Charsetjava.nio.charset 套件中的類

  • 而這跟 Reader / Writer 有什麼關係呢?

    使用 Java 的 Reader / Writer 類,它會 自動(也可以指定)幫我們切換「程式編碼」與「目標文件」之間的關係(互相轉換)

    Java IO Writer

    Java IO Reader

    目標文件_編碼_BIG5

    程式編碼_假設為_UTF-8

    • 當然,你也可以對 Java IO 的 Writer / Reader 指定要轉換的編碼

    • 由於 JVM 統一採用平台無關編碼,所以也是為何 Java 應用可以跨平台執行的原因(應為面對個平台編碼的責任推到了 JVM 負責)

      Java_應用

      JVM

      Window

      Linux

      Unix

      Mac

Reader / Writer 類別概述

  • Reader 與 InputStream 的差異,從層級結構之下來看有一定的差異InputStream 可以清楚地知道所有 FilterInputStrem 的子類都用來裝飾,而 Reader 則不是

    Reader

    CharArrayReader

    BufferedReader

    StringReader

    PipedReader

    FilterReader

    InputStreamReader

    LineNumberReader

    PushBackReader

    FileReader

  • Writer 與 OutputStream 層級結構較為相似(但仍有差異)

    Writer

    CharArrayWriter

    BufferedWriter

    StringWriter

    PipedWriter

    FilterWriter

    OutputStreamWriter

    FileWriter

CharArrayReader / CharArrayWriter 讀取字元

讀取的是「字符」(一至多個 Byte),不是「字元」(一個 Byte)

  • CharArrayReaderCharArrayWriter 類別是使用 Adapter 模式,將 字元陣列類型轉換為 Reader、Wirter 類型

    Reader

    CharArrayReader

    char_array

    Writer

    CharArrayWriter

    char_array_

    使用範例如下

    • CharArrayReader 類使用

      ​​​​​​​​static void readerUsage() throws IOException { ​​​​​​​​ char[] charArray = new char[] { 'H', 'i', '~', '安', '安'}; ​​​​​​​​ // 正確讀取字符 ​​​​​​​​ CharArrayReader car = new CharArrayReader(charArray); ​​​​​​​​ int data; ​​​​​​​​ while ((data = car.read()) != -1) { ​​​​​​​​ System.out.println("Get data: " + data); ​​​​​​​​ } ​​​​​​​​}

      我們可以看到,中文字確實就超過一個 Byte 的大小

    • CharArrayWriter 類使用

      ​​​​​​​​static void writerUsage() throws IOException { ​​​​​​​​ CharArrayWriter caw = new CharArrayWriter(); ​​​​​​​​ // 正確寫入字符 ​​​​​​​​ caw.write("ABC"); ​​​​​​​​ caw.write("你好"); ​​​​​​​​ for (char c : caw.toCharArray()) { ​​​​​​​​ System.out.println("char value:" + c); ​​​​​​​​ } ​​​​​​​​}

StringReader 讀取本地編碼字串

  • StringReader 可以 解決被遺棄的 StringBufferInputStream 類問題,它會完整讀取 String 字串完整字

    Reader

    StringReader

    String

    使用範例

    ​​​​public static void main(String[] args) throws IOException { ​​​​ // 儲存在記憶體的是 Unicode ​​​​ StringReader stringReader = new StringReader("ABC 安安"); ​​​​ int data; ​​​​ while ((data = stringReader.read()) != -1) { ​​​​ // 透過 StringReader 讀取 ​​​​ // 轉換為 Unicode(因為都是在記憶體中,所以不用轉換) ​​​​ System.out.println("Get data: " + data); ​​​​ } ​​​​}

InputStreamReader / OutputStreamWriter 可指定編碼

  • InputStreamReader 也是使用 Adapter 模式,它可以用來 一次性讀取、輸出本地編碼(預設)的數據,而不會只讀取一個 Byte 數據

    Reader

    InputStreamReader

    InputStream

    Writer

    OutputStreamWriter

    OutputStream

    • 並且 InputStreamReaderOutputStreamWriter 操作可以「指定編碼

      編碼有像是: GBKUTF-8UTF-16UTF-32ISO-88591Big 5 等等相當的多~

      NotePad++ 的可用編碼

    使用範例如下

    • InputStreamReader 使用

      ​​​​​​​​public static void main(String[] args) throws IOException { ​​​​​​​​ String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello.txt"; ​​​​​​​​ FileInputStream fis = new FileInputStream(targetFile); ​​​​​​​​ InputStreamReader isr = new InputStreamReader(fis); ​​​​​​​​ int data; ​​​​​​​​ while ((data = isr.read()) != -1) { ​​​​​​​​ System.out.println("Get data: " + data); ​​​​​​​​ } ​​​​​​​​}

      Reader 會依照個平台的編碼方式進行讀取,所以中文字「安安」可以正常作為一個字元輸出(23433, 當前是 UTF-8 編碼)

      • 如果單純使用 FileInputStream 讀取,會被一次只能讀取一個 Byte 限制,導致中文字「安安」會分開來輸出

        229174137, 當前是 UTF-8 編碼)

        ​​​​​​​​​​​​public static void main(String[] args) throws IOException { ​​​​​​​​​​​​ String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello.txt"; ​​​​​​​​​​​​ FileInputStream bais = new FileInputStream(targetFile); ​​​​​​​​​​​​ int data; ​​​​​​​​​​​​ while ((data = bais.read()) != -1) { ​​​​​​​​​​​​ System.out.println("Get data: " + data); ​​​​​​​​​​​​ } ​​​​​​​​​​​​}

    1. Writer 使用

      ​​​​​​​​static void outputStreamWriterUsage() throws IOException { ​​​​​​​​ String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello2.txt"; ​​​​​​​​ FileOutputStream fos = new FileOutputStream(targetFile); ​​​​​​​​ OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_16); ​​​​​​​​ osw.write("Apple 早"); ​​​​​​​​ osw.flush(); ​​​​​​​​ FileInputStream fis = new FileInputStream(targetFile); ​​​​​​​​ InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_16); ​​​​​​​​ int data; ​​​​​​​​ while ((data = isr.read()) != -1) { ​​​​​​​​ System.out.println("Get data: " + (char) data); ​​​​​​​​ } ​​​​​​​​}

      • 假設這邊將寫入、讀取的編碼方式修改為不同的編碼(寫 UTF-16, 讀 UTF-8),就會出現讀取的亂碼(但事實上,數據並沒有錯誤)

自動轉本地編碼 FileReader / FileWriter

  • FileReaderInputStreamReader 的子類 (FileWriterOutputStreamWriter 的子類),它們的特點在於 自動轉換檔案內容為「本地編碼」,不能指定 其他字元編碼類型

    使用方式如下 (其實就是簡化,組裝了 )

    ​​​​public static void main(String[] args) throws IOException { ​​​​ String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello.txt"; ​​​​ FileReader fr = new FileReader(targetFile); ​​​​ int data; ​​​​ while ((data = fr.read()) != -1) { ​​​​ System.out.println("Get data: " + data); ​​​​ } ​​​​}

BufferedReader / BufferedWriter 減少密集 IO

  • 這兩個類都帶有 緩衝區,資料會先輸出到緩衝區,當緩衝區滿了之後,才會將「字元」輸出到目標裝置中,這樣可以有效避免密集 IO 帶來的低效率操作

    ​​​​class IOBufferedUsage { ​​​​ public static void main(String[] args) throws IOException { ​​​​ String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello3.txt"; ​​​​ FileWriter fw = new FileWriter(targetFile); ​​​​ BufferedWriter bw = new BufferedWriter(fw); ​​​​ bw.write("Yo man, what up!"); ​​​​ bw.flush(); ​​​​ FileReader fr = new FileReader(targetFile); ​​​​ BufferedReader br= new BufferedReader(fr); ​​​​ int data; ​​​​ while ((data = br.read()) != -1) { ​​​​ System.out.println("Get data: " + (char) data); ​​​​ } ​​​​ } ​​​​}

PrintWriter

  • PrintWriter 的建構函數中,可以接收(裝飾)Writer、OutputStream 兩個類

    • PrintWriter & PrintStream 差異

      兩者的差別在 PrintWriter 可以指定特殊編碼,而 PrintStream 只能用於本地編碼

IO 操作實戰:常用的操作

讀 & 寫 - 複製檔案

  • 邊讀邊寫就可以達到 複製檔案 的效果

    try-with-resources 可以管理多個資源,不同資源中間使用 ; 分開即可

    ​​​​ public static void readWrite() { ​​​​ int len; ​​​​ try(FileInputStream fis = new FileInputStream("D:\\JavaIO\\android_3rd_lib.aar"); ​​​​ FileOutputStream fos = new FileOutputStream("D:\\JavaIO\\android_3rd_lib_copy.aar")) { ​​​​ byte[] cacheBuf = new byte[1024]; ​​​​ while ((len = fis.read(cacheBuf)) != -1) { ​​​​ // 讀取檔案 ​​​​ System.out.println(len); ​​​​ // 寫入檔案 ​​​​ fos.write(cacheBuf); ​​​​ fos.flush(); // 真正寫出 ​​​​ } ​​​​ System.out.println("複製成功"); ​​​​ } catch (IOException e) { ​​​​ e.printStackTrace(); ​​​​ } ​​​​ }

    實作結果

    • 若是 轉為 String 透過 string#getBytes() 再寫出則會造成 copy 出來的檔案大小錯誤,這部分 可能是 getBytes() 讀取的是本地編碼 造成的

      因為你的檔案可能並不是 本地編碼輸出 的檔案

      ​​​​​​​​ public static void readWrite() { ​​​​​​​​ int len; ​​​​​​​​ try(FileInputStream fis = new FileInputStream("D:\\JavaIO\\android_3rd_lib.aar"); ​​​​​​​​ FileOutputStream fos = new FileOutputStream("D:\\JavaIO\\android_3rd_lib_copy.aar")) { ​​​​​​​​ byte[] cacheBuf = new byte[1024]; ​​​​​​​​ while ((len = fis.read(cacheBuf)) != -1) { ​​​​​​​​ // 將檔案資訊轉為 String(不太好!) ​​​​​​​​ String str = new String(cacheBuf, 0, len); ​​​​​​​​ System.out.println(len); ​​​​​​​​ // 寫入檔案(出問題的點) ​​​​​​​​ fos.write(str.getBytes()); ​​​​​​​​ fos.flush(); // 真正寫出 ​​​​​​​​ } ​​​​​​​​ System.out.println("複製成功"); ​​​​​​​​ } catch (IOException e) { ​​​​​​​​ e.printStackTrace(); ​​​​​​​​ } ​​​​​​​​ }

      reference link

IO 有無 Buffered 差異測試

  • 有緩衝的 字節流 IO,這樣對於硬碟較不傷,並且可以 加快速度,以下讀取 android_3rd_lib.aar,一個使用 Buffer 來讀取,另一個則是使用一般的讀取,來查看這兩個對象所使用的時間

    以下以讀取並使用有 Buffer、無 Buffer 作為比腳

    ​​​​public class JavaStreamDemo_3 { ​​​​ public static void main(String[] str) { ​​​​ readFile(); ​​​​ readFileByBuffer(); ​​​​ } ​​​​ // 無 Buffer ​​​​ public static void readFile() { ​​​​ long start = System.currentTimeMillis(); ​​​​ try(FileInputStream fis = new FileInputStream("android_3rd_lib.aar")) { ​​​​ // 讀取後的操作相同 ​​​​ byte[] cacheBuf = new byte[1024]; ​​​​ int len; ​​​​ while ((len = fis.read(cacheBuf)) != -1) { ​​​​ String str = new String(cacheBuf, 0, len); ​​​​ } ​​​​ } catch (IOException e) { ​​​​ e.printStackTrace(); ​​​​ } ​​​​ System.out.println("使用時間: " + (System.currentTimeMillis() - start)); ​​​​ } ​​​​ // 有 Buffer ​​​​ public static void readFileByBuffer() { ​​​​ long start = System.currentTimeMillis(); ​​​​ try(BufferedInputStream bis = new BufferedInputStream(new FileInputStream("android_3rd_lib.aar"))) { ​​​​ // 讀取後的操作相同 ​​​​ byte[] cacheBuf = new byte[1024]; ​​​​ int len; ​​​​ while ((len = bis.read(cacheBuf)) != -1) { ​​​​ String str = new String(cacheBuf, 0, len); ​​​​ } ​​​​ } catch (IOException e) { ​​​​ e.printStackTrace(); ​​​​ } ​​​​ System.out.println("使用時間: " + (System.currentTimeMillis() - start)); ​​​​ } ​​​​}

    實測結果

    這邊特別展示時間的縮短,BufferOutputStream 就是相同的道理這裡就不贅述了

RandomAccessFile - 隨機存取檔案

  • RandomAccessFile 也是一個相對重要的類,以往我們讀取、寫入檔案時,必須按照 IO 順序來存取,而 RandomAccessFile 則可以透過指標(Pointer)移動來操作檔案

    其中,建構函數中有一個 Mode 參數,其常見設置如下

    Mode 設置 說明
    r 唯讀模式
    rw 讀寫模式(多線程不安全)
    rws 「讀、寫」都同步,效能較低,不果可以保證資料的完整性
    rwd 延遲讀寫;只有「寫」是同步的,也就是說在讀取數據上可能會發生不及時的狀況(可以用在下載)
    • Mode 不可以單獨設置為「w」,否則會拋出異常

  • RandomAccessFile 實作 DataInput、DataOutput 界面,所以可以做格式化讀取、寫入(本地編碼);

    範例如下

    ​​​​public static void main(String[] args) { ​​​​ String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello4.txt"; ​​​​ try(RandomAccessFile rsf = new RandomAccessFile(targetFile, "rw")) { ​​​​ for (int i = 0; i < 5; i++) { ​​​​ String writeTarget = i + ",嗨"; ​​​​ System.out.println("Before write ptr: " + rsf.getFilePointer()); ​​​​ rsf.writeUTF(writeTarget); ​​​​ System.out.println("After write ptr: " + rsf.getFilePointer()); ​​​​ // 調整 Ptr 位置 ​​​​ rsf.seek(rsf.getFilePointer() + "\t".getBytes().length); ​​​​ System.out.println("---- : " + rsf.getFilePointer() + "\n"); ​​​​ } ​​​​ // 移動 Ptr 到開頭,準備重新讀取 ​​​​ rsf.seek(0); ​​​​ for (int i = 0; i < 5; i++) { ​​​​ String readStr = rsf.readUTF(); ​​​​ System.out.println(readStr); ​​​​ // 移動 Ptr,跳過 "\t" ​​​​ // 沒跳過的話,讀取會出錯 ​​​​ rsf.skipBytes("\t".getBytes().length); ​​​​ } ​​​​ } catch (IOException e) { ​​​​ throw new RuntimeException(e); ​​​​ } ​​​​}

Object 物件 - 序列化 IO

在需要的時候可以把對象,輸出到外部檔案,並在需要的時候重讀回來

import java.io.Serializable; public class AccountInfo implements Serializable { public final String name; public final long id; public final String password; public AccountInfo(String name, long id, String password) { this.name = name; this.id = id; this.password = password; } @Override public String toString() { return "Name: " + name + "\n" + "id: " + id + "\n" + "password: " + password; } }
  • 要輸出的物件 必須要實作 Serializable,這個界面沒有方法,所以又稱為「標示界面」,若是沒有這個界面則會錯誤

ObjectOutputStream / ObjectInputStream 物件序列化、反序列化

  • 使用範例

    ​​​​public class ObjectStreamDemo { ​​​​ public static void main(String[] str) { ​​​​ writeObj(); ​​​​ readObj(); ​​​​ } ​​​​ public static void writeObj() { ​​​​ AccountInfo accountInfo = new AccountInfo("Alien", 9527, "HelloWorld"); ​​​​ try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Account"))){ ​​​​ oos.writeObject(accountInfo); ​​​​ oos.flush(); ​​​​ System.out.println("寫入對象成功: " + accountInfo.hashCode()); ​​​​ } catch (IOException e) { ​​​​ e.printStackTrace(); ​​​​ } ​​​​ } ​​​​ private static void readObj() { ​​​​ try (ObjectInputStream oos = new ObjectInputStream(new FileInputStream("Account"))){ ​​​​ AccountInfo accountInfo = (AccountInfo) oos.readObject(); ​​​​ System.out.println("讀取對象成功: " + accountInfo.toString() + ", " + accountInfo.hashCode()); ​​​​ } catch (IOException | ClassNotFoundException e) { ​​​​ e.printStackTrace(); ​​​​ } ​​​​ } ​​​​}
    • 注意點:

      • Java IO 輸出的物件資料只有 ObjectInputStream 看得懂,普通的文字編輯器打開後只會看到亂碼

      • 物件雖然可以在 JVM 中重新被建立,但是 物件的記憶體位置已經不同

        觀察 hashcode

transient 忽略 IO

  • 從上面輸出的檔案中可以看到密碼,這並不是我們想要的結果,所以必須在序列化時隱藏,這時需要改動的是要序列化的檔案 (AccountInfo.java),並使用關鍵字 transient (短暫的) 描述需要隱藏的參數

    ​​​​public class AccountInfo implements Serializable { ​​​​ public final String name; ​​​​ public final long id; ​​​​ public transient final String password; ​​​​ ... 省略部分 ​​​​}

    這時再重新 Run 依次程式 password 輸出結果就是 null

    實作結果

寫入多個物件 - 技巧

  • 寫入多個對象時通常會出問題,這個時候就 要使用 ArrayList 把多個物件存起來,這樣才能保證序列化 & 反序列化不會出問題

    ​​​​public class ObjectStreamDemo { ​​​​ public static void main(String[] str) { ​​​​ writeMulObj(); ​​​​ readMulObj(); ​​​​ } ​​​​ public static void writeMulObj() { ​​​​ AccountInfo accountInfo = new AccountInfo("Alien", 9527, "HelloWorld1"); ​​​​ AccountInfo accountInfo2 = new AccountInfo("Apple", 1111, "HelloWorld2"); ​​​​ AccountInfo accountInfo4 = new AccountInfo("Banana", 2222, "HelloWorld3"); ​​​​ List<AccountInfo> list = new ArrayList<>(); ​​​​ list.add(accountInfo); ​​​​ list.add(accountInfo2); ​​​​ list.add(accountInfo4); ​​​​ try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Account"))){ ​​​​ oos.writeObject(list); ​​​​ oos.flush(); ​​​​ System.out.println("寫入對象成功"); ​​​​ } catch (IOException e) { ​​​​ e.printStackTrace(); ​​​​ } ​​​​ } ​​​​ private static void readMulObj() { ​​​​ try (ObjectInputStream oos = new ObjectInputStream(new FileInputStream("Account"))){ ​​​​ List<AccountInfo> list = (List<AccountInfo>) oos.readObject(); ​​​​ for(AccountInfo info : list) { ​​​​ System.out.println("讀取對象成功: " + info.toString()); ​​​​ } ​​​​ } catch (IOException | ClassNotFoundException e) { ​​​​ e.printStackTrace(); ​​​​ } ​​​​ } ​​​​}

    實作結果

隱藏方法 - readObject、writerObject

  • readObjectwriterObject 是序列化、反序列化物件的過程中的隱藏方法,可以在這裡客製化自己需要的序列方案

    這要兩方法是藏在你要序列化的物件內,於是做以下修改

    在序列化、反序列化時密碼成員 由自己手動寫入、讀取!

    ​​​​class AccountInfo implements Serializable { ​​​​ public final String name; ​​​​ public final long id; ​​​​ // 仍保持不自動序列化 ​​​​ public transient String password; ​​​​ private void changeBytes(byte[] bytes) { ​​​​ for (int i = 0; i < bytes.length; i++) { ​​​​ bytes[i] = (byte) (~bytes[i]); ​​​​ } ​​​​ } ​​​​ @Serial ​​​​ private void writeObject(ObjectOutputStream stream) throws IOException { ​​​​ // 先使用預設寫物件 ​​​​ stream.defaultWriteObject(); ​​​​ // 手動序列化 ​​​​ byte[] original = password.getBytes(); ​​​​ // 反向 Password 再寫入 ​​​​ changeBytes(original); ​​​​ // 手動寫物件 ​​​​ stream.writeObject(original); ​​​​ } ​​​​ @Serial ​​​​ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { ​​​​ // 先使用預設讀物件 ​​​​ stream.defaultReadObject(); ​​​​ // 手動反序列化(讀物件) ​​​​ byte[] readBytes = (byte[]) stream.readObject(); ​​​​ // 反向 Bytes ​​​​ changeBytes(readBytes); ​​​​ // 手動建立 Password ​​​​ password = new String(readBytes); ​​​​ } ​​​​ ... 省略部分 ​​​​}

    讀取、寫入物件的 Demo 如下(沒變)

    ​​​​class ObjectStreamHideMethod { ​​​​ public static void main(String[] str) { ​​​​ writeObj(); ​​​​ readObj(); ​​​​ } ​​​​ public static void writeObj() { ​​​​ AccountInfo accountInfo = new AccountInfo("Alien", 9527, "HelloWorld"); ​​​​ try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Account"))){ ​​​​ oos.writeObject(accountInfo); ​​​​ oos.flush(); ​​​​ System.out.println("寫入對象成功: " + accountInfo.hashCode()); ​​​​ } catch (IOException e) { ​​​​ e.printStackTrace(); ​​​​ } ​​​​ } ​​​​ private static void readObj() { ​​​​ try (ObjectInputStream oos = new ObjectInputStream(new FileInputStream("Account"))){ ​​​​ AccountInfo accountInfo = (AccountInfo) oos.readObject(); ​​​​ System.out.println("讀取對象成功: " + accountInfo.toString() + ", " + accountInfo.hashCode()); ​​​​ } catch (IOException | ClassNotFoundException e) { ​​​​ e.printStackTrace(); ​​​​ } ​​​​ } ​​​​}

    • 其中的 readObjectwriterObject 的名稱、參數必須完全相同

    • 為何不把這兩個方法加入到「Serializable」 界面? 這有關到,介面的特點

      • 由於界面中的方法 必須全部公開

        而這兩個方法實際上是希望被隱藏的!

      • 界面中的方法會 強迫使用界面者一定要全部實現

        但這兩個方法並非每個序列化成員都會使用到,這就不符合界面隔離原則

        • 藉此,用戶只能靠讀取 JavaDoc、額外文件 來了解序列化、反序列化的所有協定

更多的 Java 語言相關文章

Java 語言深入

Java IO 相關文章

深入 Java 物件導向

Appendix & FAQ

tags: Java 基礎