如有引用請標明出處
以下部分使 Jave7 新語法
try-with-resources
,它會協助 IO 自動 Close
如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 DevTech Ascendancy Hub
本篇文章對應的是 深入理解 Java IO 操作:徹底了解流、讀寫、序列化與技巧
程式說白了,大部分的時間都在操作資料,只是這些資料可能存在不同地方,像是 Register, Memory, Disk… 等等,而每個區域的資料都有不同特性
資料儲存地方的特性,並非本章要探討的,可以參考 另一篇文章 CPU、記憶體、快取分頁快取
在 Java 中,負責處理裡 I/O 的模塊分配在 java.io
、java.nio
Package 之下,並且會 將一組有序的資料序列稱為「流」
java.nio
是 JDK 1.4 版本之後導入的新 I/O 庫,為了 IO 增高效率
而流的輸出、輸出導向是站在應用的角度來觀察
流入應用的數據代表 input
流出應用的數據代表 output
在 Java IO 流中,最常見的設計有兩種
適配器 Adapter
裝飾器 decorate
我們知道流是一種有序資料的傳遞方式,而其中 Java 又把流為兩個大類別,他們也代表了不同的資料流動方案
流類型 | 流動的最小單位 | 特點 | IO 庫關鍵字 |
---|---|---|---|
位元組流 | Byte | 沒有分檔案類型,皆是一系列的 Byte 數組 | InputStream , OutputStream |
字元流 | 字元 | 字元,會依照「解碼」方式而有不同的切分方式 | Reader , Writer |
InputStream
是一個抽象類,它底下分別有繼承幾個類,如下圖,之後會依序介紹一些常見的類、如何使用
其中
FilterInputStream
較為特別,它屬於一個裝飾類,用來加強某些 IO 類
OutputStream
也是一個抽象類,如下圖,之後會依序介紹一些常見的類、如何使用
其中
FilterOutputStream
較為特別,它屬於一個裝飾類,用來加強某些 IO 類
ByteArrayInputStream
、ByteArrayOutputStream
是 使用 Adapter 模式,組裝 byte[]
將其轉換為流,繼承、組合關係如下圖
ByteArrayInputStream
範例如下
ByteArrayOutputStream
範例如下
以 Byte 的方式,讀取、輸出檔案資料到到應用中(記憶體中)
FileInputStream
讀取範例
原檔案資料如下(等等將要讀取的檔案)
補充知識,用 Shell 創建隨機檔案
這裡要特別注意,由於檔案是 透過 Byte 讀取進應用的記憶體中,所以沒有辦法正確顯示超過 Byte 大小的字元,像是上述的「安」這個字就超過 Byte 大小,在使用 Byte 讀取時就會分開表示
"ABC "-> (65, 66, 67, 32)
"安" -> (229, 174, 137):字符「安」的 UTF-8 表示形式由三個字節組成,分別是0xE5、0xB0 和 0x89,轉為 10 進位就是 (229, 174, 137)
"安" -> (229, 174, 137)
最後的 10 代表的是換行
如果要高效讀取檔案,就應該使用 read(byte[])
讀取檔案,才不會平凡操作 IO
FileOutputStream
輸出範例
將資料以 Byte 為單位輸出到檔案中
StringBufInputStreamUsage
是 使用 Adapter 模式,組裝 String
將其轉換為流,繼承、組合關係如下圖
範例如下
被遺棄的原因:
由於 它只使用字元編碼的低 8 位元,所以假如字元編碼超過 8 位元,就無法正常被讀取
在上面範例我們知道「安」的組成是 (229
, 174
, 137
) 才對,但以結果來看,它只讀取了低 8 位元,所以顯示錯誤(只顯示了最後的 137
)!
執行序除了可以使用 wait
/notify
通訊,也可以使用 I/O 的方式通訊;它的概念就像不同 Thread 對 同一個 文件(管道)進行塞資料
範例如下:
對 Piped 輸入資料方:使用 PipedOutputStream
(想像成將資料輸出到應用以外)
讀取特定 Piped 方:使用 PipedInputStream
(想像成將資料讀到應用中)
使用範例:
這裡測試時特別將發送、接收方得時間錯開(使用 Thread#sleep
不同時間),用來證明 Piped 中是可以儲存資料的
內部是使用 Vector
結構來串連兩個 Stream
Vector 是有安全保護的 ArrayList,但又由於它的同步安全機制導致,效能較差
使用範例
設計模式中的「裝飾模式」特點在於 對原先類型的加強、減弱,而在 IO 中使用,則是 基於基礎的 ByteStream 用來體現(加強)各個 File 不同的讀寫方式
這有利於提高程式碼的重用性
DataInputStream
實作 DataInput 介面,用於 讀取基礎資料類型(int, float, long, double… 等等);
另外它還提供兩個特別方法: 1. 能讀取 UTF-8
編碼的 DataInputStream
#readUTF
方法、2. 能寫入 UTF-8
編碼的 DataOutputStream
#writeUTF
方法
UTF-8
、Unicode
編碼?
Unicode
是 Java 大多數預設的編碼方式,而 UTF-8
則是它的變體(特點是它支援作業系統)
Unicode
預設佔用 2 Byte
空間,有時候會浪費空間,而 UTF-8
則會針對不同字元進行編碼,給予適合的空間
eg.
UTF-8
對於 ASCII 就採用 1 Byte 空間,而非 ASCII 則給予 2 Byte 空間
DataOutputStream
範例:
輸出一般資料類型、UTF8 資料類型
DataOutputStream
範例:
按照順序讀取相同檔案
如果不按照順序讀取,或是使用錯誤方式讀取會拋出 EOFException
異常
使用 BufferedInputStream
/ BufferedOutputStream
這兩個類都可以有效的減少密集讀寫 IO,可以 加快 IO 效率,提升 IO 品質
以也可依照需求,在建構函數時傳入緩衝區大小的設定
使用上述範例進行修改
PrintStream 是輸出流(與 DataOutputStream 一樣),可以輸出格式化的資料;PrintStream 特點如下
PrintStream
& DataOutputStream
差異?
PrintStream 不會拋出異常
須透過 PrintStream#checkError 函數來檢查判斷是否輸出成功
緩衝輸出的控制
PrintStream 可以透過設定來自動輸出,而輸出時機如下表
類 | 輸出的時機 |
---|---|
PrintStream | 完整輸出一個 byte[] 、換行字元 \n (或是 println ) |
DataOutputStream | Buffer 滿、呼叫 flush 強制刷新 |
輸出編碼差異:
PrintStream#println 等同於 DataOutputStream#writeUTF,但兩者 對於資料的編碼方式不同:
方法 | 編碼方式 |
---|---|
PrintStream#println | 本地作業系統預設的編碼(有可能是 BIG5, UTF-8, GBK… 等等) |
DataOutputStream#writeUTF | 預設採用 UTF-8 |
從結果可以看出,我目前使用的作業系統 (PrintStream
) 就是 UTF-8 (與 DataOutputStream
差不多)
在程式中作資料輸出較為單純,一般來說只需要輸出 Byte 即可,但是當 在應用場合中 Java 需要注意每個文字檔中的編碼方式,如果使用不對的方式編碼
文字編輯器無法正常閱讀 Java 程式輸出的資料
Java 程式不知道如何正確的解碼程式
String#getBytes
方法,預設(不帶參數的狀況下)是使用「本地作業系統編碼」… 而本地作業系統的編碼就可能有多種
可能有 UTF-8, UTF-16, GBK, BIG5… 等等
使用 Java 查看本地作業系統編碼的方式如下
Charset
是java.nio.charset
套件中的類
而這跟 Reader / Writer 有什麼關係呢?
使用 Java 的 Reader / Writer 類,它會 自動(也可以指定)幫我們切換「程式編碼」與「目標文件」之間的關係(互相轉換)
當然,你也可以對 Java IO 的 Writer / Reader 指定要轉換的編碼
由於 JVM 統一採用平台無關編碼,所以也是為何 Java 應用可以跨平台執行的原因(應為面對個平台編碼的責任推到了 JVM 負責)
Reader 與 InputStream
的差異,從層級結構之下來看有一定的差異;InputStream
可以清楚地知道所有 FilterInputStrem
的子類都用來裝飾,而 Reader 則不是
Writer 與 OutputStream
層級結構較為相似(但仍有差異)
讀取的是「字符」(一至多個 Byte),不是「字元」(一個 Byte)
CharArrayReader
、CharArrayWriter
類別是使用 Adapter 模式,將 字元陣列類型轉換為 Reader、Wirter 類型
使用範例如下
CharArrayReader
類使用
我們可以看到,中文字確實就超過一個 Byte 的大小
CharArrayWriter
類使用
StringReader
可以 解決被遺棄的 StringBufferInputStream
類問題,它會完整讀取 String 字串完整字
使用範例
InputStreamReader
也是使用 Adapter 模式,它可以用來 一次性讀取、輸出本地編碼(預設)的數據,而不會只讀取一個 Byte 數據
並且 InputStreamReader
、OutputStreamWriter
操作可以「指定編碼」
編碼有像是:
GBK
、UTF-8
、UTF-16
、UTF-32
、ISO-88591
、Big 5
等等相當的多~NotePad++ 的可用編碼
使用範例如下
InputStreamReader
使用
Reader 會依照個平台的編碼方式進行讀取,所以中文字「安安」可以正常作為一個字元輸出(23433
, 當前是 UTF-8 編碼)
如果單純使用 FileInputStream
讀取,會被一次只能讀取一個 Byte 限制,導致中文字「安安」會分開來輸出
(
229
、174
、137
, 當前是 UTF-8 編碼)
Writer
使用
假設這邊將寫入、讀取的編碼方式修改為不同的編碼(寫 UTF-16
, 讀 UTF-8
),就會出現讀取的亂碼(但事實上,數據並沒有錯誤)
FileReader
是 InputStreamReader
的子類 (FileWriter
是 OutputStreamWriter
的子類),它們的特點在於 自動轉換檔案內容為「本地編碼」,不能指定 其他字元編碼類型
使用方式如下 (其實就是簡化,組裝了 )
這兩個類都帶有 緩衝區,資料會先輸出到緩衝區,當緩衝區滿了之後,才會將「字元」輸出到目標裝置中,這樣可以有效避免密集 IO 帶來的低效率操作
PrintWriter
的建構函數中,可以接收(裝飾)Writer、OutputStream 兩個類
PrintWriter
& PrintStream
差異
兩者的差別在 PrintWriter
可以指定特殊編碼,而 PrintStream
只能用於本地編碼
邊讀邊寫就可以達到 複製檔案 的效果
try-with-resources
可以管理多個資源,不同資源中間使用;
分開即可
–實作結果–
若是 轉為 String 透過 string#getBytes() 再寫出則會造成 copy 出來的檔案大小錯誤,這部分 可能是 getBytes()
讀取的是本地編碼 造成的
因為你的檔案可能並不是 本地編碼輸出 的檔案
有緩衝的 字節流 IO,這樣對於硬碟較不傷,並且可以 加快速度,以下讀取 android_3rd_lib.aar
,一個使用 Buffer 來讀取,另一個則是使用一般的讀取,來查看這兩個對象所使用的時間
以下以讀取並使用有 Buffer、無 Buffer 作為比腳
–實測結果–
這邊特別展示時間的縮短,BufferOutputStream 就是相同的道理這裡就不贅述了
RandomAccessFile
也是一個相對重要的類,以往我們讀取、寫入檔案時,必須按照 IO 順序來存取,而 RandomAccessFile
則可以透過指標(Pointer
)移動來操作檔案
其中,建構函數中有一個 Mode 參數,其常見設置如下
Mode 設置 | 說明 |
---|---|
「r 」 |
唯讀模式 |
「rw 」 |
讀寫模式(多線程不安全) |
「rws 」 |
「讀、寫」都同步,效能較低,不果可以保證資料的完整性 |
「rwd 」 |
延遲讀寫;只有「寫」是同步的,也就是說在讀取數據上可能會發生不及時的狀況(可以用在下載) |
Mode 不可以單獨設置為「w」,否則會拋出異常
RandomAccessFile
實作 DataInput、DataOutput 界面,所以可以做格式化讀取、寫入(本地編碼);
範例如下
在需要的時候可以把對象,輸出到外部檔案,並在需要的時候重讀回來
要輸出的物件 必須要實作 Serializable
,這個界面沒有方法,所以又稱為「標示界面」,若是沒有這個界面則會錯誤
使用範例
注意點:
Java IO 輸出的物件資料只有 ObjectInputStream
看得懂,普通的文字編輯器打開後只會看到亂碼
物件雖然可以在 JVM 中重新被建立,但是 物件的記憶體位置已經不同
觀察 hashcode
從上面輸出的檔案中可以看到密碼,這並不是我們想要的結果,所以必須在序列化時隱藏,這時需要改動的是要序列化的檔案 (AccountInfo.java),並使用關鍵字 transient (短暫的) 描述需要隱藏的參數
這時再重新 Run 依次程式 password 輸出結果就是 null
–實作結果–
寫入多個對象時通常會出問題,這個時候就 要使用 ArrayList 把多個物件存起來,這樣才能保證序列化 & 反序列化不會出問題
–實作結果–
readObject
、writerObject
是序列化、反序列化物件的過程中的隱藏方法,可以在這裡客製化自己需要的序列方案
這要兩方法是藏在你要序列化的物件內,於是做以下修改
在序列化、反序列化時密碼成員 由自己手動寫入、讀取!
讀取、寫入物件的 Demo 如下(沒變)
其中的 readObject
、writerObject
的名稱、參數必須完全相同
為何不把這兩個方法加入到「Serializable」 界面? 這有關到,介面的特點
由於界面中的方法 必須全部公開!
而這兩個方法實際上是希望被隱藏的!
界面中的方法會 強迫使用界面者一定要全部實現
但這兩個方法並非每個序列化成員都會使用到,這就不符合界面隔離原則
在這個系列中,我們深入探討了 Java 語言的各個方面,從基礎類型到異常處理,從運算子到物件創建與引用細節。點擊連結了解更多!
探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容!
探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!
Java 基礎