Java | Java 詳細・モダン文法:日付・時刻 API – 不変オブジェクトとしての日時

Java Java
スポンサーリンク

「不変な日時」ってどういうことか

まず言葉からいきます。
LocalDateLocalDateTimeZonedDateTime など java.time のクラスは「不変(immutable)」です。
一度作ったインスタンスの中身(年・月・日・時刻・タイムゾーンなど)は、あとから書き換えることができません。

「今日」を表すオブジェクトを作ったら、それは永遠に「今日」のままです。
「明日」が欲しくなったら、「今日」を変えるのではなく、「今日から 1 日足した“新しいオブジェクト”」を作ります。


旧 API(Date / Calendar)が抱えていた「変わってしまう怖さ」

Calendar の例で怖さを体感する

古い Calendar はミュータブル(可変)です。
こんなコードを見てください。

Calendar cal = Calendar.getInstance();
cal.set(2025, Calendar.JANUARY, 18);

Calendar cal2 = cal;
cal2.add(Calendar.DAY_OF_MONTH, 1);

System.out.println(cal.getTime());  // 2025-01-19 が出る
System.out.println(cal2.getTime()); // 2025-01-19
Java

cal2 に 1 日足したつもりが、cal まで一緒に変わっています。
なぜかというと、cal2 = cal; は「同じオブジェクトを指しているだけ」だからです。

この「どこかで誰かが add / set したら、別の場所で使っている日時まで変わってしまう」という性質が、バグの温床になります。
特に、メソッドの引数で Calendar を受け取って、そのまま add したり set したりすると、「呼び出し元の状態まで書き換えてしまう」ことになります。


java.time の日時クラスが不変だと何が嬉しいか

plus しても「元の値」は絶対に変わらない

同じことを LocalDate でやってみます。

LocalDate date = LocalDate.of(2025, 1, 18);

LocalDate date2 = date.plusDays(1);

System.out.println(date);   // 2025-01-18
System.out.println(date2);  // 2025-01-19
Java

plusDays は「自分自身を書き換える」のではなく、「1 日足した新しい LocalDate を返す」メソッドです。
だから、date はずっと 2025-01-18 のまま。

この性質のおかげで、

「このメソッドの中で日時をいじっても、呼び出し元の日時が勝手に変わることはない」

と安心して書けます。

共有しても怖くない(スレッドセーフ)

不変オブジェクトは、いくら共有しても安全です。

LocalDate base = LocalDate.of(2025, 1, 18);

// 複数のメソッドに同じ base を渡しても OK
foo(base);
bar(base);
Java

foo の中で plusDays しようが、bar の中で minusMonths しようが、
どちらも「新しい LocalDate を返す」だけで、base 自体は変わりません。

マルチスレッド環境でも同じです。
複数スレッドが同じ LocalDateTime を参照していても、誰も中身を変えられないので、ロックや同期を気にする必要がありません。


「値オブジェクト」としての日時

日付・時刻は「状態」ではなく「値」として扱う

不変オブジェクトとしての日時は、「値オブジェクト」という考え方と相性がいいです。

「2025-01-18 の 10:00(日本時間)」というのは、「その瞬間」を表す“値”です。
「ユーザーの残高」みたいに、あとから変わっていく“状態”ではありません。

だからこそ、

「この日時は、ある瞬間を表す“値”であって、あとから書き換えるものではない」

という設計にしておくと、コードの意味がとてもクリアになります。

equals / hashCode も「値」として振る舞う

不変な日時オブジェクトは、「同じ日時なら同じ」として扱えます。

LocalDate d1 = LocalDate.of(2025, 1, 18);
LocalDate d2 = LocalDate.of(2025, 1, 18);

System.out.println(d1.equals(d2)); // true
Java

中身が変わらないので、Map のキーや Set の要素としても安心して使えます。
「途中で値が変わって、ハッシュが合わなくなる」といった心配がありません。


メソッドチェーンで「時間の流れ」を安全に表現できる

不変だからこそできる、読みやすいチェーン

不変オブジェクトだと、メソッドチェーンがとても自然に書けます。

LocalDateTime meeting =
        LocalDateTime.of(2025, 1, 18, 10, 0)
                     .plusDays(3)
                     .withHour(15)
                     .minusMinutes(30);
Java

これは、

「2025-01-18 10:00 から始めて、
3 日足して、
時刻を 15 時に変えて、
そこから 30 分引く」

という“時間の編集”を、そのままコードで表現しています。

各ステップは「新しい LocalDateTime を返す」だけなので、
途中のどこかで「元の値が壊れる」ことはありません。

古い API だとこうなる

同じことを Calendar でやると、こうなります。

Calendar cal = Calendar.getInstance();
cal.set(2025, Calendar.JANUARY, 18, 10, 0);
cal.add(Calendar.DAY_OF_MONTH, 3);
cal.set(Calendar.HOUR_OF_DAY, 15);
cal.add(Calendar.MINUTE, -30);
Java

やっていることは同じですが、

  • すべて同じオブジェクトを書き換えている
  • 途中の状態を別のメソッドに渡すときに注意が必要

という違いがあります。
不変オブジェクトだと、「編集の過程」も「最終結果」も、安心して扱えます。


設計として一番大事なポイント:「どこでも安心して渡せる」

「このメソッドに渡したら壊されるかも」という不安が消える

ミュータブルな日時オブジェクトだと、メソッドのシグネチャを見ただけでは不安が残ります。

void adjust(Calendar cal) {
    cal.add(Calendar.DAY_OF_MONTH, 1); // 呼び出し元の cal も変わる
}
Java

呼び出し側は、「このメソッドは cal を破壊するのか?」を実装を読まないと分かりません。

不変な日時オブジェクトなら、こうです。

LocalDate adjust(LocalDate date) {
    return date.plusDays(1); // 引数は変えず、新しい日付を返す
}
Java

シグネチャだけで、「引数は壊さない」「戻り値で結果を返す」ということが分かります。
これは設計としてものすごく大きいメリットです。

「副作用のないメソッド」が書きやすくなる

不変オブジェクトを前提にすると、

「引数を受け取って、新しい値を返すだけ」

という“純粋な”メソッドが書きやすくなります。
こういうメソッドは、テストしやすく、バグも入りにくいです。

日時を不変オブジェクトにしたのは、単にスレッドセーフにしたかったからだけではなく、
「副作用の少ない、読みやすいコードを書きやすくする」という設計上の狙いも大きいです。


まとめ:不変オブジェクトとしての日時を自分の言葉で言うなら

あなたの言葉でまとめると、こうなります。

java.time の日時クラスは、不変(immutable)な“値オブジェクト”として設計されている。
一度作った日時は二度と変わらず、pluswith は“元を壊さずに新しい日時を返す”だけ。
そのおかげで、
・どこに渡しても勝手に書き換えられる心配がなく、
・複数スレッドで共有しても安全で、
・メソッドチェーンで“時間の編集”を宣言的に書けて、
・副作用の少ない、読みやすいコードを組み立てやすくなる。」

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