---
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` 是一個抽象類,它底下分別有繼承幾個類,如下圖,之後會依序介紹一些常見的類、如何使用
> 
> 其中 `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` 也是一個抽象類,如下圖,之後會依序介紹一些常見的類、如何使用
> 
> 其中 `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);
}
}
}
```
> 
* `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 輸出
:::
> 
### 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 代表的是換行
> 
:::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);
}
}
}
```
> 
### 被遺棄的 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`)!
> 
### 多執行序的管道 - 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();
}
}
```
> 
### 流的串連 - 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);
}
}
}
```
> 
### 裝飾類型的 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_準備被加強的類
```
> 
### 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);
}
}
```
> 
:::danger
* 如果不按照順序讀取,或是使用錯誤方式讀取會拋出 `EOFException` 異常
> 
:::
### 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);
}
}
}
```
> 
### 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 |
```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` 差不多)
> 
## 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` 套件中的類
>
> 
* 而這跟 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 的大小
>
> 
* `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);
}
}
```
> 
### 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);
}
}
```
> 
### 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++ 的可用編碼
>
> 
:::
使用範例如下
* `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 編碼)
> 
:::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);
}
}
```
> 
:::
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);
}
}
```
> 
:::danger
* 假設這邊將寫入、讀取的編碼方式修改為不同的編碼(寫 `UTF-16`, 讀 `UTF-8`),就會出現讀取的亂碼(但事實上,數據並沒有錯誤)
> 
:::
### 自動轉本地編碼 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);
}
}
```
> 
### 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);
}
}
}
```
> 
### 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();
}
}
```
**--實作結果--**
> 
:::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();
}
}
```
> 
:::
### 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 就是相同的道理這裡就不贅述了
>
> 
### RandomAccessFile - 隨機存取檔案
* `RandomAccessFile` 也是一個相對重要的類,以往我們讀取、寫入檔案時,必須按照 IO 順序來存取,而 **`RandomAccessFile` 則可以透過指標(`Pointer`)移動來操作檔案**
其中,建構函數中有一個 Mode 參數,其常見設置如下
| Mode 設置 | 說明 |
| - | - |
| 「`r`」 | 唯讀模式 |
| 「`rw`」 | 讀寫模式(多線程不安全) |
| 「`rws`」 | **「讀、寫」都同步**,效能較低,不果可以保證資料的完整性 |
| 「`rwd`」 | 延遲讀寫;**只有「寫」是同步的**,也就是說在讀取數據上可能會發生不及時的狀況(可以用在下載) |
:::warning
* Mode 不可以單獨設置為「w」,否則會拋出異常
> 
:::
* `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);
}
}
```
> 
## 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`,這個界面沒有方法,所以又稱為「標示界面」**,若是沒有這個界面則會錯誤
> 
:::
### 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` 看得懂,普通的文字編輯器打開後只會看到亂碼
> 
* 物件雖然可以在 JVM 中重新被建立,但是 **物件的記憶體位置已經不同**
> 觀察 hashcode
:::
> 
### 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
**--實作結果--**
> 
### 寫入多個物件 - 技巧
* 寫入多個對象時通常會出問題,這個時候就 **要使用 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();
}
}
}
```
**--實作結果--**
> 
### 隱藏方法 - 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();
}
}
}
```
> 
:::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 基礎`