3日目のゴール
3日目のテーマは
「ただの“メモの列”から、“意味のある1レコード”に進化させる」 ことです。
1〜2日目で、あなたはこういう感覚をつかみました。
ファイルに文字を書けば、プログラム終了後も残る
追記モードで、メモが行としてどんどん溜まっていくreadLine() のループで、全行を読み出せる
3日目では、ここから一歩進んで、
「1行=1つのデータ(レコード)」として扱う
行の中に“項目”を持たせる(ID、タイトル、本文など)
ファイルを「ただのテキスト」から「簡易データベース」に近づける
というところまで行きます。
「1行=1レコード」という発想を持つ
行を“ただの文章”から“構造のあるデータ”に変える
2日目までの memo.txt は、
こんな感じの中身でしたね。
今日はカレーを食べた
明日は映画を観に行く
Java の勉強を続ける
これは「1行=1メモ」ではありますが、
行の中に「項目」はありません。
3日目では、
1行をこんなふうにしてみます。
1|2026-06-06|今日はカレーを食べた
2|2026-06-07|明日は映画を観に行く
3|2026-06-08|Java の勉強を続ける
ここには、3つの項目があります。
先頭の数字 → メモID
次 → 日付
最後 → メモ本文
このように、
「1行の中に、意味のある“区切り”を持たせる」
というのが、今日の一番大事な発想です。
区切り文字(デリミタ)という考え方
上の例では、
項目の区切りに |(縦棒)を使いました。
このような「項目と項目の間に入れる記号」を
区切り文字(デリミタ) と呼びます。
よく使われるのは、
カンマ , → CSV(カンマ区切り)
タブ \t → TSV(タブ区切り)
縦棒 | → よくある簡易フォーマット
などです。
今日は、見た目が分かりやすくて、
メモ本文にもあまり使われなさそうな | を使います。
今日の題材:ID+日付+本文を保存するミニアプリ
何を作るか
2日目の「メモ履歴アプリ」を、
少しだけ“構造化”します。
メニューはこうです。
「1: メモを追加」
「2: メモ一覧を表示」
「0: 終了」
保存するときは、
次のIDを自動で振る(1, 2, 3, …)
今日の日付を自動で入れる
本文はユーザーが入力する
読み込むときは、
1行を「ID」「日付」「本文」に分解して表示する
これで、
「ファイルを、簡単な“テーブル”として扱う感覚」
をつかみに行きます。
行のフォーマットを決める:まずは“仕様”から
1行の形を言葉で決める
コードを書く前に、
1行のフォーマットを言葉で決めてしまいましょう。
「1行は、こういう形にする」
ID|日付|本文
ID は整数(1, 2, 3, …)
日付は文字列(例: 2026-06-06)
本文は、ユーザーが入力したメモ
この「仕様」を先に決めることが、
実はめちゃくちゃ大事です。
仕様が決まれば、
保存するとき → この形で書く
読み込むとき → この形を前提に分解する
というコードが書けるようになります。
日付はどうやって作るか(シンプル版)
本当は LocalDate などを使うのが王道ですが、
3日目では、
まずは「文字列としての日付」を使います。
例えば、2026-06-06 のような形です。
ここでは、
日付の生成は一旦「手書き」でも構いません。
後で LocalDate.now() に差し替えることもできます。
ID を自動採番する:ファイルから“最後のID”を読む
なぜ ID が必要なのか
ID があると、
こんなことができるようになります。
「3番のメモだけ削除したい」
「5番のメモだけ編集したい」
つまり、
「特定の行を指させる」 ようになります。
そのために、
保存するときに「次のID」を振る必要があります。
次のIDを決める基本戦略
次のIDを決めるには、
こう考えます。
ファイルを全部読む
最後の行のIDを取り出す
それに 1 を足す
ファイルが空なら、
最初のIDは 1 にする。
これをコードにすると、
こんな感じになります。
private static int getNextId() {
int lastId = 0;
try (BufferedReader reader = new BufferedReader(new FileReader(FILE_NAME))) {
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split("\\|", 3);
if (parts.length >= 1) {
try {
lastId = Integer.parseInt(parts[0]);
} catch (NumberFormatException e) {
// ID が数字でない行は無視(簡易版)
}
}
}
} catch (IOException e) {
// ファイルがない、読めない場合は lastId = 0 のまま扱う
}
return lastId + 1;
}
Javaここでの重要ポイントを丁寧に見ていきます。
split で「1行を項目に分解する」
String.split の基本
line.split("\\|", 3) は、
「文字列を区切り文字で分割する」 メソッドです。
第一引数 "\\|" は、
「| で区切ってね」という意味です。
ここで \\| と書いているのは、split の引数が「正規表現」だからです。
| は正規表現の中で特別な意味を持つ記号なので、
「ただの縦棒として扱ってね」と伝えるために\\| とエスケープしています。
第二引数 3 は、
「最大3つに分割してね」という意味です。
つまり、
"1|2026-06-06|今日はカレー"
→ ["1", "2026-06-06", "今日はカレー"]
というふうに分解されます。
ID だけ取り出す
getNextId の中では、
ID だけが欲しいので、parts[0] を見ています。
if (parts.length >= 1) {
try {
lastId = Integer.parseInt(parts[0]);
} catch (NumberFormatException e) {
// ID が数字でない行は無視
}
}
Javaここでやっていることは、
行の先頭が数字なら、それを ID として採用する
数字でなければ、その行は「IDなし」とみなして無視する
という簡易ルールです。
3日目版:ID+日付+本文を保存・表示するアプリ
全体のコード
ここまでの部品を組み合わせた、
3日目の「ファイル保存アプリ」です。
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Scanner;
public class FileSaveAppDay3 {
private static final String FILE_NAME = "memo.txt";
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println();
System.out.println("=== ファイル保存アプリ 3日目 ===");
System.out.println("1: メモを追加(ID+日付+本文)");
System.out.println("2: メモ一覧を表示");
System.out.println("0: 終了");
System.out.print("番号を選んでください: ");
String choice = scanner.nextLine();
if ("0".equals(choice)) {
System.out.println("終了します。");
break;
} else if ("1".equals(choice)) {
addMemo(scanner);
} else if ("2".equals(choice)) {
showAllMemos();
} else {
System.out.println("不正な入力です。0, 1, 2 のいずれかを入力してください。");
}
}
scanner.close();
}
private static void addMemo(Scanner scanner) {
int nextId = getNextId();
System.out.print("メモ本文を1行で入力してください: ");
String body = scanner.nextLine();
if (body.isEmpty()) {
System.out.println("空のメモは保存しません。");
return;
}
String date = "2026-06-06"; // とりあえず固定。後で LocalDate.now() に差し替え可能
String line = nextId + "|" + date + "|" + body;
try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_NAME, true))) {
writer.write(line);
writer.newLine();
System.out.println("メモを追加しました。ID: " + nextId);
} catch (IOException e) {
System.out.println("メモの保存に失敗しました。");
System.out.println("(開発者向け情報)" + e.getMessage());
}
}
private static int getNextId() {
int lastId = 0;
try (BufferedReader reader = new BufferedReader(new FileReader(FILE_NAME))) {
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split("\\|", 3);
if (parts.length >= 1) {
try {
lastId = Integer.parseInt(parts[0]);
} catch (NumberFormatException e) {
// ID が数字でない行は無視
}
}
}
} catch (IOException e) {
// ファイルがない、読めない場合は lastId = 0 のまま扱う
}
return lastId + 1;
}
private static void showAllMemos() {
try (BufferedReader reader = new BufferedReader(new FileReader(FILE_NAME))) {
String line;
boolean hasLine = false;
System.out.println("=== メモ一覧 ===");
while ((line = reader.readLine()) != null) {
hasLine = true;
String[] parts = line.split("\\|", 3);
if (parts.length == 3) {
String id = parts[0];
String date = parts[1];
String body = parts[2];
System.out.println("ID: " + id + " / 日付: " + date + " / 本文: " + body);
} else {
// 想定外の形式の行は、そのまま表示(簡易版)
System.out.println("(形式不正) " + line);
}
}
if (!hasLine) {
System.out.println("(まだメモが保存されていません)");
}
System.out.println("=== 一覧表示終了 ===");
} catch (IOException e) {
System.out.println("メモの読み込みに失敗しました。");
System.out.println("(開発者向け情報)" + e.getMessage());
}
}
}
Java重要な部分を深掘りして説明する
「1行を“仕様どおりに組み立てる”」という意識
String line = nextId + "|" + date + "|" + body;
Javaこの一行は、
「仕様どおりに1レコードを組み立てる」 という意味です。
仕様はこうでした。
ID|日付|本文
だから、
ID → nextId
日付 → date
本文 → body
を | でつないで、
ひとつの文字列にしています。
ここで大事なのは、
「行の中身にルールがある」という意識です。
ルールがあるからこそ、
読み込むときに split で分解できる。
ルールがないと、
「ただの文章」としてしか扱えない。
この「ルールを決める → ルールどおりに書く → ルールどおりに読む」
という流れが、
ファイル永続化の本質にかなり近いです。
読み込み側は「仕様を信じて分解する」
String[] parts = line.split("\\|", 3);
if (parts.length == 3) {
String id = parts[0];
String date = parts[1];
String body = parts[2];
System.out.println("ID: " + id + " / 日付: " + date + " / 本文: " + body);
} else {
System.out.println("(形式不正) " + line);
}
Javaここでは、
「このファイルは、ID|日付|本文 の形式で書かれているはずだ」
という前提(仕様)を信じて、split で分解しています。
parts.length == 3 でない行は、
「仕様どおりではない」と判断して、
そのまま表示しています。
本番アプリなら、
こういう行はログに出したり、
エラーとして扱ったりします。
3日目では、
「仕様どおりに書くと、仕様どおりに読める」
という感覚をつかむのが目的なので、
ここは軽めに扱っています。
3日目で“体に入れてほしい感覚”
ファイルは「ただのテキスト」だけど、「意味のあるテキスト」にできる
2日目までのファイルは、
「ただの行の集まり」でした。
3日目のファイルは、
「意味のある行の集まり」 になっています。
1行=1レコード
レコードの中に項目(ID、日付、本文)がある
項目の区切りには、決めた記号(|)を使う
この構造を意識できると、
ファイルは一気に「データベースっぽいもの」に近づきます。
もちろん、
本物のデータベースとは違いますが、
考え方の軸はかなり似ています。
永続化は「データの形を決めるところから始まる」
今日の流れを振り返ると、
1行の仕様を決める(ID|日付|本文)
その仕様どおりに書く
その仕様どおりに読む
という順番でした。
この順番は、
どんな永続化でもほぼ同じです。
JSON に保存するときも
CSV に保存するときも
データベースに保存するときも
まず「データの形(構造)」を決める。
その形どおりに書く。
その形どおりに読む。
3日目でやったことは、
その一番小さい版です。
次につながる話を少しだけ
今日は、
1行を「ID+日付+本文」という“レコード”にした
区切り文字(|)で項目を分けたsplit で行を分解して、項目ごとに表示した
ファイルから「最後のID」を読んで、次のIDを自動採番した
ところまで来ました。
この先は、例えばこんな方向に進めます。
日付を本物の LocalDate から生成する
ID を削除・編集に使う(特定行の操作)
行の形式が崩れたときの例外処理をちゃんと設計する
でも、その土台になるのは、
今日やった 「1行=1レコード」「区切り文字で項目を持たせる」 です。
ここが腹に落ちていれば、
ファイル保存はもう「ただのテキスト」ではなく、
「自分で設計した小さなデータストア」
として扱えるようになっていきます。

