--- title: '深入理解 Java IO 操作' disqus: kyleAlien --- 深入理解 Java IO 操作 === ## Overview of Content 如有引用請標明出處 :smile: > 以下部分使 Jave7 新語法 `try-with-resources`,它會協助 IO 自動 Close :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**深入理解 Java IO 操作:徹底了解流、讀寫、序列化與技巧**](https://devtechascendancy.com/deep-in-java-io-operations_stream-io/) ::: [TOC] ## 資料操作 - 概述 程式說白了,大部分的時間都在操作資料,只是這些資料可能存在不同地方,像是 Register, Memory, Disk... 等等,而每個區域的資料都有不同特性 > 資料儲存地方的特性,並非本章要探討的,可以參考 另一篇文章 [**CPU、記憶體、快取分頁快取**](https://hackmd.io/8qOCfHI0SA6cK3l3gDCtOg) ``` mermaid graph TD; 資料-->Register; 資料-->Memory; 資料-->Disk; ``` ### 流概念 / 設計 * 在 Java 中,負責處理裡 I/O 的模塊分配在 `java.io`、`java.nio` Package 之下,並且會 **將一組有序的資料序列稱為「流」** > `java.nio` 是 JDK 1.4 版本之後導入的新 I/O 庫,為了 IO 增高效率 而流的輸出、輸出導向是站在應用的角度來觀察 * 流入應用的數據代表 `input` * 流出應用的數據代表 `output` ``` mermaid graph LR; 資料-->input; 鍵盤-->input; input-->應用-->output; output-->控制台; output-->記憶體; output-->文件; ``` :::success * 在 Java IO 流中,最常見的設計有兩種 * **適配器 Adapter** * **裝飾器 decorate** ::: ### 位元組流 / 字元流 * 我們知道流是一種有序資料的傳遞方式,而其中 Java 又把流為兩個大類別,他們也代表了不同的資料流動方案 | 流類型 | 流動的最小單位 | 特點 | IO 庫關鍵字 | | - | - | - | - | | 位元組流 | Byte | 沒有分檔案類型,皆是一系列的 Byte 數組 | `InputStream`, `OutputStream` | | 字元流 | 字元 | 字元,會依照「解碼」方式而有不同的切分方式 | `Reader`, `Writer` | ``` mermaid graph LR; Java-->位元組流; Java-->字元流; ``` ## InputStream / OutputStream 流 - 概述 * `InputStream` 是一個抽象類,它底下分別有繼承幾個類,如下圖,之後會依序介紹一些常見的類、如何使用 > ![](https://hackmd.io/_uploads/rJB_iuYya.png) > 其中 `FilterInputStream` 較為特別,它屬於一個裝飾類,用來加強某些 IO 類 ``` mermaid graph RL; InputStream; FileInputStream-->InputStream; ByteArrayInputStream-->InputStream; StringBufferInputStream-->InputStream; PipedInputStream-->InputStream; FilterInputStream-->InputStream; ObjectInputStream-->InputStream; BufferedInputStream-->FilterInputStream DataInputStream-->FilterInputStream LinenumberInputStream-->FilterInputStream PushbackInputStream-->FilterInputStream ``` * `OutputStream` 也是一個抽象類,如下圖,之後會依序介紹一些常見的類、如何使用 > ![](https://hackmd.io/_uploads/rkhUoOtJa.png) > 其中 `FilterOutputStream` 較為特別,它屬於一個裝飾類,用來加強某些 IO 類 ``` mermaid graph RL; OutputStream; FileOutputStream-->OutputStream; ByteArrayOutputStream-->OutputStream; PipedOutputStream-->OutputStream; FilterOutputStream-->OutputStream; ObjectOutputStream-->OutputStream; BufferedOutputStream-->FilterOutputStream DataOutputStream-->FilterOutputStream PrintInputStream-->FilterOutputStream ``` ### ByteArrayInputStream / ByteArrayOutputStream 位元組流 * `ByteArrayInputStream`、`ByteArrayOutputStream` 是 **使用 Adapter 模式**,組裝 `byte[]` 將其轉換為流,繼承、組合關係如下圖 ```mermaid classDiagram InputStream <|-- ByteArrayInputStream ByteArrayInputStream *-- byte_array ``` * `ByteArrayInputStream` 範例如下 ```java= 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); } } } ``` > ![](https://hackmd.io/_uploads/H15HdVd1p.png) * `ByteArrayOutputStream` 範例如下 ```java= 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)); } } ``` :::warning * 輸出得資料是以 Byte 為單位,所以如果資料轉為 Byte 後超過 255 就會換到下一個 Byte 輸出 ::: > ![](https://hackmd.io/_uploads/HJS4ltF1a.png) ### FileInputStream 讀取 / FileOutputStream 輸出檔案資料 * **以 ++Byte 的方式++,讀取、輸出檔案資料到到應用中**(記憶體中) * **`FileInputStream` 讀取範例** 原檔案資料如下(等等將要讀取的檔案) ```shell= ABC 安安 ``` :::info * 補充知識,用 Shell 創建隨機檔案 ```shell= mktemp -t InputSteamFile ``` ::: * 這裡要特別注意,由於檔案是 **透過 ++Byte++ 讀取進應用的記憶體中**,所以沒有辦法正確顯示超過 Byte 大小的字元,像是上述的「安」這個字就超過 Byte 大小,在使用 Byte 讀取時就會分開表示 ```java= 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 代表的是換行 > ![](https://hackmd.io/_uploads/r1DoCEdkp.png) :::success * 如果要高效讀取檔案,就應該使用 `read(byte[])` 讀取檔案,才不會平凡操作 IO ```java= 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 為單位輸出到檔案中 ```java= 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); } } } ``` > ![](https://hackmd.io/_uploads/ryW2-tKJT.png) ### 被遺棄的 StringBufferInputStream * `StringBufInputStreamUsage` 是 **使用 Adapter 模式**,組裝 `String` 將其轉換為流,繼承、組合關係如下圖 ```mermaid classDiagram InputStream <|-- StringBufInputStreamUsage StringBufInputStreamUsage *-- String ``` 範例如下 ```java= 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); } } } ``` :::danger * 被遺棄的原因: 由於 **它只使用字元編碼的低 8 位元**,所以假如字元編碼超過 8 位元,就無法正常被讀取 ::: 在上面範例我們知道「安」的組成是 (`229`, `174`, `137`) 才對,但以結果來看,它只讀取了低 8 位元,所以顯示錯誤(只顯示了最後的 `137`)! > ![](https://hackmd.io/_uploads/rkXymB_1T.png) ### 多執行序的管道 - PipedInputStream / PipedOutputStream * 執行序除了可以使用 `wait`/`notify` 通訊,也可以使用 I/O 的方式通訊;它的概念就像不同 Thread 對 **同一個** 文件(管道)進行塞資料 ``` mermaid graph LR; ThreadA_輸入 --> 管道_Piped; 管道_Piped --> ThreadB_讀取; ``` 範例如下: 1. 對 Piped 輸入資料方:使用 `PipedOutputStream` (想像成將資料輸出到應用以外) ```java= 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`(想像成將資料讀到應用中) ```java= 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 中是可以儲存資料的 ```java= class PipedInputStreamUsage { public static void main(String[] args) { Sender sender = new Sender(); Receiver receiver = new Receiver(sender); sender.start(); receiver.start(); } } ``` > ![](https://hackmd.io/_uploads/HJQ1TVty6.png) ### 流的串連 - SequenceInputStream * 內部是使用 `Vector` 結構來串連兩個 Stream > Vector 是有安全保護的 ArrayList,但又由於它的同步安全機制導致,效能較差 ``` mermaid graph LR; StreamA --> SequenceInputStream; StreamB --> SequenceInputStream; SequenceInputStream --> 順序輸出_Stream_A_B ``` 使用範例 ```java= 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); } } } ``` > ![](https://hackmd.io/_uploads/HyQjRNtyp.png) ### 裝飾類型的 FilterInputStream / FilterOutputStream 概述 * 設計模式中的「[**裝飾模式**](https://hackmd.io/DlDU-niGRg-0p-dw-BqxKQ?view)」特點在於 **++對原先類型的加強、減弱++**,而在 IO 中使用,則是 **基於基礎的 ByteStream 用來體現(加強)各個 File 不同的讀寫方式** > 這有利於提高程式碼的重用性 ```mermaid classDiagram InputStream <|-- FilterInputStream FilterInputStream o-- InputStream_準備被加強的類 OutputStream <|-- FilterOutputStream FilterOutputStream o-- OutputStream_準備被加強的類 ``` > ![](https://hackmd.io/_uploads/ByfBgSYka.png) ### DataInputStream 讀取 / DataOutputStream 寫入基本類型 & UTF-8 * `DataInputStream` 實作 DataInput 介面,用於 **讀取基礎資料類型**(int, float, long, double... 等等); 另外它還提供兩個特別方法: ^1.^ **能讀取 `UTF-8` 編碼的 `DataInputStream`#`readUTF` 方法**、^2.^ **能寫入 `UTF-8` 編碼的 `DataOutputStream`#`writeUTF` 方法** :::info * **`UTF-8`、`Unicode` 編碼**? * `Unicode` 是 Java 大多數預設的編碼方式,而 `UTF-8` 則是它的變體(特點是它支援作業系統) * `Unicode` 預設佔用 `2 Byte` 空間,有時候會浪費空間,而 `UTF-8` 則會針對不同字元進行編碼,給予適合的空間 > eg. `UTF-8` 對於 ASCII 就採用 1 Byte 空間,而非 ASCII 則給予 2 Byte 空間 ::: 1. `DataOutputStream` 範例: 輸出一般資料類型、**UTF8 資料類型** ```java= 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` 範例: 按照順序讀取相同檔案 ```java= 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); } } ``` > ![](https://hackmd.io/_uploads/Bk2R8dYJT.png) :::danger * 如果不按照順序讀取,或是使用錯誤方式讀取會拋出 `EOFException` 異常 > ![](https://hackmd.io/_uploads/SySXvOtya.png) ::: ### BufferedInputStream / BufferedOutputStream 減少密集 IO * 使用 `BufferedInputStream` / `BufferedOutputStream` 這兩個類都可以有效的減少密集讀寫 IO,可以 **加快 IO 效率,提升 IO 品質** > 以也可依照需求,在建構函數時傳入緩衝區大小的設定 使用上述範例進行修改 ```java= 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); } } } ``` > ![](https://hackmd.io/_uploads/H1ZFFuKkT.png) ### PrintStream 格式化輸出 * PrintStream 是輸出流(與 DataOutputStream 一樣),**可以輸出格式化的資料**;PrintStream 特點如下 * `PrintStream` & `DataOutputStream` 差異? 1. **PrintStream 不會拋出異常** 須透過 PrintStream#checkError 函數來檢查判斷是否輸出成功 2. **緩衝輸出的控制** PrintStream 可以透過設定來自動輸出,而輸出時機如下表 | 類 | 輸出的時機 | | - | - | | PrintStream | 完整輸出一個 `byte[]`、換行字元 `\n`(或是 `println`) | | DataOutputStream | Buffer 滿、呼叫 `flush` 強制刷新 | > ![](https://hackmd.io/_uploads/B1wsWFc1T.png) 3. **輸出編碼差異**: PrintStream#println 等同於 DataOutputStream#writeUTF,但兩者 **對於資料的編碼方式不同**: | 方法 | 編碼方式 | | - | - | | PrintStream#println | 本地作業系統預設的編碼(有可能是 BIG5, UTF-8, GBK... 等等) | | DataOutputStream#writeUTF | 預設採用 UTF-8 | ```java= 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` 差不多) > ![](https://hackmd.io/_uploads/S1rrlFqJ6.png) ## Reader / Writer 概述 在程式中作資料輸出較為單純,一般來說只需要輸出 Byte 即可,但是當 **在應用場合中 Java 需要注意每個文字檔中的編碼方式**,如果使用不對的方式編碼 1. 文字編輯器無法正常閱讀 Java 程式輸出的資料 ```mermaid graph LR; Java_app --> 輸出_UTF8; 輸出_UTF8 --> 文件; 文字編輯器 --> 使用_BIG5_閱讀; 使用_BIG5_閱讀 -.-> |無法正常訪問|文件; ``` 2. Java 程式不知道如何正確的解碼程式 ```mermaid graph LR; 文字編輯器 --> 輸出_BIG5; 輸出_BIG5 --> 文件; Java_app --> 使用_UTF8_閱讀; 使用_UTF8_閱讀 -.-> |無法正常訪問|文件; ``` ### 自動、指定編碼 * String#`getBytes` 方法,預設(不帶參數的狀況下)是使用「本地作業系統編碼」… 而本地作業系統的編碼就可能有多種 > 可能有 UTF-8, UTF-16, GBK, BIG5... 等等 :::success * **Java 大多預設儲存的編碼方式是 Unicode** ::: 使用 Java 查看本地作業系統編碼的方式如下 ```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); } } ``` > `Charset` 是 `java.nio.charset` 套件中的類 > > ![](https://hackmd.io/_uploads/SJMJJkjkT.png) * 而這跟 Reader / Writer 有什麼關係呢? 使用 Java 的 Reader / Writer 類,它會 **自動(也可以指定)幫我們切換「程式編碼」與「目標文件」之間的關係**(互相轉換) ```mermaid graph LR; 目標文件_編碼_BIG5 -.->|Java IO Writer| 程式編碼_假設為_UTF-8; 程式編碼_假設為_UTF-8 -.->|Java IO Reader| 目標文件_編碼_BIG5; ``` :::success * 當然,你也可以對 Java IO 的 **Writer / Reader 指定要轉換的編碼** * 由於 **JVM 統一採用平台無關編碼**,所以也是為何 Java 應用可以跨平台執行的原因(應為面對個平台編碼的責任推到了 JVM 負責) ```mermaid graph LR; Java_應用 --> JVM; JVM --> Window; JVM --> Linux; JVM --> Unix; JVM --> Mac; ``` ::: ### Reader / Writer 類別概述 * Reader 與 `InputStream` 的差異,**從層級結構之下來看有一定的差異**;`InputStream` 可以清楚地知道所有 `FilterInputStrem` 的子類都用來裝飾,而 Reader 則不是 ``` mermaid graph RL; Reader; CharArrayReader --> Reader; BufferedReader --> Reader; StringReader --> Reader; PipedReader --> Reader; FilterReader --> Reader; InputStreamReader --> Reader; LineNumberReader --> BufferedReader; PushBackReader --> FilterReader; FileReader --> InputStreamReader; ``` * Writer 與 `OutputStream` 層級結構較為相似(但仍有差異) ``` mermaid graph RL; Writer; CharArrayWriter --> Writer; BufferedWriter --> Writer; StringWriter --> Writer; PipedWriter --> Writer; FilterWriter --> Writer; OutputStreamWriter --> Writer; FileWriter --> OutputStreamWriter; ``` ### CharArrayReader / CharArrayWriter 讀取字元 > 讀取的是「字符」(一至多個 Byte),不是「字元」(一個 Byte) * `CharArrayReader`、`CharArrayWriter` 類別是使用 Adapter 模式,將 **字元陣列類型轉換為 Reader、Wirter 類型** ```mermaid classDiagram Reader <|-- CharArrayReader CharArrayReader *-- char_array Writer <|-- CharArrayWriter CharArrayWriter *-- char_array_ ``` 使用範例如下 * `CharArrayReader` 類使用 ```java= 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 的大小 > > ![](https://hackmd.io/_uploads/SyCeIkoyp.png) * `CharArrayWriter` 類使用 ```java= 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); } } ``` > ![](https://hackmd.io/_uploads/HymRD6ikp.png) ### StringReader 讀取本地編碼字串 * `StringReader` 可以 **解決被遺棄的 `StringBufferInputStream` 類問題**,它會完整讀取 String 字串完整字 ```mermaid classDiagram Reader <|-- StringReader StringReader *-- String ``` 使用範例 ```java= 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); } } ``` > ![](https://hackmd.io/_uploads/B144P1s1T.png) ### InputStreamReader / OutputStreamWriter 可指定編碼 * `InputStreamReader` 也是使用 Adapter 模式,它可以用來 **一次性讀取、輸出本地編碼(預設)的數據**,而不會只讀取一個 Byte 數據 ```mermaid classDiagram Reader <|-- InputStreamReader InputStreamReader *-- InputStream Writer <|-- OutputStreamWriter OutputStreamWriter *-- OutputStream ``` :::info * 並且 `InputStreamReader`、`OutputStreamWriter` 操作可以「**指定編碼**」 > 編碼有像是: `GBK`、`UTF-8`、`UTF-16`、`UTF-32`、`ISO-88591`、`Big 5` 等等相當的多~ > > NotePad++ 的可用編碼 > > ![](https://i.imgur.com/Hvi5KQh.png) ::: 使用範例如下 * `InputStreamReader` 使用 ```java= 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 編碼) > ![](https://hackmd.io/_uploads/H1dCOkikp.png) :::warning * 如果單純使用 `FileInputStream` 讀取,會被一次只能讀取一個 Byte 限制,導致中文字「安安」會分開來輸出 > (`229`、`174`、`137`, 當前是 UTF-8 編碼) ```java= 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); } } ``` > ![](https://hackmd.io/_uploads/rkI_dJiya.png) ::: 2. `Writer` 使用 ```java= 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); } } ``` > ![](https://hackmd.io/_uploads/SkCNRpiJa.png) :::danger * 假設這邊將寫入、讀取的編碼方式修改為不同的編碼(寫 `UTF-16`, 讀 `UTF-8`),就會出現讀取的亂碼(但事實上,數據並沒有錯誤) > ![](https://hackmd.io/_uploads/H1gjRTo1a.png) ::: ### 自動轉本地編碼 FileReader / FileWriter * `FileReader` 是 `InputStreamReader` 的子類 (`FileWriter` 是 `OutputStreamWriter` 的子類),它們的特點在於 **自動轉換檔案內容為「本地編碼」,++不能指定++ 其他字元編碼類型** 使用方式如下 (其實就是簡化,組裝了 ) ```java= 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); } } ``` > ![](https://hackmd.io/_uploads/Sk54Xpokp.png) ### BufferedReader / BufferedWriter 減少密集 IO * 這兩個類都帶有 **緩衝區**,資料會先輸出到緩衝區,當緩衝區滿了之後,才會將「字元」輸出到目標裝置中,這樣可以有效避免密集 IO 帶來的低效率操作 ```java= 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); } } } ``` > ![](https://hackmd.io/_uploads/r1P-Q0i1T.png) ### PrintWriter * `PrintWriter` 的建構函數中,可以接收(裝飾)Writer、OutputStream 兩個類 :::info * `PrintWriter` & `PrintStream` 差異 兩者的差別在 `PrintWriter` 可以指定特殊編碼,而 `PrintStream` 只能用於本地編碼 ::: ## IO 操作實戰:常用的操作 ### 讀 & 寫 - 複製檔案 * 邊讀邊寫就可以達到 ++**複製檔案**++ 的效果 > **`try-with-resources` 可以管理多個資源,不同資源中間使用 `;` 分開即可** ```java= 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(); } } ``` **--實作結果--** > ![](https://i.imgur.com/L4q1Bnq.png) :::warning * 若是 **轉為 String 透過 string#getBytes() 再寫出則會造成 copy 出來的檔案大小錯誤**,這部分 **可能是 ++`getBytes()` 讀取的是本地編碼++ 造成的** > 因為你的檔案可能並不是 本地編碼輸出 的檔案 ```java= 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](https://i.imgur.com/ju0DtoS.png) ::: ### IO 有無 Buffered 差異測試 * 有緩衝的 **++字節++流 IO**,這樣對於硬碟較不傷,並且可以 **==加快速度==**,以下讀取 `android_3rd_lib.aar`,一個使用 Buffer 來讀取,另一個則是使用一般的讀取,來查看這兩個對象所使用的時間 以下以讀取並使用有 Buffer、無 Buffer 作為比腳 ```java= 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 就是相同的道理這裡就不贅述了 > > ![](https://i.imgur.com/UZn07kY.png) ### RandomAccessFile - 隨機存取檔案 * `RandomAccessFile` 也是一個相對重要的類,以往我們讀取、寫入檔案時,必須按照 IO 順序來存取,而 **`RandomAccessFile` 則可以透過指標(`Pointer`)移動來操作檔案** 其中,建構函數中有一個 Mode 參數,其常見設置如下 | Mode 設置 | 說明 | | - | - | | 「`r`」 | 唯讀模式 | | 「`rw`」 | 讀寫模式(多線程不安全) | | 「`rws`」 | **「讀、寫」都同步**,效能較低,不果可以保證資料的完整性 | | 「`rwd`」 | 延遲讀寫;**只有「寫」是同步的**,也就是說在讀取數據上可能會發生不及時的狀況(可以用在下載) | :::warning * Mode 不可以單獨設置為「w」,否則會拋出異常 > ![](https://hackmd.io/_uploads/r1Ev7DCJa.png) ::: * `RandomAccessFile` 實作 DataInput、DataOutput 界面,所以可以做格式化讀取、寫入(本地編碼); 範例如下 ```java= 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); } } ``` > ![](https://hackmd.io/_uploads/BJLIsD0kT.png) ## Object 物件 - 序列化 IO 在需要的時候可以把對象,輸出到外部檔案,並在需要的時候重讀回來 ```java= 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; } } ``` :::info * 要輸出的物件 **必須要實作 `Serializable`,這個界面沒有方法,所以又稱為「標示界面」**,若是沒有這個界面則會錯誤 > ![](https://i.imgur.com/SjmKI5m.png) ::: ### ObjectOutputStream / ObjectInputStream 物件序列化、反序列化 * 使用範例 ```java= 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(); } } } ``` :::warning * 注意點: * Java IO 輸出的物件資料只有 `ObjectInputStream` 看得懂,普通的文字編輯器打開後只會看到亂碼 > ![](https://i.imgur.com/jXqfM8f.png) * 物件雖然可以在 JVM 中重新被建立,但是 **物件的記憶體位置已經不同** > 觀察 hashcode ::: > ![](https://hackmd.io/_uploads/HJ_KJ_AJ6.png) ### transient 忽略 IO * 從上面輸出的檔案中可以看到密碼,這並不是我們想要的結果,所以必須在序列化時隱藏,這時需要改動的是要序列化的檔案 (AccountInfo.java),並使用關鍵字 **==transient== (短暫的) 描述需要隱藏的參數** ```java= public class AccountInfo implements Serializable { public final String name; public final long id; public transient final String password; ... 省略部分 } ``` 這時再重新 Run 依次程式 password 輸出結果就是 null **--實作結果--** > ![](https://i.imgur.com/hul67Dv.png) ### 寫入多個物件 - 技巧 * 寫入多個對象時通常會出問題,這個時候就 **要使用 ArrayList 把多個物件存起來**,這樣才能保證序列化 & 反序列化不會出問題 ```java= 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(); } } } ``` **--實作結果--** > ![](https://i.imgur.com/68YTP8f.png) ### 隱藏方法 - readObject、writerObject * `readObject`、`writerObject` 是序列化、反序列化物件的過程中的隱藏方法,可以在這裡客製化自己需要的序列方案 > 這要兩方法是藏在你要序列化的物件內,於是做以下修改 **在序列化、反序列化時密碼成員** 由自己手動寫入、讀取! ```java= 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 如下(沒變) ```java= 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(); } } } ``` > ![](https://hackmd.io/_uploads/BJMAQOCyp.png) :::danger * 其中的 `readObject`、`writerObject` 的名稱、參數必須完全相同 * 為何不把這兩個方法加入到「Serializable」 界面? 這有關到,介面的特點 * 由於界面中的方法 **必須全部公開**! > 而這兩個方法實際上是希望被隱藏的! * 界面中的方法會 **強迫使用界面者一定要全部實現** > 但這兩個方法並非每個序列化成員都會使用到,這就不符合界面隔離原則 :::success * 藉此,用戶只能靠讀取 JavaDoc、額外文件 來了解序列化、反序列化的所有協定 ::: ::: ## 更多的 Java 語言相關文章 ### Java 語言深入 * 在這個系列中,我們深入探討了 Java 語言的各個方面,從基礎類型到異常處理,從運算子到物件創建與引用細節。點擊連結了解更多! :::info * [**深入探索 Java 基礎類型、編碼、浮點數、參考類型和變數作用域 | 探討細節**](https://devtechascendancy.com/basic-types_encoding_reference_variables-scopes/) * [**深入了解 Java 應用與編譯:從原始檔到命令產出 JavaDoc 文件 | JDK 結構**](https://devtechascendancy.com/java-compilation_jdk_javadoc_jar_guide/) * [**深入理解 Java 異常處理:從基礎概念到最佳實踐指南**](https://devtechascendancy.com/java-jvm-exception-handling-guide/) * [**深入理解 Java 運算子與修飾符 | 重要概念、細節 | equals 比較**](https://devtechascendancy.com/java-operators-modifiers-key-concepts_equals/) * [**深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用**](https://devtechascendancy.com/java-object-creation_jvm-reference-details/) ::: ### Java IO 相關文章 * 探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容! :::warning * [**Java File 操作指南:基礎屬性判斷、資料夾和檔案的創建、以及簡單示範**](https://devtechascendancy.com/java-file-operations-guide/) * [**深入探索 Java 編碼知識**](https://devtechascendancy.com/basic-types_encoding_reference_variables-scopes/) * [**深入理解 Java IO 操作:徹底了解流、讀寫、序列化與技巧**](https://devtechascendancy.com/deep-in-java-io-operations_stream-io/) * [**深入理解 Java NIO:緩衝、通道與編碼 | Buffer、Channel、Charset**](https://devtechascendancy.com/deep-dive-into-java-nio_buf-channel-charset/) ::: ### 深入 Java 物件導向 * 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念! :::danger * [**深入比較介面與抽象類:從多個角度剖析**](https://devtechascendancy.com/comparing-interfaces-abstract-classes/) * [**深度探究物件導向:繼承的利與弊 | Java、Kotlin 為例 | 最佳實踐 | 內部類細節**](https://devtechascendancy.com/deep-dive-into-oop-inheritance/) * [**「類」的生命週期、ClassLoader 加載 | JVM 與 Class | Java 為例**](https://devtechascendancy.com/class-lifecycle_classloader-exploration_jvm/) ::: ## Appendix & FAQ :::info ::: ###### tags: `Java 基礎`