Learning Objectives
- 說明相對路徑與絕對路徑,用 FileWriter 以覆寫或附加模式寫入文字檔
- 說明為什麼寫完檔案一定要 close,用 try-with-resources 讓資源自動關閉
- 用 BufferedReader 逐行讀取文字檔,以 readLine() 回傳 null 判斷檔案結束;用 Scanner 讀取檔案裡的資料
- 說明 IOException 是檢查型例外,必須用 try-catch 接住或在方法宣告 throws
- 用 Files 與 Path 讀寫整個檔案,讀取 CSV 檔並計算平均
為什麼要讀寫檔案
到目前為止,程式裡的資料都放在記憶體:程式一結束,變數、陣列、ArrayList 裡的東西就全部消失。
要把資料保存下來(成績紀錄、設定、日誌),或讀進別人準備好的資料(老師給的成績表、網站匯出的 CSV),就要讀寫檔案。
本課只處理文字檔:內容是一行一行的文字,可以用記事本打開,例如 .txt、.csv。
路徑(path)告訴程式檔案在哪裡:
| 寫法 | 意思 |
|---|---|
"scores.txt" |
相對路徑:從程式執行時的工作目錄開始找(通常是你執行 java 指令的那個資料夾) |
"data/scores.txt" |
相對路徑:工作目錄底下 data 資料夾裡的檔案 |
"C:\\Users\\amy\\scores.txt" |
Windows 的絕對路徑;字串裡的 \ 要寫成 \\(跳脫字元),也可以直接寫 / |
"/home/amy/scores.txt" |
macOS、Linux 的絕對路徑 |
本課的範例都用相對路徑。逐步執行動畫的輸出區旁邊會出現檔案分頁:程式要讀的檔案,以及程式寫出來的檔案,點分頁就能看到程式結束時檔案的內容。
讀寫檔案要用到的類別分在兩個套件:
java.io:FileWriter、BufferedWriter、PrintWriter、FileReader、BufferedReader、File,從 Java 早期就有java.nio.file:Files、Path,寫法更短,讀寫整個小檔案很方便(本課最後介紹)
FileWriter:覆寫、附加與 close
FileWriter w = new FileWriter("todo.txt"); // 覆寫模式:檔案原本的內容會被清空
FileWriter w = new FileWriter("todo.txt", true); // 附加模式:寫在原本內容的後面
w.write("買牛奶\n"); // 寫入字串;換行要自己寫 \n
w.close(); // 關閉檔案
- 檔案不存在時,
FileWriter會建立新檔案 - 第二個參數
true表示附加(append),寫日誌時常用;不寫或寫false就是覆寫 - 一定要
close():為了效率,寫入的文字會先放在記憶體的緩衝區(buffer),累積夠多才一次寫進磁碟。close()會先把緩衝區剩下的內容寫出去(flush),再關閉檔案。忘了close,程式結束時緩衝區裡的內容就不見了,檔案可能是空的 - 每個開著的檔案都佔用作業系統的資源,用完要關閉,也是為了讓其他程式能使用這個檔案
寫入、附加與忘記 close
右邊每個 FileWriter 物件會顯示它開啟的檔名,呼叫 close() 之後標示「已關閉」。播完之後點輸出旁邊的 todo.txt、note.txt、lost.txt 分頁:todo.txt 有三行、note.txt 只剩第二版,最後的 lost.txt 沒有呼叫 close(),內容是空的。
檢查型例外與 try-with-resources
開啟檔案可能失敗:路徑不存在、沒有寫入權限、磁碟滿了。這些情況會拋出 IOException,它是檢查型例外(checked exception,第 12 課):
呼叫可能拋出它的方法時,一定要處理,否則編譯錯誤 unreported exception IOException; must be caught or declared to be thrown。處理方式有兩種:
// 方式一:在方法宣告 throws,交給呼叫的人處理(main 宣告 throws,例外發生時程式中止並印出訊息)
public static void main(String[] args) throws IOException { … }
// 方式二:用 try-catch 接住
try {
FileWriter w = new FileWriter("a.txt");
…
} catch (IOException e) {
System.out.println("寫檔失敗:" + e.getMessage());
}
用 try-catch 時有個麻煩:如果 write 拋出例外,後面的 close() 就不會執行,檔案沒有關閉。
以前要把 close() 放進 finally,還要再處理 close() 本身拋出的例外,程式很冗長。
Java 7 加入的 try-with-resources 解決了這個問題:在 try 後面的小括號裡開啟資源,不管是正常結束還是拋出例外,離開 try 區塊時都會自動呼叫 close():
try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("report.txt")))) {
out.println("成績報告");
out.printf("%s,%d%n", "Amy", 88);
} // 這裡自動 out.close()
只要類別實作了 AutoCloseable 介面(檔案的讀寫類別都有實作),就能放進小括號;放好幾個資源時用分號隔開,關閉的順序和開啟的順序相反。
上面那行把三個類別包在一起,各有分工:
| 類別 | 作用 |
|---|---|
FileWriter |
真正把文字寫進檔案 |
BufferedWriter |
加上緩衝區,減少寫入磁碟的次數;多了 newLine() 方法 |
PrintWriter |
提供熟悉的 print、println、printf,用法和 System.out 一樣 |
關閉最外層的 PrintWriter,裡面包著的 BufferedWriter、FileWriter 也會一起關閉。
用 try-with-resources 寫報告
out 只在 try 區塊裡存在。看第 17 行到第 21 行之間:離開 try 區塊時 out 自動關閉,變數從左邊消失。播完點 report.txt 分頁,println 和 printf 寫進去的四行都在。
想一想:寫入、覆寫與 close
下面的程式執行結果是什麼?
import java.io.FileWriter;
public class Main {
public static void main(String[] args) {
FileWriter w = new FileWriter("a.txt");
w.write("hello");
w.close();
System.out.println("done");
}
}
下面的程式會印出什麼?
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
FileWriter w = new FileWriter("log.txt");
w.write("A");
w.close();
w = new FileWriter("log.txt", true);
w.write("B");
w.close();
w = new FileWriter("log.txt");
w.write("C");
w.close();
System.out.println(Files.readString(Path.of("log.txt")));
}
}
下面的程式會印出什麼?
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
FileWriter w = new FileWriter("hi.txt");
w.write("hi");
System.out.println(Files.readString(Path.of("hi.txt")).length());
}
}
逐行讀取:BufferedReader 與 Scanner
讀檔最常見的寫法是 BufferedReader 包著 FileReader,用 readLine() 一次讀一行:
try (BufferedReader in = new BufferedReader(new FileReader("poem.txt"))) {
String line;
while ((line = in.readLine()) != null) { // 讀一行;讀到檔案結尾回傳 null
System.out.println(line);
}
}
readLine()回傳的字串不含行尾的換行字元- 讀到檔案結尾時回傳
null,這是迴圈結束的條件。空白行回傳的是空字串"",不是null while ((line = in.readLine()) != null)是常見的簡寫:先把讀到的值指派給line,再拿它和null比較。下面的動畫用比較好懂的寫法:迴圈前先讀一行,迴圈最後再讀下一行- 檔案不存在時,
new FileReader(…)拋出FileNotFoundException,它是IOException的子類別
要讀的是一個一個的數字或單字,用讀鍵盤輸入時用過的 Scanner 比較方便,把 System.in 換成 new File(…) 就好:
try (Scanner sc = new Scanner(new File("numbers.txt"))) {
int sum = 0;
while (sc.hasNextInt()) { // 還有下一個整數嗎?
sum += sc.nextInt(); // 數字之間用空白或換行隔開都可以
}
}
new Scanner(new File(…)) 會拋出 FileNotFoundException,同樣要處理。File 類別代表一個路徑,還可以用 exists() 檢查檔案是否存在、delete() 刪除檔案。
一行一行讀一首詩
要讀的 poem.txt 已經準備好,可以先點輸出旁邊的分頁看內容。注意 line 的值:每呼叫一次 readLine() 就換成下一行,讀完第四行之後再讀一次,line 變成 null,迴圈結束。
想一想:讀取檔案
data.txt 的內容是三行:a、空白行、b。下面的程式會印出什麼?
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
int count = 0;
try (BufferedReader in = new BufferedReader(new FileReader("data.txt"))) {
while (in.readLine() != null) {
count++;
}
}
System.out.println(count);
}
}
nums.txt 的內容是兩行:3 4 和 5。下面的程式會印出什麼?
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class Main {
public static void main(String[] args) throws FileNotFoundException {
Scanner sc = new Scanner(new File("nums.txt"));
int sum = 0;
while (sc.hasNextInt()) {
sum += sc.nextInt();
}
sc.close();
System.out.println(sum);
}
}
用 try-with-resources 開啟 BufferedReader,在 try 區塊中間拋出了例外並被 catch 接住。這個 BufferedReader 會怎樣?
Files 與 Path:一行讀寫整個檔案
java.nio.file 套件的 Files 類別提供 static 方法,讀寫小檔案只要一行,也不必自己 close:
Path p = Path.of("fruits.txt"); // Path 代表一個路徑
Files.writeString(p, "apple\nbanana\n"); // 把整個字串寫進檔案(覆寫)
List<String> lines = Files.readAllLines(p); // 讀出所有行,放進 List
String all = Files.readString(p); // 整個檔案讀成一個字串
boolean ok = Files.exists(p); // 檔案存在嗎?
| 方法 | 加入的版本 |
|---|---|
Files 類別、Path 介面 |
Java 7 |
Files.readAllLines(Path)(不指定編碼的版本) |
Java 8 |
Files.readString、Files.writeString、Path.of |
Java 11 |
- 這些方法同樣會拋出
IOException,要處理 - 預設使用 UTF-8 編碼,中文不會變成亂碼
readAllLines、readString會把整個檔案讀進記憶體,適合小檔案;幾百 MB 的大檔案請用BufferedReader逐行處理- 要附加而不是覆寫:
Files.writeString(p, "…", StandardOpenOption.APPEND)
Files 的寫入與讀取
整支程式沒有 new FileWriter,也沒有 close():Files 的方法自己開啟、讀寫、關閉檔案。看右邊 lines 這個 List 裡的三個元素。
實作:讀 CSV 算平均
CSV(Comma-Separated Values)用逗號分隔欄位,第一行通常是標題,試算表軟體都能匯入匯出:
name,score
Amy,88
Bob,75
處理每一行的步驟:
line.split(",")切成字串陣列,例如["Amy", "88"]Integer.parseInt(parts[1].trim())把分數轉成整數;trim()去掉前後空白,避免"88 "轉換失敗拋出NumberFormatException- 迴圈從索引 1 開始,跳過標題行
真實的 CSV 欄位裡可能有逗號(用雙引號包起來,例如
"台北市,大安區"),這時單純split(",")會切錯。正式專案通常使用現成的 CSV 函式庫。
計算平均分數
先點 scores.csv 分頁看資料。注意第 18 行的 (double) total / n:先把 total 轉成 double 再除,否則整數除法會把小數捨去。
動手寫:訂單統計
orders.csv 的每一行是「品名,單價,數量」。完成程式:算出所有訂單的總金額、找出金額(單價 × 數量)最高的一筆,再用 PrintWriter 把結果寫進 summary.txt。程式最後一行會把 summary.txt 讀出來印在螢幕上,用來檢查有沒有寫成功。
重點回顧
- 相對路徑從工作目錄開始找;Windows 路徑字串裡的
\要寫成\\ new FileWriter(檔名)覆寫、new FileWriter(檔名, true)附加;write不會自動換行- 寫入的內容先放在緩衝區,
close()(或flush())才會寫進檔案;忘了 close,檔案可能是空的 IOException、FileNotFoundException是檢查型例外:用 try-catch 接住,或在方法宣告throws- try-with-resources(Java 7 起):
try (資源) { … }離開區塊時自動 close,就算拋出例外也一樣 PrintWriter包BufferedWriter包FileWriter:有緩衝區,又能用println、printfBufferedReader.readLine()一次讀一行,讀到檔案結尾回傳null;Scanner(new File(…))適合讀數字與單字Files.readAllLines、Files.readString、Files.writeString與Path.of:一行讀寫整個小檔案,不用自己 close- 讀 CSV:
split(",")切欄位、Integer.parseInt轉數字、跳過標題行