檔案處理
前言 目前所學的:從使用者取得資料,把資料放在物件或者陣列,然後對這些資料進行處理;在處理完資料之後,將結果列印出來 可是,程式結束後,資料都不見了;如果之後我們需要再次處理相同的資料的時候,怎麼辦? 再讓使用者輸入一次嗎?先不說輸入的過程有可能會輸入錯誤的資料,更何況如果這些資料有上千筆、甚至上萬筆的時候,那 … 我們需要一種機制,把處理的資料儲存起來,然後需要的時候,再把資料讀進來 檔案 資料庫
資料串流 在 Java,串流除了用在檔案的輸出/輸入之外,其實它用在所有的 I/O(Input/Output),例如:印表機、網路連線、資料庫的輸入/輸出等等。
串流 依據處理的資料單位,串流分成兩大類 一類是位元組串流(Byte Stream),其讀寫的單位為位元組(byte) 另一類是字元串流(Character Stream),其讀寫的單位是字元(character) 依據處理的資料流向,串流又可以分成輸入串流(Input Stream)以及輸出串流(Output Stream) Stream 是單行道
串流類別 所有的串流類別都是屬於 java.io 的套件 所有的位元組串流都有一個父類別,依照輸入以及輸出分別定義了 InputStream 和OutputStream 兩個類別。 如果要讀取二進位的檔案,請使用 InputStream 的子類別;如果要輸出一個二進位的檔案,請使用OutputStream 的子類別 所有的字元串流也有一個父類別,依照輸入以及輸出分別定義了 Reader 和 Writer 兩個類別。 如果是讀/寫純文字資料,請使用 Reader/Writer 的子類別
串流類別 不論是 InputStream/OutputStream 或者Reader/Writer,一般都不建議直接使用這四個類別以及其子類別(direct subclasses),因為這些子類別被歸類為低階串流,它們所提供的方法都很少也不好用 一般建議使用的串流類別大多屬於這些類別的子孫類別,我們稱之為過濾串流(Filter Stream)
將位元組轉換成字元 有可能把位元組串流轉換成字元串流? Java 提供了兩個互轉的類別 InputStreamReader:將位元組串流轉成字元串流 OutputStreamWriter:將字元串流轉成位元組串流
檔案的輸出與輸入 檔案(或者說所有 I/O)處理的三個主要步驟: 一個實際的檔案(或者 I/O 設備)在程式中由一個串流物件代表 開啟檔案 檔案資料的處理 關閉檔案 一個實際的檔案(或者 I/O 設備)在程式中由一個串流物件代表 例如:螢幕由 System.out 物件代表 因此,利用物件來讀取/寫出資料,相當於對檔案進行讀取/寫出的動作。
檔案的輸出與輸入 開啟檔案 檔案資料的處理 關閉檔案 一般來說,檔案格式處理前都已經知道。 輸入:若出現例外,可能檔案不存在、沒有存取權限等。 輸出:若檔案不存在,則產生新檔;若檔案存在,則刪除舊檔並產生新檔。若出現例外,可能有一個與檔案名稱相同的目錄、硬碟(或者配額)滿了、沒有足夠的權限等。 檔案資料的處理 一般來說,檔案格式處理前都已經知道。 大多不知道會有幾筆資料,因此一般使用迴圈處理資料並需要知道判斷 EOF(End of File)的方式。 請留意檔案的編碼:請使用相對應的串流類別 關閉檔案 非常重要。例如,連結資料庫的連線一般都有上限的限制,如果不關閉,…
檔案的輸出與輸入 檔案輸入/輸出的相關串流類別: FileInputStream/FileOutputStream FileReader/FileWriter
開啟檔案 以檔案名稱來開啟一個輸出檔案,而該檔案的開啟方式如下: FileWriter f = new FileWriter(“test.txt”); 由於 PrintWriter 所提供的方法非常多,我們希望使用它;產生 PrintWriter 物件的方式如下: PrintWriter p = new PrintWriter(f); 有了 p 之後,我們就可以使用 println()、format()、或者 printf() 等方法將資料輸出到test.txt 中
13 PrintWriter p = new PrintWriter(f); 15 // 資料處理:將資料輸出 16 p.println(1234); 17 p.println(21.34); 18 p.println("Hello, world."); 20 Worker w = new Worker("John", 32000); 21 p.println(w.getName()); 22 p.println(w.getSalary()); 24 // 關閉檔案 25 p.close(); 26 } 27 } 01 import java.io.*; 03 public class TestFileWriter1 { 04 public static void main(String[] args) { 05 // 開啟檔案 06 FileWriter f = null; 07 try { 08 f = new FileWriter("test.txt"); 09 } catch (IOException e) { 10 System.out.println("檔案開啟錯誤"); 11 System.exit(-1); 12 }
執行畫面
執行畫面 FileWriter f = new FileWriter(“test.txt”, true);
練習題 請修改 TestFileWriter1 使得每一次執行的資料會不斷的加到 test.txt 的最後。 我們想把 PrintWriter 改成BufferedWriter,請修改 TestFileWriter1。 依據相同的概念,但是請使用 FileOutputStream 來處理檔案,請對 TestFileWriter1 作必要的修改。
讀取純文字檔 由於 test.txt 是純文字檔,開啟輸入檔案的方式如下 FileReader f = new FileReader(“test.txt”); 由於大部分的文字檔案都是一行一行的,所以我們就使用一次可以讀一行的BufferedReader來讀取資料,而產生一個BufferedReader物件的方式如下 BufferedReader r = new BufferedReader(f); 由於我們事先知道資料的內容依序是整數、浮點數、字串、以及利用 Worker 的 get 方法所取得的字串和整數,所以我們讀進資料之後,就可以依照其資料型態作進一步的處理
BufferedReader 由於 BufferedReader 會把讀進來的資料先暫時放在一個 Buffer 裡,這樣處理速度會比較快,而且它還提供了 readLine() 方法
TestFileReader1 04 public static void main(String[] args) { 05 // 開啟檔案 05 // 開啟檔案 06 FileReader f = null; 07 try { 08 f = new FileReader("test.txt"); 09 } catch (IOException e) { 10 System.out.println("檔案開啟錯誤"); 11 System.exit(-1); 12 } 13 BufferedReader p = new BufferedReader(f); 14
19 int num1 = Integer.parseInt(input); 20 input = p.readLine(); 15 // 資料處理:將資料讀進來併處理 16 String input; 17 try { 18 input = p.readLine(); 19 int num1 = Integer.parseInt(input); 20 input = p.readLine(); 21 double num2 = Double.parseDouble(input); 22 System.out.println("Total: " + (num1 + num2)); 23 24 input = p.readLine(); 25 System.out.println(input); 26 27 input = p.readLine(); 28 Worker w = new Worker(input, Integer.parseInt(p.readLine())); 29 w.setSalary(w.getSalary() + 1200); 30 w.setName(w.getName() + " Steward"); 31 System.out.println(w.getName() + " 薪資為 " + w.getSalary()); 32 } catch (IOException e) { 33 System.out.println("資料讀取錯誤"); 34 System.exit(-2); 35 } 雖然當初儲存前是整數,但是實際儲存的方式是字串,因此需要做適當的轉換。
TestFileReader1 37 // 關閉檔案 38 try { 39 p.close(); 37 // 關閉檔案 38 try { 39 p.close(); 40 } catch (IOException e) { 41 System.out.println("檔案無法關閉"); 42 } 43 } 44 }
輸出/輸入整數、物件 有沒有可能將原始資料型態的資料寫入檔案,然後以原始資料型態的方式將資料讀入? 可以使用 DataInputStream 和 DataOutputStream。 不在本課程的範圍,請自行學習。 有沒有可能直接將物件寫入檔案,然後將物件直接讀入? 可以使用 ObjectInputStream 和 ObjectOutputStream。
範例 假設總公司的人事資料系統每隔一段時間就會收到從各個分公司傳過來的 Worker 薪資資料(以便經由銀行匯款方式給附薪資)。薪資資料儲存在一個名為 person.dat 的檔案,這些檔案有可能是空的,事先也不知道會有多少筆。但是如果有資料的話,其格式如下: s 采壹林 65000 h 無中線 132 s 舟截輪 67000
範例 person.dat 第一個欄位,代表員工類別,s 代表SalaryWorker,而 h 代表 HourlyWorker 第二個欄位代表員工姓名 第三個欄位代表薪資(如果是 s 的話)或者工作時數(如果是 h 的話) 假設這個人事系統在讀完資料之後,除了需要將每一個人的薪資清單列印出來之外,還會把這一次檔案內的薪資總額列印出來。
程式說明 由於讀進來的每一筆資料都有三個欄位,因此程式必須能夠把這三項資料分別 把資料分開的方式有好幾種,例如利用一個類別 java.util.StringTokenizer 一個 StringTokenizer 的物件能夠利用預設的分隔符號(空格)將一個字串的資料項目分開 可呼叫 nextToken() 方法 讀完一個 token,指標會移到下一個 token
練習題 請依照下列需求,修改 TestFileReader: 假設讀完資料之後,將每一個人的薪資清單輸出到一個叫做 salary.dat 的檔案中,而且每一次新處理的資料都會加到 salary.dat 的最後。 假設每一筆資料讀進來之後,就把資料存放到一個ArrayList (或者陣列)的物件;然後利用 ArrayList (或者陣列) 來處理資料,例如排序。 假設 person.dat 的分隔符號從空格改成分號(;),請善用 StringTokenizer,重新修改你的程式。
練習題 繼續之前 Circle/Rectangle 的範例, 請設計一套系統,使得使用者可以從檔案 shape.txt 讀取[任意/固定]個 Circle 或者 Rectangle 物件,檔案格式如下: C 4.2 R 1.2 4.5 C 3.1 C 代表 cirlce,而 R 代表 rectangle 請依照之前的範例,將讀取的物件的面積、周長等資料列印出來 請將使用者輸入的 Circle 或者 Rectangle 物件新增至 shape.txt 中(保留原來的資料)
File 類別 利用 File 物件來開啟檔案 一個 File 物件可以代表一個檔案或者目錄,它所提供的方法包含目前的檔案路徑、檔案大小、新增/刪除檔案(或者目錄)等 開啟一個 File 物件的方式 File f = new File(“TestFile1.java”);
執行畫面
指定 File 路徑 在 Unix 類的作業系統中,檔案絕對路徑名稱為 Windows 類的作業系統中,檔案絕對路徑名稱為 File f = new File(“/home/MyBooks/Java101/Ch13/I4/TestFile1.java”); Windows 類的作業系統中,檔案絕對路徑名稱為 File f = new File(“D:\\MyBooks\\Java101\\Ch13\\I4\\TestFile1.java”); 或者 File f = new File(“D:/MyBooks/Java101/Ch13/I4/TestFile1.java”);
範例 (指定 File 路徑) 01 import java.io.*; 02 03 public class TestFile2 { 04 public static void main(String[] args) { 05 File f = new File("d:/MyBooks/Java101/Ch13/I4"); 06 System.out.println(f.getName() + " 的父目錄是 " + f.getParent()); 07 if(f.isDirectory()) 08 System.out.println(f.getName() + " 是一個目錄"); 09 displayDirectory(f); 10 11 File newd = new File(f.getAbsolutePath() + "/newDir"); 12 newd.mkdir(); displayDirectory(f); }
範例 (指定File路徑) 16 public static void displayDirectory(File f) { 17 // 顯示目錄內檔案 18 File[] list = f.listFiles(); 19 for(int i=0; i<list.length; i++) { 20 if(list[i].isDirectory()) 21 System.out.println("<" + list[i].getName() + ">"); 22 else 23 System.out.println(list[i].getName()); 24 } 25 } 26 }
執行結果
練習題 請改寫 TestFile2 使得檔案名稱的顯示類似 “dir /w” 的格式。 請寫一個程式 Dir.java,使得使用者可以利用 ”java Dir dir1 dir2 … dirN” 的指令來顯示目錄 dir1、dir2、…、dirN 內的檔案名稱。如果某個目錄不存在(利用 exists()方法),該目錄輸出的部份會顯示”目錄不存在”。 請寫一個程式 Copy.java,使得使用者可以利用 ”java Copy file1 file2” 的指令將 file1 複製到 file2。如果 file2 已經存在,程式必須問使用者要覆蓋原檔案、另外取名、或者取消。由於我們不知道 file1 是不是文字檔,因此我們建議你使用位元組串流相關類別(也就是FileInputStream/FileOutputStream 的相關類別)來複製,複製的方式就是讀一個位元組進來,就寫一個位元組出去。
JFileChooser 類別 java.swing.JFileChooser 是一個 Swing 的類別,該類別提供使用者一個圖型介面來選擇檔案。產生 JFileChooser 物件的方式如下: JFileChooser fc = new JFileChooser(); 預設的目錄:Windows 是”我的文件”,而 Unix 是使用者的”家目錄”(home directory) 利用 fc.showOpenDialog(null); 則會出現如下畫面的檔案開啟視窗讓使用者選擇檔案
JFileChooser 類別
範例 假設我們希望修改 TestFile1 的程式,使得使用者能夠利用檔案開啟視窗在目前這個目錄內選擇某個文字檔,而且在開啟該檔案後,程式會將檔案的內容呈現在視窗上
為了不必要的、且非常快速的產生”孤兒”物件,一般會使用 StringBuffer 類別。 之前的作法是:每次處理完一個字串就立即輸出;而這裡的作法是先累積到一個字串,然後一次輸出。哪一種好?
執行結果
程式說明 一旦檔案選擇完畢,由以上執行畫面所示,使用者不是選擇右下角的”開啟”按鈕,就是”取消”按鈕。如果使用者選擇”取消”按鈕,則 showOpenDigloa() 方法回傳一個 JFileChooser.CANCEL_OPTION 反之,如果使用者點選”開啟”按鈕,則showOpenDigloa() 方法回傳一個 JFileChooser.APPROVE_OPTION
執行畫面 本範例在使用者選擇了TestFile2.java之後的執行畫面如右:
顯示特定副檔名的檔案 要使得檔案開啟視窗只顯示擁有特定副檔名的檔案,我們需要藉助 使用方式如下: javax.swing.filechooser.FileNameExtensionFilter 類別 使用方式如下: import javax.swing.*; import javax.swing.filechooser.*;
顯示擁有特定副檔名 呼叫 fc.showOpenDialog() 之前,將副檔名過濾器設定到 fc 物件,設定方式: FileNameExtensionFilter filter = new FileNameExtensionFilter("Java 原始碼", "java"); fc.setFileFilter(filter);
執行結果
FileNameExtensionFilter 第一個引數為顯示副檔名的描述 第二個引數為副檔名;例如要顯示所有副檔名為 .java 的檔案,第二個引數就設定為 ”java” 如果某一種檔案型態的副檔名有兩種以上,我們就將可能出現的副檔名,全部放在包含第二個引數的後面,而且每一個引數之間以逗點分開,用法如下: FileNameExtensionFilter filter = new FileNameExtensionFilter(“網頁”, “html”, “htm”);
練習題 請修改 TestFile3.java 修改成開啟檔案視窗只顯示副檔名為 html 或者 htm 的檔案,然後把使用者選擇的網頁內容顯示在螢幕上。 請修改 TestFileReader,使得 使用視窗介面來選擇檔案,而不是預設的 person.dat 使用視窗介面來選擇輸出檔案的名稱,而不是預設的 salary.dat(你開始使用 API 了嗎?)