Download presentation
Presentation is loading. Please wait.
1
第 16 章 資料輸入與輸出
2
本章提要 16-1 甚麼是串流? 16-2 Java 串流類別架構 16-3 輸出、輸入資料 16-4 物件的讀寫 16-5 綜合演練
3
前言 在本章之前, 我們已多次用 "import java.io.*" 敘述匯入 Java 的 I/O (資料輸入與輸出) 套件, 並使用其中的 BufferedReader 類別的 readLine() 方法從鍵盤讀取使用者輸入的資料, 以及用 System.out.println() 方法在螢幕上顯示訊息或輸出程式執行的結果。
4
前言 但 java.io 套件的功能可不僅止於此, 舉凡從電腦的螢幕、鍵盤等各種裝置輸出或輸入資料, 或是讀寫電腦中的文字檔、二元檔 (binary file), 甚至是讀寫 ZIP 格式的壓縮檔, 都可透過 java.io 套件中的類別來完成。本章就要來介紹 Java 的資料輸入與輸出架構, 以及如何使用 java.io 套件的各項 I/O 類別。
5
16-1 甚麼是串流? 為了簡化程式設計人員處理 I/O 的動作, 不管讀取資料或寫入資料的來源/目的為何 (檔案、網路、或記憶體等等), 都是以串流 (stream) 的方式進行資料的讀取與寫入。而串流就是形容資料像一條河流一樣, 將資料依序從資料來源中流出, 或是流入目的地中。
6
甚麼是串流?
7
甚麼是串流? 在 java.io 套件中, 所有的資料輸出入類別都是以串流的方式來操作資料, 不管讀取或寫入, 都離不開以下三個基本動作:
開啟串流 (建構串流物件) 從串流讀取資料、或將資料寫入串流 關閉串流
8
甚麼是串流? 從程式的觀點, 可供程式讀取的資料來源稱為輸入串流 (input stream);而可用來寫入資料的則稱為輸出串流 (output stream)。不管我們是從磁碟 (檔案)、網路 (URL) 或其它來源或目的建立串流物件, 讀寫的方式都相似, Java 已替我們將其間的不同隱藏起來, 讓我們可以用一致的方式來操作串流, 大幅簡化學習過程。
9
16-2 Java 串流類別架構 在 java.io 套件中, 共有 4 組串流類別, 這 4 組類別可分為兩大類:
以 byte 為處理單位的輸出入串流, 又可稱之為位元串流 (Byte Streams) 以 char 為處理單位的輸出入串流, 又可稱之為字元串流 (Character Streams)
10
位元串流 位元串流是以 8 位元的 byte 為單位進行資料的讀寫, 位元串流有兩個最上層的抽像類別:InputStream (輸入) 及 OutputStream (輸出)。所有的輸出入位元串流都是由這兩個類別衍生出來的, 例如我們已用過很多次的 System.out, 它是個 java.io.PrintStream 類別的物件, 此類別是 FilterOutputStream 的子類別, 而 FilterOutputStream 則是 OutputStream 的子類別。
11
位元串流 關於位元串流的主要類別, 請參見以下的類別圖:
12
位元串流
13
位元串流 每種類別都適合於某類的讀取或寫入的動作, 例如 ByteArrayInputStream 適用於讀取位元陣列;FileOutputStream 則適用於寫入檔案。 另外比較特別的是 ObjectIntputStream 和ObjectOutputStream, 這兩個串流類別是特別為了讀寫我們自訂類別的物件而設計, 其詳細用法會在 16-4 節中介紹。
14
位元串流 這些串流類別的讀/ 寫方法都有個共通的特性, 就是它們的原型宣告都註明 "throws IOException", 所以使用這些方法時, 要記得用 try/catch 來執行, 或是在您的方法宣告也加上 "throws IOException" 的註記, 將例外拋給上層。
15
字元串流 字元串流是以 16 位元的 char 為單位進行資料的讀寫, 字元串流同樣有兩個最上層的抽像類別 Reader、Writer, 分別對應於位元串流的 InputStream、OutputStream。這類串流類別主要是因應國際化的趨勢, 為方便處理 16 位元的Unicode 字元而設的, 而且字元串流也會自動分辦資料中的 8 位元 ASCII 字元和 Unicode 字元, 不會將兩種資料弄混。
16
字元串流 字元串流類別的架構和位元串流有些類似, 而且其中各類別、方法的用法也都和位元串流中對應的類別、方法相似, 所以學會一種用法就等於學會兩種。不過 Reader、Writer 的衍生類別數量較少:
17
字元串流
18
字元串流
19
16-3 輸出、輸入資料 標準輸出、輸入 檔案輸出、輸入 讀寫二元檔
20
標準輸出、輸入 所謂標準輸出一般就是指螢幕, 而標準輸入則是指鍵盤, 在前幾章的程式中, 就是從鍵盤取得使用者輸入的資料, 從螢幕輸出訊息及執行結果。
21
標準輸出 在 System 類別中, 有兩個 PrintStream 類別的成員:
out 成員:代表標準輸出裝置, 一般而言, 都是指電腦螢幕。不過我們可以利用轉向的方式, 讓輸出的內容是輸出到檔案、印表機、或遠端的終端機等等。例如在命令提示字元視窗中, 可以用 “dir > test” 的方式, 使 dir 原本會顯示在螢幕上的資訊『轉向』存到 "test" 這個檔案中。(在 Unix/Linux 系統下也可用相同的轉向技巧, 例如 "ls > test")。
22
標準輸出 err 成員:代表標準『示誤訊息』輸出裝置, 同樣預設為螢幕。以往當應用程式執行過程中遇到錯誤並需顯示相關訊息通知使用者, 此時就是將訊息輸出到此裝置。雖然 err 與 out 同樣預設為螢幕, 但我們將 out 轉向時, err 並不會跟著轉向。
23
標準輸出 舉例來說, 如果執行 "dir ABC > test" 這個命令, 但資料夾中並無 ABC 這個檔案, 此時 dir 指令仍會將 "找不到檔案" 的示誤訊息顯示在螢幕上, 而不會存到 test 檔案中。
24
標準輸出 PrintStream 類別多重定義了適用於 Java 各種資料型別的 print()、println() 方法 (後者會在輸出資料後再多輸出一個換行字元以進行換行), 所以我們能用這兩個方法輸出任何資料型別, Java 都會自動以適當的格式輸出。
25
標準輸出 此外, PrintStream 類別還有一對多重定義的 write() 方法, 其功能也是輸出位元資料, 但此時參數是資料的『位元值』。例如我們要輸出 "A" 這個字元, 必須指定其 ASCII 碼 65, 例如 "write(65);"。另一個 write() 方法則是可輸出位元陣列的元素, 且可指定要從第幾個元素開始輸出、共輸出幾個元素:
26
標準輸出 PrintStream 類別有個和其它串流類別不同的特點, 就是它的方法都不會拋出 IOException 例外。以下這個簡單的程式示範了這幾個方法的用法及效果:
27
標準輸出
28
標準輸出
29
標準輸出 第 8〜12 行的迴圈會分別用 print( ) 和 write( ) 方法輸出 a[ ] 陣列中的元素。print( ) 方法會將各元素當整數值輸出, 所以可正常看到輸出值;使用 write( ) 方法時則是將元素值當成一個 2 進元數值輸出, 對螢幕而言, 就是將元素值當成 ASCII 碼, 然後輸出對應的 ASCII 字元。
30
標準輸出 以 a[0] 為例, ASCII 碼 10 是換行字元, 所以輸出這行後會自動換行; 至於ASCII 碼 20 對應的字元則是一個特殊的控制字元, 所以 a[1] 這行後面看不到內容;至於最後一個 a[4]:160 對應的字碼超出 127 (該字元是 a 上面多一撇), 所以在中文環境被當成 Big-5 字碼第一碼, 但因為無第二碼, 因此只輸出一個問號。
31
標準輸出 第 14 行改用 err 物件以 write() 方法輸出 b 陣列的全部內容。由於 ASCII 碼 7 是個特殊的 BEL 字元, 它只會讓電腦發出嗶聲, 但不會輸出任何『字』, 而 ASCII 碼 32 對應的是『空白』字元, 所以這行敘述只會讓電腦發出三聲嗶聲, 但螢幕上看不到任何輸出。
32
標準輸出 若要測試 System.out、System.err 的差異, 可改以轉向的方式來執行這個範例程式, 例如:
33
標準輸入 標準輸入一般指的是鍵盤, 但同樣可以利用轉向的方式從其它裝置來取得。不過細心的讀者或許發現, 前幾章的範例程式並未直接用 System.in 這個物件來讀取鍵盤輸入, 我們都是另外建立一個 BufferedReader 類別的物件, 然後用這個物件來讀取鍵盤輸入。為什麼要這樣做呢?原因很簡單:就是為了方便處理。
34
標準輸入 System.in 這個成員是 InputStream 類別的物件, 換言之它是將標準輸入當成位元串流來處理, 所以我們若用它來讀取鍵盤輸入, 讀到的都是位元的形式, 處理上並不方便 (想一下如果要讀取中文或 Unicode 字元, 就需進行額外的處理)。此外直接讀取鍵盤輸入串流時, 由於電腦鍵盤緩衝區的運作方式, 會造成一些不易處理的狀況。
35
標準輸入 為讓讀者瞭解直接使用 System.in 的情況, 我們先介紹 InputStream 類別的 read() 方法:
36
標準輸入 使用這些方法時, 都需處理 IOException 例外, 或是單純拋給上層處理。我們就來看一下透過 System.in 物件用這些方法直接讀取鍵盤輸入的情形:
37
標準輸入
38
標準輸入
39
標準輸入
40
標準輸入 第 9、18 行分別用不同的 read() 方法讀取鍵盤輸入的位元資料。
第 10 行叫用 Character.toString() 方法 (參見第 17 章) 將字元轉成字串。 第 13、22 行用 Math.pow() 方法 (參見第 17 章) 計算 2 的 N 次方。
41
標準輸入 讀者可能會覺得很奇怪, 為何會有如上的執行結果?最主要的原因是範例程式第 1 次呼叫 read() 方法只讀取1個位元, 但使用者可能輸入 2 位數字 (多個位元)、且 InputStream 的 read() 方法也會讀到 [Enter] 按鍵的資訊所造成的。
42
標準輸入 回頭看第一個執行結果:程式第 1 次要求輸入, 我們輸入 2 時, read() 方法傳回的是 “2” 這個字元的 ASCII 碼, 也就是 50, 所以程式必須進行一些轉換, 才能得到整數以進行運算。 程式第 2 次要求輸入時, 我們還未輸入, 程式就直接顯示例外訊息而結束, 這是因為前一次輸入 2 時按下的 [Enter] 鍵會產生歸位 (Carriage Return) 及換行 (Line Feed) 字元 (控制碼分別 13 及 10)。
43
標準輸入 所以第 2 次讀取時, read() 方法便直接讀到這些字元, 造成輸入的字串變成空字串, 導致第 19 行程式進行轉換時發生例外。 至於第 2 個執行結果, 則是在第 1 次輸入時, 就故意輸入多個字元。結果第 2 次的 read() 方法就讀到前次未讀到的 '5', 所以就直接計算 2 的 5 次方。
44
標準輸入 雖然 [Enter] 鍵的問題並非不能解決, 但一來這樣做會讓程式多做額外的處理, 二來大多數的應用程式都是要求使用者輸入『字元』而非位元, 所以我們會用字元串流來包裝 System.in, 達到簡化處理的目的。
45
用字元串流來包裝 System.in 為了方便我們從鍵盤取得資料, 我們會以字元串流來包裝 System.in 這個位元串流, 『包裝』 (wrap) 意指用 System.in 來建立字元串流的物件, 所以對程式來說, 它使用的是 『字元』 串流, 而非原始的 System.in 『位元』 串流。
46
用字元串流來包裝 System.in 以前幾章取得鍵盤輸入的方式為例, 我們都使用如下的程式:
47
用字元串流來包裝 System.in 上述程式就是先將 System.in 物件先包裝成 InputStreamReader 物件, 然後再包一層變成 BufferedReader 物件, 最後才用此物件的 readLine() 方法來取得輸入。之所以要包兩層, 主要原因可分為 2 點:
48
用字元串流來包裝 System.in InputStreamReader 是個特殊的字元串流, 它的功用就是從位元串流取得輸入, 然後將這些位元解讀成字元。因此在建構 InputStreamReader 物件時, 必須以一個位元串流物件為參數來呼叫其建構方法。但 InputStreamReader 在使用上仍有前述 [Enter] 鍵的問題, 操作並不方便。因此一般都會將它再包裝成其它更方便使用的串流類別物件, 例如 BufferedReader。
49
用字元串流來包裝 System.in BufferedReader 是所謂的緩衝式輸入串流, 也就是先將串流的輸入存到一記憶體緩衝區中, 程式再到這個緩衝區讀取輸入。在讀取檔案時這種緩衝式輸入效率較佳, 而讀取鍵盤輸入時, 也可免去處理 [Enter] 鍵的問題。
50
用字元串流來包裝 System.in 但 BufferedReader 只有以 Reader 物件為參數的建構方法, 因此我們必須先將 System.in 轉成 InputStreamReader 物件, 才能用後者呼叫 BufferedReader 的建構方法, 產生所要的物件。 使用 BufferedReader 的 readLine() 方法讀取輸入時, 每次會讀取 『一行』的內容, 且會自動忽略該行結尾的歸位及換行字元, 因此可順利解決 [Enter] 鍵的問題。請參考以下範例:
51
用字元串流來包裝 System.in
52
用字元串流來包裝 System.in
53
用字元串流來包裝 System.in
54
用字元串流來包裝 System.in 第 09 行用 InputStreamReader 包裝 System.in。
第 14 行以 while 迴圈的方式連續讀取多個字元, 遇到換行字元 (字碼為 10) 時即停止。 第 18、19 行改以 for 迴圈輸出所有讀到的字元。 第 24 行使用 BufferedReader 包裝第 09 行建立的 InputStreamReader 物件。
55
用字元串流來包裝 System.in 此外 BufferedReader 仍是有兩個 read() 方法可用於特定的字元讀取方式:
56
檔案輸出、輸入 在前一節我們透過 System.in 及 System.out 認識一些位元串流及字元串流的基本用法。其實只要稍加變化, 我們就能用串流來讀寫檔案了。 如前所述, 要進行檔案讀寫, 首先要做的就是開啟檔案串流, 接著即可用串流的方法進行讀寫, 讀寫完畢後則需關閉串流以節省系統資源。
57
使用字元串流讀取文字檔 要讀寫檔案, 可使用內建的 FileReader/FileWriter 字元串流來處理, 如其名稱所示, 它們是專為檔案所設計的。 這兩個字元串流的用法都很簡單, 分別以檔案名稱為參數呼叫其建構方法即可建立該檔案的串流物件, 以下我們先來看 FileReader 的用法。
58
使用字元串流讀取文字檔 FileReader 是 InputStreamReader 的子類別, 所以可用前一節介紹的 read() 方法來讀取串流中的字元。以下就是用 FileReader 讀取文字檔中所有字元並輸出在螢幕上的小程式。
59
使用字元串流讀取文字檔
60
使用字元串流讀取文字檔
61
使用字元串流讀取文字檔
62
使用字元串流讀取文字檔 第 13 行取得使用者輸入的檔名 (路徑) 字串, 第 14 行即以此字串建立 FileReader 物件 fr。
第 18、19 行以 while 迴圈的方式連續用 fr.read() 讀取檔案中的字元, 讀到檔案結尾時, read() 會傳回 -1, 即停止迴圈。 第 21 行呼叫 close() 關閉檔案串流。
63
使用字元串流讀取文字檔 至於寫入檔案用的 FileWriter 類別則是 OutputStreamWriter 的子類別。請注意, 如果在建立寫入串流時, 指定了已存在的檔案, 則程式會將檔案中原有的資料全部清除, 再寫入新的資料。 FileReader 類別並無定義自己的寫入方法, 其寫入功能只有繼承自 OutputStreamWriter 的三個 write() 方法:
64
使用字元串流讀取文字檔
65
使用字元串流讀取文字檔 相信這 3 個 write() 的用法應不必特別說明了, 我們直接來看範例程式的使用情形。以下這個範例程式請使用者輸入新的檔案名稱, 並建立 FileReader 寫入串流, 接著請使用者輸入字串、整數、浮點數等三種資料, 並寫入檔案串流中, 最後並輸出檔案內容以比對檢視:
66
使用字元串流讀取文字檔
67
使用字元串流讀取文字檔
68
使用字元串流讀取文字檔
69
使用字元串流讀取文字檔
70
使用字元串流讀取文字檔 第 14 行用使用者輸入的檔名路徑建立新的串流物件。雖然訊息提示的是請使用者輸入新檔案, 但其實也可輸入現有的檔名, 但此舉將會使檔案原有的內容被範例程式寫入的內容覆蓋掉, 因此建議不要用既有檔案做測試。 第 18、23、28 行分別將使用者輸入的資料以字串的格式用 write() 方法寫入。
71
使用字元串流讀取文字檔 第 19、24 行以 write() 寫入換行字元, 模擬輸入 [Enter] 按鍵的效果。也就是讓輸入的三個字串會分別存在 3 行。若不加這幾行程式, 寫入檔案的內容, 都會在同一行。 第 30 行用 flush() 方法將所有未寫入的內容立即寫入串流, 然後才於 31 行用 close() 方法關閉檔案串流。
72
使用字元串流讀取文字檔 第 33〜37 行另外建立 FileReader 物件讀取檔案內容, 並顯示在螢幕上, 以檢查剛才的輸入及寫入是否正常。 讀者可發現, 直接使用 FileReader/FileWriter 字元串流來處理檔案其實並不方便, 簡單如檔案換行的動作也要我們自行用 write() 方法寫入個換行字元。
73
使用字元串流讀取文字檔 而且我們還只介紹文字檔的部份, 若要處理二元檔案 (binary file, 例如圖形檔), 顯然會遇到更多的不便。因此一般在處理檔案串流時, 也和使用 System.in 一樣, 將檔案串流用較好用的緩衝式的串流包裝起來, 以下就來介紹如何透過緩衝式串流來讀寫檔案。
74
使用緩衝式串流包裝檔案串流 讀取檔案時, 我們同樣可用 BufferedReader 來包裝 FileReader 物件, 然後就能用 readLine() 方法來做整行的讀取。 至於寫入方面, 則可用對應的 BufferedWriter 來包裝 FileWriter 物件, BufferedWriter 除了有和 FileWriter 一樣的三個方法外, 還多了一個 newLine() 方法可替我們進行換行動作。
75
效率較佳的緩衝式處理 使用緩衝式串流來處理檔案讀寫還有一個優點, 就是讀寫的效率會比較佳。如果直接以檔案串流讀寫檔案, 程式每一個讀寫敘述, 都會使系統進行一次讀寫動作;而使用緩衝式讀寫串流, 可將一大筆資料都預先讀到緩衝區 (記憶體空間), 或是等要寫入的資料累積滿整個緩衝區時再一次寫入, 如此程式的效能會稍有提昇。
76
使用緩衝式串流包裝檔案串流 使用緩衝式寫入串流 BufferedWriter 時, 一定要在關閉串流物件前, 用 flush() 將緩衝區中的資料立即寫入串流, 否則會造成關閉串流而仍有資料未寫入的情況。以下就是使用緩衝式串流讀寫檔案的範例:
77
使用緩衝式串流包裝檔案串流
78
使用緩衝式串流包裝檔案串流
79
使用緩衝式串流包裝檔案串流
80
使用緩衝式串流包裝檔案串流
81
使用緩衝式串流包裝檔案串流
82
使用緩衝式串流包裝檔案串流 第 14、15 行用使用者輸入的檔名路徑建立新 FileWriter 串流物件, 再用此物件建立 BufferedWriter 緩衝式字元寫入串流。和前一範例相同, 雖然訊息提示的是請使用者輸入新檔案, 但其實也可輸入現有的檔名, 但此舉將會使檔案原有的內容被範例程式寫入的內容覆蓋掉。
83
使用緩衝式串流包裝檔案串流 第 22、28 行分別以 BufferedWriter 的 write() 方法寫入使用者輸入的姓名和電話字串。
第 33 行判斷使用者輸入的是否為大/小寫的 "Y", 是就再執行一次迴圈, 也就是再讓使用者輸入一筆資料。 第 35、36 行將緩衝區內容全部寫入, 並關閉串流。
84
使用緩衝式串流包裝檔案串流 第 43〜47 行是建立 BufferedReader 串流物件以讀取檔案內容, 並顯示在螢幕上。
第 45、46 行利用 while 迴圈重複以 BufferedReader 的 readLine() 方法讀取檔案的每一行, 當讀到的字串為 null 時, 即表示已到檔案結尾。
85
使用緩衝式串流包裝檔案串流 此例改用 BufferedReader 的 readLine() 方法來讀取檔案內容, 就不必像前幾個範例程式一樣, 用 Reader 類別的字元讀取方法 read() 來讀取了。
86
讀寫二元檔 文字檔可說是為了直接給人看而存在的, 給電腦程式用的檔案其實使用二元檔 (binary file) 就可以了。以 Java 為例, 早在第 4 章我們就學過 Java 的各種資料型別, 這些資料型態就是可由程式直接取用的。如果連數字都存成字串型式 "123456", 那 Java 還要自己把它轉成整數或其它數值型別才能進行運算, 非常不便。
87
讀寫二元檔 所以如果存程式用的資料也能使用像資料型別的格式, 顯然就比存成文字檔方便得多了。而要讓資料以有如資料型別的格式來儲存, 就要使用二元檔。 以 "123456" 為例, 若是使用整數格式存放時, 其 4 個位元組的值是 "00 01 E2 40"。如果我們看到這樣的檔案內容, 一定無法理解它們是什麼意思, 所以說二元檔是『給程式 (電腦) 看的檔案』。
88
讀寫二元檔 使用二元檔時, 由於很多資料都不是字元, 所以通常是以位元串流來處理。在位元串流中, 有 FileInputStream 和 FileOutputStream 兩個檔案輸入與輸出串流。但同樣的, 直接用這兩個串流來讀寫檔案非常不便, 因此我們通常會用 DataInputStream、DataOutputStream 這兩個位元串流包裝檔案串流, 然後讀寫二元檔。
89
讀寫二元檔 這兩個類別的特別之處, 就在於它們分別實作了 java.io 套件中 DataInput、DataOutput 這兩個介面。
90
DataOutputStream DataOutput 介面定義了一組寫入的方法, 而 DataOutputStream 實作了這個介面, 方便我們可直接寫入各種 Java 原生資料型別。只要呼叫這些方法, 就能將資料以二元的方式寫入串流中。以下所列就是 DataOutputStream 的資料寫入方法:
91
DataOutputStream
92
DataOutputStream 以下就是個簡單的資料寫入程式:
93
DataOutputStream
94
DataOutputStream
95
DataOutputStream 第 14 〜17 行以層層包裝的方式, 建構程式寫入檔案時所用的DataOutputStream 物件。
96
DataOutputStream 第 30 行呼叫 DataOutputStream 的 size() 方法傳回寫入的總位元數, 此數值應和用 "dir" 命令所看到的檔案大小數字相同。 第 31 、32 行做最後的『清理』及關閉串流動作。
97
DataOutputStream 執行此程式, 輸入檔名後, 程式就會將計算結果寫入指定的檔案中, 並傳回寫入的位元組數。但因為是以二元檔的格式儲存, 所以我們無法用一般文字編輯器讀取其內容, 例如用我們先前寫的文字檔讀取程式來讀取程式寫入的檔案, 只會看到如 "1Aj? 0Agg?" 這些亂碼。
98
DataInputStream 要解讀上述的二元檔案, 當然是以對應的 DataInputStream 來處理最為方便。DataInputStream 實作了 DataInput 介面, 同理此介面定義了各種資料型別的讀取方法, 透過 DataInputStream 物件呼叫這些現成的方法, 即可輕鬆從串流讀取各種資料型別。這些方法的名稱也都很一致, 幾乎是前述的 writeXXX() 方法改成 readXXX() 即可, 例如:
99
DataInputStream
100
DataInputStream 以下就是我們用 DataInputStream 讀取前一個程式所建立的二元檔的範例程式:
101
DataInputStream
102
DataInputStream
103
DataInputStream
104
DataInputStream 第 14〜17 行以層層包裝的方式, 建構程式讀取檔案時所用的 DataIuput Stream 物件。
第 21〜29 行以 try 的方式執行讀取檔案及顯示資料的動作。
105
DataInputStream 第 22〜25 行以 while() 迴圈持續讀取檔案, 其中第 23〜24 行分別以DataInputStream 的 readInt()、readDouble() 方法來讀取檔案中的整數及浮點數資料。 第 27 行呼叫 DataInputStream 的 skipBytes() 跳過 12 個位元組, 使程式每讀一筆整數及浮點數資料, 就跳過另一筆。因此只會顯示檔案中『第單數筆』的資料。
106
DataInputStream 第 30 行的 catch 敘述捕捉 EOFException 檔案結束例外物件, 並在第 31 行關閉串流。EOFException 是 IOException 的衍生類別, 用來表示已讀到檔案結尾 (End Of File, EOF) 或串流結尾的例外狀況。
107
無正負號的整數 Java 的整數型別都是可存放正負數值, 但像 C/C++ 程式語言都可宣告『無正負號』(unsigned) 的整數。以 16 位元的 short 為例, "unsigned short" ,可存放 0〜65535 的數值, 但 Java 的 short 因為也要能表示負數, 所以只能表示 〜32767 的數值。
108
無正負號的整數 為了讓 Java 程式也能正確讀寫由 C/C++ 程式讀寫的這類資料, DataInputStream 和 DataOutputStream 各有一對特別的讀寫方法, 可讀寫無正負號的整數資料:
109
16-4 物件的讀寫 Java 是物件導向的程式語言, 因此很多情況我們會需要將物件的資料寫入檔案。如果使用前面學過的方法, 以寫入二元檔為例, 您必須呼叫 DataOutput Stream 的多個 writeXXX() 方法, 才能將物件中每個資料成員一一寫入串流。
110
物件的讀寫 其實從本章開頭的類別架構圖可發現, 在位元串流部分, Java 已提供 ObjectOutputStream、ObjectInputStream 這兩個專用於物件讀寫的串流。它們各有readObject()、writeObject() 方法可一次就讀取整個物件的資料, 或寫入整個物件。
111
實作 Serializable 介面 但是, 我們不能任意用這些串流及其方法來讀寫類別物件, 必須有實作 java.io 套件中 Serializable 介面的類別, 才能用 ObjectXXX 串流物件來讀寫其物件。所幸, 實作 Serializable 介面是個相當簡單的動作, 因為這個介面未定義任何的方法和成員, 所以我們只要在類別定義加上 "implements Serializable" 這幾個字就可以了, 完全不需再自訂任何方法。
112
實作 Serializable 介面 此外要讀寫物件還需注意一點, 因為 ObjectOutputStream 在寫入物件時, 也會將類別的資訊記錄下來, 所以若要用另一個程式以 ObjectInputStream 將物件讀回來, 必須兩個程式中所定義的物件類別『完全』相同, 不能只是有相同的資料成員, 必須連方法及其它宣告也都一樣, 否則程式在進行讀取時, 會引發 ClassNotFoundException (找不到類別) 的例外。
113
寫入物件 以下我們就沿用第 14 章 TestCar.java 這個範例程式中的 MyCar 自訂類別, 將它宣告為 "implements Serializable":
114
寫入物件
115
寫入物件
116
寫入物件 接著我們就能寫一個程式, 利用 ObjectOutputStream 串流物件將汽車物件寫到指定的檔案中。以下這個範例程式會先請使用者輸入愛車的油量及耗油率資訊, 並以之建立 MyCar 物件, 然後再用 ObjectOutputStream 串流物件將之寫入 mycar 這個檔案。
117
寫入物件
118
寫入物件
119
寫入物件
120
寫入物件 第 23、24 行將 FileOutputStream 物件包裝成 ObjectOutputStream 物件。
第 26 行以 ObjectOutputStream 的 writeObject() 方法將物件寫入串流中。 第 27 、28 行呼叫將串流中所有資料立即寫入並關閉串流。
121
寫入物件 若以一般文書編輯器開啟程式寫入的檔案 "mycar", 將會看到一團亂碼, 因為 ObjectOutputStream 是以二元檔的方式將物件寫入檔案中, 要讀回檔案中的物件資訊, 可用 ObjectInputStream 串流。
122
從檔案讀取物件資料 要將檔案 (或其它串流) 中的物件資料讀回程式中處理, 可使用 ObjectInputStream 的 readObject() 方法。 請特別注意, readObject() 會拋出 IOException、ClassNotFoundException 這兩個 Checked 例外, 所以在呼叫 readObject() 的方法中, 必須拋出或處理這兩個例外。
123
從檔案讀取物件資料 也因此在讀取物件的範例程式中, 必須比讀寫其它串流時, 多處理一個 ClassNot FoundException 例外。請參考以下的範例程式:
124
從檔案讀取物件資料
125
從檔案讀取物件資料
126
從檔案讀取物件資料
127
從檔案讀取物件資料 第 6 行將 main() 方法宣告多拋出一個 ClassNotFoundException 例外。
第 09、10 行將 FileInputStream 包裝成 ObjectInputStream 物件。
128
從檔案讀取物件資料 第 11 行以 ObjectInputStream 的 readObject() 方法將從串流讀回物件。由於此程式只讀一筆物件就自行關閉迴圈, 所以未用 try/catch 來執行 readObject() 方法, 若您要參考前幾個範例程式的作法, 讓程式一直讀到檔案結尾, 就必須用 try 來執行 readObject() 方法, 並用 catch 捕捉 EOFException 例外物件。
129
從檔案讀取物件資料 第 12 行關閉串流。 第 17〜30 行則是模擬汽車行駛的迴圈, 此部份的說明可參見第 14 章。
130
16-5 綜合演練 將學生成績資料存檔 讀取學生成績檔並計算平均
131
將學生成績資料存檔 在現實環境中, 將物件資料存檔是很實際的應用。本範例程式就是建立一個學生成績資料類別, 並提供輸入介面, 最後再將輸入的學生成績物件存檔。為方便起見, 我們先設計一個存放學生資料的 Student 類別, 並存於 Student.java 檔案中。
132
將學生成績資料存檔
133
將學生成績資料存檔
134
將學生成績資料存檔 第 3 行用 "implements Serializable" 宣告此類別可寫入檔案中。
第 5〜10 行為可設定所有資料成員值的建構方法。
135
將學生成績資料存檔 第 15〜18 行定義了 4 個可傳回物件中各成員值的方法。
第 21 行定義了計算及傳回個人平均分數的方法, 在下個範例中會用到。 第 25〜28 行分別宣告姓名及英文/數學/Java 三科成績的資料成員。 接下來我們就來設計一個程式, 可讓使用者輸入學生資料, 並將學生資料存檔。
136
將學生成績資料存檔
137
將學生成績資料存檔
138
將學生成績資料存檔
139
將學生成績資料存檔
140
將學生成績資料存檔
141
將學生成績資料存檔 第 14、15 行將 FileOutputStream 包裝成 ObjectOutputStream 物件。
第 18 行宣告的 counter 變數是用來記錄使用者共輸入了幾筆學生資料。 第 20〜44 行以 do/while 迴圈持續取得使用者輸入的學生資料以建立學生物件, 並於第 40 行以 ObjectOutputStream 的 writeObject() 方法將物件寫入串流中。
142
將學生成績資料存檔 第 46 、47 行呼叫將串流中所有資料立即寫入並關閉串流。 第 49 、50 行顯示程式共寫入幾筆學生資料至檔案中。
程式會一直請使用者輸入學生資料, 直到使用者回答不再輸入為止。利用這個程式建立的學生資料檔也是二元檔, 我們可用物件讀取串流來讀取這個檔案的內容。
143
讀取學生成績檔並計算平均 這個範例是要讀取檔案中的學生成績並顯示出來, 同時還會加總各科分數, 以計算各科的平均分數。此程式會用 try/catch 區塊來進行讀取物件的動作, try 區塊中以 while 迴圈持續用 ObjectInputStream 的 readObject() 方法讀取物件, 當程式讀到檔案結尾時, readObject() 方法會拋出 EOFException 例外, 此時程式即可計算總平均分數並關閉串流。
144
讀取學生成績檔並計算平均
145
讀取學生成績檔並計算平均
146
讀取學生成績檔並計算平均
147
讀取學生成績檔並計算平均
148
讀取學生成績檔並計算平均
149
讀取學生成績檔並計算平均 第 15、16 行將 FileInputStream 物件包裝成 ObjectInputStream 物件。
第 18 行宣告的 counter 變數是用來記錄共讀取了幾筆學生資料。 第 20〜22 行宣告三個變數, 以計算各科所有學生分數的總和, 以便算出各科的平均分數。
150
讀取學生成績檔並計算平均 第 26 〜38 行以 try 區塊來進行讀取物件資料的動作, 第 28 行以 ObjectInputStream 的 readObject() 方法從串流讀取物件, 成功讀出物件後, 即在 29 行將 counter 變數加 1。 第 35〜37 行將讀到的學生成績輸出到螢幕上, 其中使用 Student 類別中所定義的 getXXX() 方法來取得各科的分數及平均分數。
151
讀取學生成績檔並計算平均 第 40〜48 行是 catch 檔案結束例外物件的區塊, 當 readObject() 方法讀到檔案結尾時, 即在螢幕上輸出讀到的總筆數、各科的總平均, 同時關閉 ObjectInputStream 串流物件。
152
讀取學生成績檔並計算平均 由於前一個 WriteObject.java 程式設計成可讓使用者自由輸入不定數量的學生資料, 所以這個 ReadObject.java 程式是用迴圈的方式持續讀 Student 物件, 直到檔案結束, 因此需以 try/catch 來處理 EOFException 例外物件, 讓程式不會意外終止執行。
Similar presentations