Javaで日付や時刻を扱う際、かつて当たり前のように使われていた java.util.Date や java.util.Calendar。
長年メンテされているシステムや、過去のコードを参考にして書いた実装で、今もこれらのクラスを見かけることは少なくありません。
「昔からある標準クラスだし、日付が取れればどれを使っても同じでしょ?」
…そう思って使い続けていた結果、**「月が1月ズレる」「マルチスレッドで日付が崩壊する」「いつの間にか値が書き換わる」**といった謎のバグが多発。チーム全員が頭を抱える事態になりました。
今回は、レガシーな日付APIに潜むトラップと、Java 8から導入された java.time(JSR-310)パッケージへ移行したことでバグを一掃できた体験談 をまとめます。
事故の全貌:レガシー日付APIに苦しめられた日々
1. 月の指定が「0始まり」で1か月ズレる事故
新機能で「指定した年月の日付オブジェクトを作る」処理を書いた時のことです。
// 2026年10月1日を作ったつもりが…
Calendar cal = Calendar.getInstance();
cal.set(2026, 10, 1);
Date date = cal.getTime();
画面で確認すると、なぜか 「2026年11月1日」 と表示されていました。
同僚:「10月を指定したはずなのに、なんで11月になってるの…?」
私:「あっ… Calendar の月指定は 0〜11 だから、10 を渡すと 11月になっちゃうんだ…!」
Calendar.OCTOBER という定数を使えば防げますが、直感的な数値で書けない仕様はバグの温床でした。
2. SimpleDateFormat のマルチスレッド共有によるデータ崩壊
Webアプリケーションで、日付のフォーマット処理を共通化しようと以下のようなクラスを作りました。
public class DateUtils {
// スレッドセーフではないのに static 共有!
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy/MM/dd");
public static String formatDate(Date date) {
return FORMATTER.format(date);
}
}
開発環境の単体テストでは何の問題もありませんでした。しかし、本番環境で同時に多数のアクセスが発生した際、「画面に表示される日付が他のユーザーの日付と混ざる」「NumberFormatException が噴射して落ちる」 という大障害が発生。
原因は、SimpleDateFormat がスレッドセーフではない(内部で状態を保持する) にもかかわらず、static フィールドとして複数スレッドで共有してしまっていたことでした。
3. 可変(Mutable)オブジェクトによる予期せぬ書き換え
関数の引数として渡した Date オブジェクトが、呼び出し先の処理の中で書き換えられ、呼び出し元の処理までバグる現象にも悩まされました。
public void processOrder(Date orderDate) {
// 注文日をもとに支払期日を計算したい
Date paymentDueDate = calculateDueDate(orderDate);
// 注文日のログを出したいのに、paymentDueDate の計算内で orderDate 自体が書き換わっていた!
log.info("注文日: {}", orderDate);
}
Date や Calendar は内部状態を変更できる 可変(Mutable)クラス であるため、カプセル化が壊れやすく、思わぬサイドエフェクトを生み出していました。
救世主:java.time(JSR-310)への全面リファクタリング
これらの不具合を根本解決するため、レガシーな日付クラスを廃止し、Java 8で導入された java.time パッケージ(LocalDate / LocalDateTime / DateTimeFormatter) へ全面的に書き換えました。
改善1:直感的な月指定(1月は「1」)
LocalDate では、月を直感的な「1〜12」の数値で指定できます。
// 2026年10月1日を直感的に作成(10月はそのまま 10)
LocalDate date = LocalDate.of(2026, 10, 1);
// 月の列挙型を使うことも可能(タイポ防止)
LocalDate date2 = LocalDate.of(2026, Month.OCTOBER, 1);
「0始まり」による1か月ズレのバグは、これで完全に撲滅されました。
改善2:完全な不変(Immutable)デザイン
java.time のクラス群はすべて 不変(Immutable) です。日付を加算・減算しても、元のインスタンスは書き換わらず、必ず新しいインスタンスが返されます。
LocalDate orderDate = LocalDate.of(2026, 10, 1);
// 7日後を計算しても、orderDate 自体は絶対に変更されない!
LocalDate paymentDueDate = orderDate.plusDays(7);
System.out.println(orderDate); // 2026-10-01 (元のまま安全!)
System.out.println(paymentDueDate); // 2026-10-08
思わぬ場所で値が変更される事故が構造的に発生しなくなりました。
改善3:スレッドセーフな DateTimeFormatter
日付のフォーマットを行う DateTimeFormatter は完全なスレッドセーフであるため、static で共有しても安全に使用できます。
public class DateUtils {
// スレッドセーフなので static 共有してOK!
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd");
public static String formatDate(LocalDate date) {
return date.format(FORMATTER);
}
}
マルチスレッド環境下での日付崩壊や例外は一気に解消しました。
比較まとめ:なぜ今すぐ java.time に移行すべきか
| 比較項目 | レガシーAPI(Date / Calendar) |
モダンAPI(java.time) |
|---|---|---|
| 可変性 | Mutable(副作用・バグの元) | Immutable(安全・値が変わらない) |
| 月の指定 | 0〜11(1月が0で混乱) | 1〜12(直感的でミスがない) |
| スレッド安全性 |
非スレッドセーフ(SimpleDateFormat) |
スレッドセーフ(DateTimeFormatter) |
| クラス構造 | 日時・タイムゾーンが混ざって複雑 |
LocalDate(日付のみ)、LocalDateTime など明確 |
新規開発で java.util.Date や Calendar を選ぶ理由はもうありません。
既存のレガシーコードも、JDBC 4.2 以降であれば DB の DATE / TIMESTAMP 型と LocalDate / LocalDateTime を直接マッピングできます。
日付に関する謎のバグに時間を奪われないためにも、プロジェクトのモダン化を進めていきましょう!
株式会社ONE WEDGE
【ITエンジニアに、IT業界に貢献する企業】
株式会社ONE WEDGEは、Webシステム開発・SES・AI/DX支援を行うIT企業です。生成AIを活用した業務効率化や次世代システム開発にも注力しており、企業の課題解決だけでなく、エンジニア一人ひとりの成長にも本気で向き合っています。また、技術は「一人で学ぶもの」ではなく「仲間と成長するもの」と考え、社内外でのコミュニティづくりにも力を入れています。
https://onewedge.co.jp/