第17回のTask.javaと同じフォルダに、次のTaskApp.javaを保存します。長い例ですが、読み込み、保存、操作の三つに分けて追ってください。
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.io.IOException;
import java.util.*;
public class TaskApp {
static List<Task> load(Path path) throws IOException {
if (Files.notExists(path)) return new ArrayList<>();
var tasks = new ArrayList<Task>();
var ids = new HashSet<Integer>();
for (String line : Files.readAllLines(path, StandardCharsets.UTF_8)) {
String[] parts = line.split("\t", -1);
if (parts.length != 3 || !(parts[1].equals("0") || parts[1].equals("1"))) {
throw new IllegalArgumentException("保存形式が不正です");
}
Task task = new Task(Integer.parseInt(parts[0]), parts[2], parts[1].equals("1"));
if (!ids.add(task.id())) throw new IllegalArgumentException("IDが重複しています");
tasks.add(task);
}
return tasks;
}
static void save(Path path, List<Task> tasks) throws IOException {
Path target = path.toAbsolutePath();
Path temp = Files.createTempFile(target.getParent(), ".tasks-", ".tmp");
try {
var lines = tasks.stream()
.map(t -> t.id() + "\t" + (t.done() ? "1" : "0") + "\t" + t.title())
.toList();
Files.write(temp, lines, StandardCharsets.UTF_8);
Files.move(temp, target, StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
} finally {
Files.deleteIfExists(temp);
}
}
static void run(String[] args) throws IOException {
if (args.length < 2) throw new IllegalArgumentException("使い方: TaskApp 保存先 add|list|done [値]");
Path path = Path.of(args[0]);
List<Task> tasks = load(path);
switch (args[1]) {
case "list" -> {
if (args.length != 2) throw new IllegalArgumentException("listに値は不要です");
for (Task t : tasks) System.out.println(t.id() + " [" + (t.done() ? "x" : " ") + "] " + t.title());
}
case "add" -> {
if (args.length != 3) throw new IllegalArgumentException("題名を一つ指定します");
int max = tasks.stream().mapToInt(Task::id).max().orElse(0);
int id = Math.addExact(max, 1);
tasks.add(new Task(id, args[2], false));
save(path, tasks);
System.out.println("追加: " + id);
}
case "done" -> {
if (args.length != 3) throw new IllegalArgumentException("IDを一つ指定します");
int id = Integer.parseInt(args[2]);
int index = -1;
for (int i = 0; i < tasks.size(); i++) if (tasks.get(i).id() == id) index = i;
if (index < 0) throw new IllegalArgumentException("IDがありません");
tasks.set(index, tasks.get(index).complete());
save(path, tasks);
System.out.println("完了: " + id);
}
default -> throw new IllegalArgumentException("不明なコマンドです");
}
}
public static void main(String[] args) {
try { run(args); }
catch (IOException | IllegalArgumentException | ArithmeticException e) {
System.err.println("エラー: " + e.getMessage());
System.exit(1);
}
}
}
javac -encoding UTF-8 Task.java TaskApp.java でコンパイルします。続いて java TaskApp tasks.tsv add "Javaを復習"、java TaskApp tasks.tsv done 1、java TaskApp tasks.tsv list を実行すると、ID1が完了状態で表示されます。
読み込みで不正な行やID重複があれば停止します。保存は同じディレクトリの一時ファイルへ書いてから置き換えます。ATOMIC_MOVEに対応しない環境ではエラーとして止めます。置き換え時の既存ファイルの扱いにはファイルシステム差があるため、対象環境で第19回の検証を行ってください。これは停電時の完全な耐久性や、複数プロセスの競合解決を保証する実装ではありません。
練習と解答
練習:doneに存在しない999を指定したとき、元のファイルは変わるでしょうか。
解答:ID検査で例外になり、saveへ到達しないので変わりません。成功メッセージは保存の後にだけ出します。この順番により、「保存できなかったのに追加成功と表示する」誤りを避けています。
公式資料
Files.move APIを参照できます。
Java全20回の目次 | 前の回 | 次の回