markdown
はじめに (対象読者・この記事でわかること)
この記事は、Javaで日付・時刻を扱うことが必要なプログラマ、特に以下のような方を対象としています。
- Java入門者や大学・専門学校の授業でDate/Time APIを学んでいる学生
- 業務システムで日付文字列の入出力を実装するエンジニア
- 既存コードの SimpleDateFormat から java.time へのリファクタリングを検討している方
本記事を読むことで、次のことができるようになります。
1. 文字列 → LocalDate / LocalDateTime への変換手順と注意点が理解できる。
2. LocalDate / LocalDateTime → 任意の文字列形式にフォーマットできる。
3. 旧来の Date / SimpleDateFormat と新しい java.time 系 API の違いと、実務での選択基準が分かる。
背景として、Java 8 以降で導入された java.time パッケージはスレッドセーフで可読性が高く、日付処理の標準となっていますが、レガシーコードが多く残っている現場では依然として SimpleDateFormat が使われています。そこで、両者の使い分けと移行手順を具体例とともに示します。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Java の基本的な構文とクラス・メソッドの書き方
- String 操作や例外処理(try-catch)の基礎
- 開発環境(IDE 例: IntelliJ IDEA、Eclipse)でのコンパイルと実行方法
日付変換の概要と背景
Java では、長らく java.util.Date と java.text.SimpleDateFormat が日付・時刻の表現に使われてきました。これらは以下の課題を抱えています。
-
ミュータブル(可変)でスレッドセーフでない
SimpleDateFormatは内部状態を保持するため、複数スレッドで同時に使用すると予期せぬ結果になる可能性があります。 -
表現力の不足
時間帯(TimeZone)やローカライズされた形式の扱いが煩雑で、コードが冗長になりがちです。
2014 年に JSR‑310 が正式に java.time パッケージとして Java 8 に組み込まれ、ISO‑8601 をベースにした不変(イミュータブル)な日付・時刻クラスが提供されました。主なクラスは次の通りです。
| クラス | 用途 | 主な特徴 |
|---|---|---|
LocalDate |
日付(年月日)だけ | タイムゾーン情報なし、不可変 |
LocalTime |
時間(時分秒)だけ | 同上 |
LocalDateTime |
日付と時間の組み合わせ | 同上 |
ZonedDateTime |
タイムゾーン付き日時 | タイムゾーン変換が容易 |
DateTimeFormatter |
パターンベースの文字列フォーマット/パース | スレッドセーフ、カスタムフォーマットが容易 |
この新しい API を使うと、「文字列 → オブジェクト → 別形式の文字列」 という典型的な変換がシンプルに記述できます。次のセクションでは、実務でよくある 3 つのシナリオを例に、具体的なコードと解説を行います。
実践的な日付形式変換手順
ステップ 1:文字列 → LocalDate / LocalDateTime に変換する
1‑1. パターンが決まっている場合(例: 2025-09-14)
Javaimport java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; public class ParseExample { public static void main(String[] args) { String input = "2025-09-14"; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); try { LocalDate date = LocalDate.parse(input, formatter); System.out.println("Parsed LocalDate: " + date); // 2025-09-14 } catch (DateTimeParseException e) { System.err.println("日付文字列の形式が正しくありません: " + e.getMessage()); } } }
ポイント
- DateTimeFormatter.ofPattern で任意のパターンを定義できる。
- LocalDate.parse は例外を投げるので try-catch で安全に処理する。
1‑2. 時間情報も含む場合(例: 2025/09/14 15:30:45)
Javaimport java.time.LocalDateTime; import java.time.format.DateTimeFormatter; String input = "2025/09/14 15:30:45"; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); LocalDateTime dateTime = LocalDateTime.parse(input, formatter); System.out.println(dateTime); // 2025-09-14T15:30:45
LocalDateTimeはタイムゾーンを持たないため、サーバーのローカル時間で扱うケースに向いている。
ステップ 2:LocalDate / LocalDateTime → 任意の文字列形式にフォーマットする
JavaLocalDate date = LocalDate.now(); // 例: 2025-09-14 DateTimeFormatter outFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日"); String formatted = date.format(outFormatter); System.out.println(formatted); // 2025年09月14日
formatメソッドはインスタンス側にあるため、DateTimeFormatterを引数に渡すだけで完了。java.time系はすべてtoString()が ISO‑8601 形式になるので、必要に応じてカスタムフォーマットを指定するだけで良い。
ステップ 3:タイムゾーン付きの変換(ZonedDateTime)
Javaimport java.time.ZoneId; import java.time.ZonedDateTime; String input = "2025-09-14T15:30:45"; LocalDateTime ldt = LocalDateTime.parse(input); ZonedDateTime zoned = ldt.atZone(ZoneId.of("Asia/Tokyo")); System.out.println(zoned); // 2025-09-14T15:30:45+09:00[Asia/Tokyo] DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss Z"); System.out.println(zoned.format(fmt)); // 2025/09/14 15:30:45 +0900
ZoneIdでタイムゾーンを指定し、atZoneでLocalDateTimeに付与できる。Zパターンでオフセット(+0900 など)を出力できる。
ハマった点やエラー解決
| 症状 | 原因 | 解決策 |
|---|---|---|
DateTimeParseException: Text '2025-09-14' could not be parsed at index 4 |
パターンが入力文字列と不一致(yyyy/MM/dd 等) |
DateTimeFormatter のパターンを入力文字列に合わせる |
java.time.format.DateTimeParseException: Unable to obtain LocalDateTime from TemporalAccessor |
時間情報が欠如しているのに LocalDateTime.parse を使用 |
LocalDate.parse に切り替えるか、時間部分をデフォルトで付与 |
ConcurrentModificationException(旧 SimpleDateFormat 使用時) |
SimpleDateFormat をシングルインスタンスで共有したためスレッド競合 |
DateTimeFormatter はスレッドセーフなのでインスタンスを共有可能。旧 API では ThreadLocal<SimpleDateFormat> を利用 |
解決策の実装例(SimpleDateFormat を ThreadLocal で安全に使用)
Javaprivate static final ThreadLocal<SimpleDateFormat> SDF = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd") ); public static Date parse(String text) throws ParseException { return SDF.get().parse(text); }
しかし、新規プロジェクトでは必ず java.time 系を採用 することを推奨します。コード量は減り、保守性が格段に向上します。
まとめ:ベストプラクティス
- 可能な限り
java.time系を使用 → 不変・スレッドセーフで直感的。 - フォーマットは
DateTimeFormatterに任せる → パターンは一元管理し、再利用を容易に。 - レガシーコードのリファクタリング →
SimpleDateFormatをThreadLocalで包むか、段階的にjava.timeに置き換える。 - タイムゾーンが関係する場合は必ず
ZonedDateTime→ オフセット・ローカライズが正確に扱える。
まとめ
本記事では、Java における日付文字列の変換手順を Legacy API(SimpleDateFormat) と Modern API(java.time) の二つの観点から解説しました。
- 文字列 ↔
LocalDate/LocalDateTimeの基本変換 - 任意フォーマットへの出力方法
- タイムゾーン付き
ZonedDateTimeの扱い - よくあるエラーとその回避策
これにより、読者は「日付文字列を安全にパースし、好きな形式で出力できる」スキルを身につけられます。次回は、Spring Boot と java.time の統合、および データベース(JPA)との相互変換 をテーマにした記事を予定しています。
参考資料
- Oracle Java SE Documentation – Date and Time API
- Effective Java (3rd Edition) – Item 31: Prefer
java.timeover legacy date‑time APIs - Baeldung – Guide to Java 8 Date/Time API
- Stack Overflow – SimpleDateFormat thread safety
