National Yunlin University of Science and Technology
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

雲林科技大學 AQI 56 27°C PM2.5 10
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 15

檔案處理

讀寫文字檔、try-with-resources、Files 類別
Interaction實作 25 minute(s)

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

1.

下面的程式執行結果是什麼?

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");
    }
}
2.

下面的程式會印出什麼?

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")));
    }
}
3.

下面的程式會印出什麼?

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());
    }
}
第 1 次作答

逐行讀取: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,迴圈結束。

想一想:讀取檔案

1.

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);
    }
}
2.

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);
    }
}
3.

用 try-with-resources 開啟 BufferedReader,在 try 區塊中間拋出了例外並被 catch 接住。這個 BufferedReader 會怎樣?

第 1 次作答

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

處理每一行的步驟:

  1. line.split(",") 切成字串陣列,例如 ["Amy", "88"]
  2. Integer.parseInt(parts[1].trim()) 把分數轉成整數;trim() 去掉前後空白,避免 "88 " 轉換失敗拋出 NumberFormatException
  3. 迴圈從索引 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、printf
  • BufferedReader.readLine() 一次讀一行,讀到檔案結尾回傳 null;Scanner(new File(…)) 適合讀數字與單字
  • Files.readAllLines、Files.readString、Files.writeString 與 Path.of:一行讀寫整個小檔案,不用自己 close
  • 讀 CSV:split(",") 切欄位、Integer.parseInt 轉數字、跳過標題行
這一課完成了!
Next lesson:多執行緒
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。