1. 歴史的背景: Java 8 以前は日付をどう扱っていたか
昔(Java 8 以前)は、日付と時刻の扱いに java.util.Date、java.util.Calendar を使い、フォーマットには java.text.SimpleDateFormat を使っていました。世界中のプログラマーがため息をつき、歯ぎしりしながら次のようなコードを書いていたものです。
import java.util.Date;
Date now = new Date();
System.out.println(now); // "Wed Jun 05 14:15:22 MSK 2025" のような奇妙なものが出力される
一見簡単そうですが、実際はそう甘くありませんでした。旧 API の「喜び」の一部を挙げると次のとおりです。
- Date は可変(mutable)オブジェクト。うっかり変更してしまい、バグの原因になりがち。
- Date と Calendar の月は 0 始まり(1 月は 0、12 月は 11)。一方で日だけは 1 始まり。
- SimpleDateFormat はスレッドセーフではない。2 つのスレッドが同時にフォーマットすると、予期せぬ結果になりうる。
- 非常に多くのメソッドが @Deprecated(非推奨)で、IDE が常に黄色の警告を出してくる。
- タイムゾーンの扱いは本当に苦痛。ローカル時刻と UTC を混同しやすく、夏時間/冬時間の切り替えはなおさら大変。
つらい例
import java.util.Date;
Date date = new Date(2025, 5, 1); // 2025 年、5 月(6 月?)、1 日
System.out.println(date); // 期待どおりではない!
2. java.time の登場: 新しいアプローチ
2014 年頃には、旧 API は不便なだけでなく危険だということが明らかになりました。そこで JSR‑310 仕様を実装した新しいパッケージ java.time が Java に登場しました。この API は人気ライブラリ Joda-Time に着想を得ており、瞬く間に事実上の標準となりました。
主なパッケージとクラス
- java.time — 新しい日付と時刻のクラスが居並ぶ中核パッケージ。
- java.time.format — 日付と時刻のフォーマット/パース用。
- java.time.temporal — より高度な時間操作のための API。
- java.time.zone — タイムゾーン関連。
新 API の主役たちは次のとおりです。
| クラス | 用途 | 使用例 |
|---|---|---|
|
日付のみ(年・月・日) | 時刻なしの誕生日 |
|
時刻のみ(時・分・秒) | 日付なしの会議時刻 |
|
タイムゾーンなしの日時 | ローカルなイベント |
|
タイムゾーン付きの日時 | ミンスクの現地時刻でのミーティング |
|
UTC の絶対的な時間点 | ログのイベントのタイムスタンプ |
|
時間間隔(時・分・秒) | 通話時間 |
|
期間(年・月・日) | 勤続年数、年齢 |
例: 新しいやり方で日付を作る
import java.time.LocalDate;
LocalDate today = LocalDate.now();
System.out.println(today); // 例: "2025-06-05"
3. 新 API の利点
不変(immutable)
java.time のクラスはすべて不変です。つまり、LocalDate オブジェクトを作成したら、それ自体を変更することはできません。操作(たとえば 1 日足す)は常に新しいオブジェクトを返します。
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);
System.out.println(today); // 2025-06-05
System.out.println(tomorrow); // 2025-06-06
タイムゾーンを明示的に扱う
旧 API では、その日時がどのタイムゾーンなのかを忘れがちでした。java.time ではすべてが明示的です。タイムゾーンが必要なら ZonedDateTime、不要なら LocalDateTime を使います。
import java.time.ZonedDateTime;
import java.time.ZoneId;
ZonedDateTime MinskTime = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
System.out.println(MinskTime); // 2025-06-05T14:23:45+03:00[Europe/Minsk]
計算や比較のための便利なメソッド
LocalDate today = LocalDate.now();
LocalDate nextMonth = today.plusMonths(1);
boolean isAfter = LocalDate.now().plusDays(1).isAfter(today); // true
フォーマットとパース
フォーマットとパースは DateTimeFormatter で行います(詳細は次回の講義)。
import java.time.format.DateTimeFormatter;
LocalDate today = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
String formatted = today.format(formatter); // "05.08.2025"
4. 互換性: 旧コードとどう付き合うか
実世界では、とても頻繁に Date や Calendar を使うライブラリや古いシステムと連携する必要があります。幸い、新 API はレガシーに優しく、旧タイプと新タイプを相互に変換できます。
Date ↔ Instant の変換
import java.util.Date;
import java.time.Instant;
// Date → Instant
Date legacyDate = new Date();
Instant instant = legacyDate.toInstant();
// Instant → Date
Date dateBack = Date.from(instant);
Calendar ↔ ZonedDateTime の変換
import java.util.Calendar;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.util.Date;
// Calendar → ZonedDateTime
Calendar calendar = Calendar.getInstance();
ZonedDateTime zdt = ZonedDateTime.ofInstant(
calendar.toInstant(),
calendar.getTimeZone().toZoneId()
);
// ZonedDateTime → Calendar
Calendar calBack = Calendar.getInstance();
calBack.setTime(Date.from(zdt.toInstant()));
表: 旧クラスと新クラスの対応
| 旧クラス | 新クラス | コメント |
|---|---|---|
|
|
絶対時刻 |
|
|
タイムゾーン付きの日時 |
|
|
日付のフォーマット/パース |
5. 実践: java.time を使ってみる
たとえば、誕生日を持つユーザーがいるとします。この日付を保存し、表示してみましょう。
import java.time.LocalDate;
public class UserProfile {
private String name;
private LocalDate birthDate;
public UserProfile(String name, LocalDate birthDate) {
this.name = name;
this.birthDate = birthDate;
}
public void printProfile() {
System.out.println("名前: " + name);
System.out.println("生年月日: " + birthDate);
}
}
public class Main {
public static void main(String[] args) {
UserProfile user = new UserProfile("Alisa", LocalDate.of(1998, 12, 25));
user.printProfile();
}
}
出力:
名前: Alisa
生年月日: 1998-12-25
6. 比較: 旧 API と新 API
例: 誕生日に 1 週間を足す
旧来の方法(Date/Calendar):
import java.util.Calendar;
Calendar cal = Calendar.getInstance();
cal.set(1998, Calendar.DECEMBER, 25);
cal.add(Calendar.WEEK_OF_YEAR, 1);
System.out.println(cal.getTime()); // 面倒でわかりにくい
新しい方法(java.time):
import java.time.LocalDate;
LocalDate birthDate = LocalDate.of(1998, 12, 25);
LocalDate nextWeek = birthDate.plusWeeks(1);
System.out.println(nextWeek); // 1999-01-01
新 API ではコードが短く、簡潔で、安全です。
7. java.time を使うときのよくある間違い
エラー1: オブジェクトが不変であることを忘れる。
date.plusDays(1); を呼んでも、結果を保存しなければ元の日時は変わりません。
エラー2: LocalDate と LocalDateTime を取り違える。
LocalDate は日付(年・月・日)のみ、LocalDateTime はさらに時刻も保持します。時と分も扱う必要がある場合は混同しないようにしましょう。
エラー3: 新しいプロジェクトで古いクラスを使ってしまう。
可能であれば常に java.time を使いましょう。旧来のクラスは互換性維持のためだけに必要です。
エラー4: タイムゾーンの扱いを誤る。
地域をまたいで重要なイベントを保存する場合は、ZonedDateTime、少なくとも Instant を使いましょう。LocalDateTime にはタイムゾーン情報が含まれません。
エラー5: LocalDate と LocalDateTime を直接比較しようとする。
これは別のデータ型なので、直接比較はできません。まず同じ型にそろえてください。
GO TO FULL VERSION