12日目のゴールとテーマ
12日目のテーマは
「タスクをファイルに保存して、“終了しても消えないアプリ”にする」 です。
ここまでのタスク管理アプリは、
起動している間はちゃんと動きますが、
アプリを終了すると、タスクはすべて消えてしまいました。
今日はここに、
タスクをファイルに書き出す
起動時にファイルからタスクを読み込む
という「保存」と「読み込み」の仕組みを足して、
一気に“アプリらしさ”を上げていきます。
キーワードはFile、FileWriter、FileReader、BufferedReader、そして try-with-resources です。
「保存する」とは何をすることか
メモリから「外の世界」に出す
今までのタスクは、ArrayList<Task> の中にだけ存在していました。
これは「メモリの中だけの世界」です。
アプリを終了すると、メモリの中身は消えます。
「保存する」というのは、
このメモリの中の情報を、
ファイルなどの「外の世界」に書き出すことです。
そして「読み込む」は、
そのファイルから情報を読み取り、
もう一度 ArrayList<Task> に復元することです。
今日は、いちばんシンプルな形として、
テキストファイル(.txt)に
「1行1タスク」の形式で保存する方法を使います。
どんな形式で保存するかを決める
人間にも読める「区切り付きテキスト」にする
まず、「Task をどう文字列にするか」を決める必要があります。
Task は今、
タイトル(String)
完了フラグ(boolean)
優先度(int)
を持っています。
これを、例えばこんな1行にします。
タイトル\tdoneフラグ\t優先度
具体例でいうと、こうです。
Javaの勉強をする false 3買い物に行く true 1
ここで \t は「タブ文字」です。
タブやカンマなど、何か1つ「区切り文字」を決めておくと、
あとで読み込むときに分解しやすくなります。
今日はタブ区切りで進めます。
Taskを「保存用の1行」に変換するメソッド
クラス自身に「自分を文字列にする方法」を持たせる
Task クラスに、
「自分を保存用の1行に変換する」メソッドを追加します。
public class Task {
private String title;
private boolean done;
private int priority;
public Task(String title, int priority) {
setTitle(title);
setPriority(priority);
this.done = false;
}
public String getTitle() {
return this.title;
}
public boolean isDone() {
return this.done;
}
public int getPriority() {
return this.priority;
}
public void setTitle(String title) {
if (title == null || title.isEmpty()) {
System.out.println("タイトルが空です。変更を無視します。");
return;
}
this.title = title;
}
public void setPriority(int priority) {
if (priority < 1 || priority > 3) {
System.out.println("優先度は1〜3で指定してください。変更を無視します。");
return;
}
this.priority = priority;
}
public void complete() {
this.done = true;
}
public void printWithNumber(int number) {
String status = done ? "完了" : "未完了";
String priorityLabel = toPriorityLabel(priority);
System.out.println(number + ": " + title + " / 状態: " + status + " / 優先度: " + priorityLabel);
}
private String toPriorityLabel(int priority) {
if (priority == 1) {
return "低";
} else if (priority == 2) {
return "中";
} else if (priority == 3) {
return "高";
} else {
return "不明";
}
}
public String toSaveLine() {
return title + "\t" + done + "\t" + priority;
}
}
JavatoSaveLine がポイントです。
タイトル、done、priority を
タブ区切りで1つの文字列にしています。
このメソッドを持っておくことで、
TaskManager 側は「保存形式の細かいこと」を知らなくてよくなります。
保存用の1行からTaskを復元するメソッド
「文字列を分解してTaskを作る」
今度は逆に、
ファイルから1行読み込んだ文字列から
Task を復元するメソッドを作ります。
これは Task クラスの「静的メソッド」として作るのが自然です。
public static Task fromSaveLine(String line) {
String[] parts = line.split("\t");
if (parts.length != 3) {
System.out.println("不正な行を読み飛ばしました: " + line);
return null;
}
String title = parts[0];
boolean done = Boolean.parseBoolean(parts[1]);
int priority;
try {
priority = Integer.parseInt(parts[2]);
} catch (NumberFormatException e) {
System.out.println("優先度の値が不正です。行を読み飛ばします: " + line);
return null;
}
Task task = new Task(title, priority);
if (done) {
task.complete();
}
return task;
}
Javaここでの重要ポイントを深掘りします。
public static Task fromSaveLine(String line)
static にすることで、
「Task のインスタンスがなくても呼べる」メソッドになります。
「1行の文字列から Task を作る工場」のようなイメージです。
line.split("\t")
タブで文字列を分割して、配列にしています。parts[0] がタイトル、parts[1] が done、parts[2] が priority です。
Boolean.parseBoolean(parts[1])
“true” なら true、”false” なら false になります。
Integer.parseInt(parts[2])
文字列を int に変換します。
ここで数字でないものが来ると NumberFormatException が出るので、
try / catch で囲んでいます。
不正な行は null を返して読み飛ばす
→ 「ファイルが少し壊れていても、読めるところだけ読む」
という方針です。
TaskManagerに「保存」と「読み込み」を追加する
ファイル名を1つ決めて、そこに全部書き出す
次に、TaskManager に
「タスク一覧をファイルに保存する」
「ファイルからタスク一覧を読み込む」
メソッドを追加します。
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
public class TaskManager {
private ArrayList<Task> tasks;
private String fileName;
public TaskManager(String fileName) {
this.tasks = new ArrayList<>();
this.fileName = fileName;
}
public void addTask(String title, int priority) {
Task task = new Task(title, priority);
tasks.add(task);
System.out.println("タスクを追加しました。");
}
public void printAllTasks() {
if (tasks.isEmpty()) {
System.out.println("タスクは登録されていません。");
return;
}
System.out.println("=== すべてのタスク ===");
for (int i = 0; i < tasks.size(); i++) {
Task task = tasks.get(i);
task.printWithNumber(i + 1);
}
}
public void saveToFile() {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
for (Task task : tasks) {
String line = task.toSaveLine();
writer.write(line);
writer.newLine();
}
System.out.println("タスクをファイルに保存しました。");
} catch (IOException e) {
System.out.println("ファイルへの保存中にエラーが発生しました: " + e.getMessage());
}
}
public void loadFromFile() {
tasks.clear();
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line;
int count = 0;
while ((line = reader.readLine()) != null) {
Task task = Task.fromSaveLine(line);
if (task != null) {
tasks.add(task);
count++;
}
}
System.out.println(count + "件のタスクを読み込みました。");
} catch (IOException e) {
System.out.println("ファイルの読み込み中にエラーが発生しました(初回起動かもしれません): " + e.getMessage());
}
}
// ほかのメソッド(printIncompleteTasks, printCompletedTasks, completeTask, removeTask)は前日までと同じ
}
Javaここでの重要ポイントを深掘りします。
private String fileName;
TaskManager が「どのファイルに保存するか」を知っています。
Main からファイル名を渡してもらう形にしています。
saveToFileBufferedWriter と FileWriter を使って、
1タスク1行で書き出しています。
writer.newLine();
行の区切りを入れています。
これで、読み込み時に readLine() で1行ずつ読めます。
try ( ... ) { ... }
これが「try-with-resources」です。
括弧の中で作った writer は、
ブロックを抜けるときに自動的に close() されます。
ファイルを開きっぱなしにしないための仕組みです。
loadFromFiletasks.clear(); で一度リストを空にしてから読み込みます。readLine() で1行ずつ読み、Task.fromSaveLine(line) で Task に変換しています。
null の場合(不正な行)は読み飛ばします。
catch (IOException e)
ファイルが存在しない、権限がない、などのときに来ます。
「初回起動かもしれません」とメッセージを出して、
アプリを落とさずに続行しています。
Mainクラスで「起動時に読み込み」「終了時に保存」
アプリのライフサイクルに「保存」と「読み込み」を組み込む
最後に、Main クラス側で
TaskManager の loadFromFile と saveToFile を呼び出します。
import java.util.Scanner;
public class Main {
public static void printMenu() {
System.out.println("=== タスク管理アプリ(12日目版) ===");
System.out.println("1: タスクを追加する");
System.out.println("2: すべてのタスクを表示する");
System.out.println("3: 未完了のタスクだけ表示する");
System.out.println("4: 完了したタスクだけ表示する");
System.out.println("5: タスクを完了にする");
System.out.println("6: タスクを削除する");
System.out.println("7: 手動で保存する");
System.out.println("0: 終了する(自動保存)");
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
TaskManager manager = new TaskManager("tasks.txt");
manager.loadFromFile();
while (true) {
printMenu();
int choice = InputHelper.readInt(scanner, "番号を選んでください: ");
if (choice == 0) {
manager.saveToFile();
System.out.println("アプリを終了します。");
break;
} else if (choice == 1) {
System.out.print("タスクのタイトルを入力してください: ");
String title = scanner.nextLine();
int priority = InputHelper.readInt(scanner, "優先度を入力してください(1:低, 2:中, 3:高): ");
manager.addTask(title, priority);
} else if (choice == 2) {
manager.printAllTasks();
} else if (choice == 3) {
manager.printIncompleteTasks();
} else if (choice == 4) {
manager.printCompletedTasks();
} else if (choice == 5) {
int number = InputHelper.readInt(scanner, "完了にするタスクの番号を入力してください: ");
manager.completeTask(number);
} else if (choice == 6) {
int number = InputHelper.readInt(scanner, "削除するタスクの番号を入力してください: ");
manager.removeTask(number);
} else if (choice == 7) {
manager.saveToFile();
} else {
System.out.println("その番号は無効です。");
}
System.out.println();
}
}
}
Javaここでの流れを整理します。
アプリ起動時に manager.loadFromFile(); を呼ぶ
→ もしファイルがあれば、前回のタスクを読み込む
→ なければ「初回起動」として空の状態で始める
アプリ終了時(choice == 0)に manager.saveToFile(); を呼ぶ
→ 現在のタスク一覧をファイルに書き出す
→ 次回起動時に復元できる
これで、「終了してもタスクが残る」アプリになりました。
ファイル入出力でつまずきやすいポイント
パス・文字コード・例外
ファイル入出力は、初心者がつまずきやすいポイントがいくつかあります。
まず「ファイルの場所」です。"tasks.txt" と書いた場合、
基本的には「プログラムを実行している場所」に作られます。
IDE(Eclipse や IntelliJ)を使っていると、
実行ディレクトリがどこか分かりにくいことがあります。
最初は「同じフォルダにできているんだな」くらいの理解でOKです。
次に「文字コード」です。
日本語を含むテキストを扱うとき、
環境によっては文字化けすることがあります。
本格的にやるなら OutputStreamWriter に文字コードを指定したりしますが、
12日目の段階では「そういう問題もあるんだな」と知っておけば十分です。
最後に「例外」です。
ファイル入出力は必ず IOException が絡みます。
try-with-resources と catch (IOException e) のセットは、
ほぼテンプレとして覚えてしまって構いません。
12日目で一番大事な感覚
「アプリに“記憶”が宿る瞬間」
今日あなたに持ってほしい感覚はこれです。
ファイルに保存して、
次回起動時に読み込めるようにした瞬間、
アプリに「記憶」が宿ちます。
それまでは、
起動するたびに真っさらな世界でした。
でも今は、
昨日の自分が登録したタスクを、
今日の自分が引き継いで見ることができます。
クラスで「登場人物」を作り、
TaskManager で「世界のルール」を作り、
ファイル入出力で「時間をまたぐ記憶」を持たせる。
ここまで来ると、
もう立派に「アプリを作っている」と言っていい段階です。
12日目のまとめと、13日目への予告
今日やったことを短く整理すると、
Task に「保存用の1行に変換する」メソッド toSaveLine を追加した
Task に「1行から復元する」静的メソッド fromSaveLine を追加した
TaskManager に saveToFile と loadFromFile を追加して、タスク一覧をファイルに出し入れできるようにした
try-with-resources でファイルを安全に開閉する形を体験した
Main で「起動時に読み込み」「終了時に保存」という流れを組み込んだ
13日目は、
ここまで作ってきた Java の世界を一度整理しつつ、
「他のクラスライブラリを使う」「パッケージで整理する」など、
もう一段階“現場寄り”の話に触れていきます。
2週間コースのラストに向けて、
「自分はもう Java でアプリを作れるんだ」という実感を、
さらに強くしていきましょう。
