Java | 1 日 90 分 × 7 日アプリ学習 初級編:ファイル保存アプリ

Web APP Java
スポンサーリンク

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レコード」「区切り文字で項目を持たせる」 です。

ここが腹に落ちていれば、
ファイル保存はもう「ただのテキスト」ではなく、
「自分で設計した小さなデータストア」
として扱えるようになっていきます。

タイトルとURLをコピーしました