Overview of Content
在 Java 程式設計中,IO(Input/Output)操作是至關重要的一部分,它涉及資料的輸入和輸出,對於任何程式來說都是不可或缺的,我們常見的使用包括檔案傳輸、串流影音、Socket IO、物件序列化... 等等。
本文將深入探討Java中的IO操作,從流(Stream)的概念與設計開始,並實作大多數常見的 IO 操作,一直到常用的 IO 操作實戰技巧,以及物件的序列化IO。
無論您是初學者還是有經驗的開發人員,都會在本文中找到對IO操作全面理解的必要知識
寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀
以下部分使 Jave7 新語法
try-with-resources
,它會協助 IO 自動 Close
了解資料操作:流 Stream
程式說白了,大部分的時間都在操作資料,只是這些資料可能存在不同地方,像是 Register, Memory, Disk... 等等,而每個區域的資料都有不同特性
流概念 / 設計
資料儲存地方的特性,並非本章要探討的,可以參考 另一篇文章 CPU、記憶體、快取分頁快取
● 在 Java 中,負責處理裡 I/O 的模塊分配在 java.io
、java.nio
Package 之下,並且會 將一組有序的資料序列稱為「流」
java.nio
是 JDK 1.4 版本之後導入的新 I/O 庫,為了 IO 增高效率
而流的輸出、輸出導向是站在應用的角度來觀察
● 流入應用的數據代表 input
● 流出應用的數據代表 output
● 在 Java IO 流中,最常見的設計有兩種
位元組流 / 字元流
● 我們知道流是一種有序資料的傳遞方式,而其中 Java 又把流為兩個大類別,他們也代表了不同的資料流動方案
位元組流 | Byte | 特點 | IO 庫關鍵字 |
---|---|---|---|
流類型 | 流動的最小單位 | 沒有分檔案類型,皆是一系列的 Byte 數組 | InputStream , OutputStream |
字元流 | 字元 | 字元,會依照「解碼」方式而有不同的切分方式 | Reader , Writer |
InputStream / OutputStream 流
● InputStream
是一個抽象類,它底下分別有繼承幾個類,如下圖,之後會依序介紹一些常見的類、如何使用
其中
FilterInputStream
較為特別,它屬於一個裝飾類,用來加強某些 IO 類
● OutputStream
也是一個抽象類,如下圖,之後會依序介紹一些常見的類、如何使用
其中
FilterOutputStream
較為特別,它屬於一個裝飾類,用來加強某些 IO 類
ByteArrayInputStream / ByteArrayOutputStream 位元組流
● ByteArrayInputStream
、ByteArrayOutputStream
是 使用 Adapter 模式,組裝 byte[]
將其轉換為流,繼承、組合關係如下圖
● 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);
}
}
}
A. "ABC "-> (65, 66, 67, 32)
B. "安" -> (229, 174, 137):字符「安」的 UTF-8 表示形式由三個字節組成,分別是 0xE5
、0xB0
和 0x89
,轉為 10 進位就是 (229
, 174
, 137
)
C. "安" -> (229, 174, 137)
D. 最後的 10 代表的是換行
Information
● 如果要高效讀取檔案,就應該使用
read(byte[])
讀取檔案,才不會平凡操作 IOclass 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
將其轉換為流,繼承、組合關係如下圖
範例如下
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 對 同一個 文件(管道)進行塞資料
範例如下:
A. 對 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);
}
}
}
B. 讀取特定 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,但又由於它的同步安全機制導致,效能較差
使用範例
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 不同的讀寫方式
這有利於提高程式碼的重用性
DataInputStream 讀取 / DataOutputStream 寫入基本類型 & UTF8
● 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 空間;想了解更多請看 深入探索 Java 基礎類型、編碼、浮點數、參考類型和變數作用域 | 探討細節
A. 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);
}
}
B. 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
差異?
A. PrintStream 不會拋出異常
須透過 PrintStream#checkError 函數來檢查判斷是否輸出成功
B. 緩衝輸出的控制
PrintStream 可以透過設定來自動輸出,而輸出時機如下表
類 | 輸出的時機 |
---|---|
PrintStream | 完整輸出一個 byte[] 、換行字元 \n (或是 println ) |
DataOutputStream | Buffer 滿、呼叫 flush 強制刷新 |
C. 輸出編碼差異:
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 IO 介紹
在程式中作資料輸出較為單純,一般來說只需要輸出 Byte 即可,但是當 在應用場合中 Java 需要注意每個文字檔中的編碼方式,如果使用不對的方式編碼
A. 文字編輯器無法正常閱讀 Java 程式輸出的資料
B. Java 程式不知道如何正確的解碼程式
自動、指定編碼
● 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);
}
}
Charset
是java.nio.charset
套件中的類
● 而這跟 Reader / Writer 有什麼關係呢?
使用 Java 的 Reader / Writer 類,它會 自動(也可以指定)幫我們切換「程式編碼」與「目標文件」之間的關係(互相轉換)
● 當然,你也可以對 Java IO 的 Writer / Reader 指定要轉換的編碼
● 由於 JVM 統一採用平台無關編碼,所以也是為何 Java 應用可以跨平台執行的原因(因為面對個平台編碼的責任推到了 JVM 負責)
graph LR; Java_應用 --> JVM; JVM --> Window; JVM --> Linux; JVM --> Unix; JVM --> Mac;
Reader / Writer 類別概述
● Reader 與 InputStream
的差異,從層級結構之下來看有一定的差異;InputStream
可以清楚地知道所有 FilterInputStrem
的子類都用來裝飾,而 Reader 則不是,主要區別在於它們的層級結構和功能特性(Reader 層級結構是屬於靜態繼承,並不向 Stream 更多聚合模式)
● Writer 與 OutputStream
層級結構較為相似(但仍有差異,Write 層級結構是屬於靜態繼承,並不向 Stream 更多聚合模式)
CharArrayReader / CharArrayWriter 讀取字元
讀取的是「字符」(一至多個 Byte),不是「字元」(一個 Byte)
● CharArrayReader
、CharArrayWriter
類別是使用 Adapter 模式,將 字元陣列類型轉換為 Reader、Wirter 類型
使用範例如下
● 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 字串完整字
使用範例
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 數據
● 並且
InputStreamReader
、OutputStreamWriter
操作可以「指定編碼」編碼有像是:
GBK
、UTF-8
、UTF-16
、UTF-32
、ISO-88591
、Big 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 限制,導致中文字「安安」會分開來輸出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); } }
(
229
、174
、137
, 當前是 UTF-8 編碼)
B. 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
● FileReader
是 InputStreamReader
的子類 (FileWriter
是 OutputStreamWriter
的子類),它們的特點在於 自動轉換檔案內容為「本地編碼」,不能指定 其他字元編碼類型
使用方式如下 (其實就是簡化,組裝了 )
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 操作實戰:常用的操作
讀 & 寫 - 複製檔案
● 邊讀邊寫就可以達到 複製檔案 的效果
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();
}
}
try-with-resources
可以管理多個資源,不同資源中間使用;
分開即可
--實作結果--
● 若是 轉為 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(); } }
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
--實作結果--
IO 技巧:寫入多個物件
● 寫入多個對象時通常會出問題,這個時候就 要使用 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
● readObject
、writerObject
是序列化、反序列化物件的過程中的隱藏方法,可以在這裡客製化自己需要的序列方案
這要兩方法是藏在你要序列化的物件內,於是做以下修改
在序列化、反序列化時密碼成員 由自己手動寫入、讀取!
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();
}
}
}
● 其中的
readObject
、writerObject
的名稱、參數必須完全相同● 為何不把這兩個方法加入到「Serializable」 界面? 這有關到,介面的特點
● 由於界面中的方法 必須全部公開!
而這兩個方法實際上是希望被隱藏的!
● 界面中的方法會 強迫使用界面者一定要全部實現
但這兩個方法並非每個序列化成員都會使用到,這就不符合界面隔離原則
● 藉此,用戶只能靠讀取 JavaDoc、額外文件 來了解序列化、反序列化的所有協定
更多的 Java 語言相關文章
Java 語言深入
● 在這個系列中,我們全方位地探討了 Java 語言的各個核心主題,旨在幫助你徹底掌握這門強大的編程語言。無論你是想深入理解 Java 的基礎類型與變數作用域,還是探索異常處理與運算子的細節,這些文章都將為您提供寶貴的知識
此外,我們還涵蓋了物件創建、函數式編程、註解應用以及泛型的深入分析,幫助您提升在實際開發中的技能和效率
點擊以下連結,開始你的學習之旅~
● 深入探索 Java 基礎類型、編碼、浮點數、參考類型和變數作用域 | 探討細節
● 深入了解 Java 應用與編譯:從原始檔到命令產出 JavaDoc 文件 | JDK 結構
● 深入理解 Java 運算子與修飾符 | 重要概念、細節 | equals 比較
● 深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用
● 認識 Java 函數式編程:從 Lambda 表達式到方法引用 | 3 種方法引用
Java IO 相關文章
● 探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容!
深入 Java 多執行緒
● 這一系列文章將帶你深入了解 Java 多執行緒技術的各個方面,從基礎知識到進階應用,涵蓋了多執行緒編程的核心概念與實踐
深入 Java 物件導向
● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!