この記事から得られる知識
この記事を読むことで、Java 8以降の標準となった java.time
パッケージ、特に日付と時刻のフォーマット(書式設定)とパース(解析)を司る java.time.format
パッケージについて、以下の知識を体系的に習得できます。
DateTimeFormatter
の基本的な使い方(日付/時刻オブジェクトから文字列への変換、文字列から日付/時刻オブジェクトへの変換)。- ISO 8601形式など、Javaにあらかじめ用意されている事前定義フォーマッタの活用法。
"yyyy/MM/dd"
のようなカスタムパターンを用いた、自由なフォーマットの作成方法。- より複雑で動的なフォーマットを構築するための
DateTimeFormatterBuilder
の使い方。 - 多言語対応に必須のロケール(
Locale
)指定がフォーマットに与える影響。 ResolverStyle
を用いた、パース時の日付・時刻の解釈の厳密さを制御する方法。- 実務で頻発する
DateTimeParseException
の原因と、その具体的な対処法。
第1章: なぜ `java.time.format` なのか? – 新時代の到来
Java 8が登場する前、Java開発者は日付と時刻の扱いに長年頭を悩ませてきました。中心的な役割を担っていたのは java.util.Date
と java.text.SimpleDateFormat
でしたが、これらにはいくつかの根深い問題がありました。
旧APIの問題点
- 可変(Mutable)であること:
Date
オブジェクトは値を変更できてしまうため、意図しない副作用を生む可能性がありました。 - スレッドセーフでないこと:
SimpleDateFormat
はスレッドセーフではないため、マルチスレッド環境で共有すると、予期せぬエラーや誤った結果を引き起こす危険性がありました。このため、使用するたびにインスタンスを生成するか、複雑な同期処理を実装する必要がありました。 - 直感的でないAPI: 月の表現が0から始まる(1月が0)など、直感的でない設計が多く、バグの温床となりがちでした。
これらの問題を解決するために、Java 8で全面的に刷新されたのが java.time
パッケージです。そして、その中で日付と時刻のフォーマットとパースを一手に引き受けるのが java.time.format
パッケージ、その中核をなすのが DateTimeFormatter
クラスです。
`DateTimeFormatter` の主な利点
DateTimeFormatter
は、旧来の SimpleDateFormat
の欠点を克服し、現代的な開発要件を満たすように設計されています。
- 不変(Immutable): 一度作成されると、その状態を変えることはできません。これにより、予期せぬ副作用の心配がなくなります。
- スレッドセーフ: インスタンスは複数のスレッドで安全に共有できます。これにより、パフォーマンスが向上し、コードがシンプルになります。
- 直感的で強力なAPI: 明確で分かりやすいメソッド群と、柔軟なカスタマイズ機能を提供します。
今後のJava開発において、日付・時刻のフォーマット処理は DateTimeFormatter
を使用することが「常識」となっています。このガイドを通じて、その強力な機能を基礎から応用までしっかりと身につけていきましょう。
第2章: 基本の「き」 – `DateTimeFormatter` の簡単な使い方
DateTimeFormatter
の仕事は大きく分けて2つあります。一つは LocalDateTime
などの日付/時刻オブジェクトを人間が読める文字列に変換する「フォーマット」、もう一つはその逆で、特定の書式の文字列を日付/時刻オブジェクトに変換する「パース」です。
2.1 事前定義済みフォーマッタを使う
DateTimeFormatter
には、国際標準であるISO 8601形式など、よく使われるフォーマットが定数としてあらかじめ用意されています。これらを使うのが最も簡単で確実な方法です。
主要な事前定義済みフォーマッタには以下のようなものがあります。
ISO_LOCAL_DATE
: ‘2011-12-03’ のような、タイムゾーン情報を含まない日付。ISO_LOCAL_TIME
: ’10:15:30′ のような、タイムゾーン情報を含まない時刻。ISO_LOCAL_DATE_TIME
: ‘2011-12-03T10:15:30’ のような、タイムゾーン情報を含まない日時。ISO_OFFSET_DATE_TIME
: ‘2011-12-03T10:15:30+09:00’ のように、UTCからのオフセット情報を含む日時。RFC_1123_DATE_TIME
: ‘Tue, 3 Jun 2008 11:05:30 GMT’ のような、HTTPヘッダーなどで使われる形式。
フォーマット(`format`)の例
LocalDateTime
オブジェクトを、事前定義された ISO_LOCAL_DATE_TIME
形式の文字列に変換します。
<?xml version="1.0" encoding="UTF-8"?>
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class BasicFormatExample { public static void main(String[] args) { // 現在の日時を取得 LocalDateTime now = LocalDateTime.now(); // 事前定義済みのフォーマッタを取得 DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; // format() メソッドで文字列に変換 String formattedDateTime = now.format(formatter); System.out.println("元のオブジェクト: " + now); System.out.println("フォーマット後: " + formattedDateTime); }
}
元のオブジェクト: 2025-07-12T12:30:45.123456789
フォーマット後: 2025-07-12T12:30:45.123456789
パース(`parse`)の例
文字列を日付/時刻オブジェクトに変換するには、parse
メソッドを使用します。どのクラスのオブジェクトに変換したいかに応じて、いくつかの方法があります。
<?xml version="1.0" encoding="UTF-8"?>
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class BasicParseExample { public static void main(String[] args) { String dateStr = "2025-07-20"; String dateTimeStr = "2025-08-01T15:45:00"; // --- 方法1: 各クラスの parse() メソッドを使用 --- // こちらが一般的で推奨されることが多いです。 LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE); LocalDateTime dateTime = LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME); System.out.println("パース後のLocalDate: " + date); System.out.println("パース後のLocalDateTime: " + dateTime); // --- 方法2: DateTimeFormatter.parse() とクエリを使用 --- // TemporalAccessor という汎用的な型で受け取り、そこから具体的な型に変換します。 TemporalAccessor parsedAccessor = DateTimeFormatter.ISO_LOCAL_DATE.parse(dateStr); LocalDate dateFromAccessor = LocalDate.from(parsedAccessor); System.out.println("TemporalAccessorから変換: " + dateFromAccessor); }
}
パース後のLocalDate: 2025-07-20
パース後のLocalDateTime: 2025-08-01T15:45:00
TemporalAccessorから変換: 2025-07-20
基本的には、変換したいクラス(LocalDate
, LocalDateTime
など)が持つ `parse()` メソッドを使うのが直感的で分かりやすいでしょう。
第3章: 自由自在に操る – カスタムパターンの作成 (`ofPattern`)
事前定義されたフォーマットでは要件を満たせない場合がほとんどでしょう。例えば、「2025年07月20日 12時30分05秒」や「25/07/20(日)」といった独自の形式で表示したい場合は、自分でフォーマットパターンを定義する必要があります。そのために使うのが DateTimeFormatter.ofPattern(String pattern)
メソッドです。
このメソッドの引数に、パターン文字を組み合わせた文字列を渡すことで、カスタムフォーマッタを作成できます。
主要なパターン文字
以下によく使われるパターン文字とその意味をまとめます。
文字 | 説明 | 例 |
---|---|---|
y | 年 (year-of-era)。通常はyyyy のように4桁で指定します。 | yyyy -> 2025, yy -> 25 |
u | 年 (year)。y とほぼ同じですが、紀元(ERA)に依存しない絶対的な年を表します。通常はこちらを使う方が安全です。 | uuuu -> 2025 |
M | 月 (month-of-year)。桁数で表現が変わります。 | M -> 7, MM -> 07, MMM -> Jul, MMMM -> July |
d | 日 (day-of-month)。 | d -> 20, dd -> 20 |
E | 曜日名 (day-of-week)。 | E ,EE ,EEE -> 日, EEEE -> 日曜日, EEEEE -> 日 |
G | 紀元 (era)。和暦などで使用します。 | G -> 西暦, GGGG -> 西暦 |
a | 午前/午後 (am-pm-of-day)。 | a -> 午後 |
H | 時 (hour-of-day)。0-23時。 | H -> 9, HH -> 09 |
h | 時 (clock-hour-of-am-pm)。1-12時。 | h -> 3, hh -> 03 |
m | 分 (minute-of-hour)。 | m -> 5, mm -> 05 |
s | 秒 (second-of-minute)。 | s -> 8, ss -> 08 |
S | 秒の小数部 (fraction-of-second)。ナノ秒。 | SSS -> 123 (ミリ秒) |
' | リテラル文字のエスケープ。'text' のように囲むと、その中の文字がそのまま出力されます。 | 'T' -> T |
カスタムパターンの使用例
実際にカスタムパターンを使ってフォーマットとパースを行ってみましょう。
<?xml version="1.0" encoding="UTF-8"?>
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class CustomPatternExample { public static void main(String[] args) { LocalDateTime dateTime = LocalDateTime.of(2025, 7, 20, 9, 5, 8); // --- フォーマットの例 --- // パターン1: "uuuu年MM月dd日 HH時mm分ss秒" DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern("uuuu年MM月dd日 HH時mm分ss秒"); String formatted1 = dateTime.format(formatter1); System.out.println("パターン1: " + formatted1); // パターン2: "uuuu/MM/dd(E)" // 日本語の曜日名を出力するためにロケールを指定 DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("uuuu/MM/dd(E)"); String formatted2 = dateTime.format(formatter2); System.out.println("パターン2: " + formatted2); // --- パースの例 --- String strToParse = "2025/07/20 09:05:08"; DateTimeFormatter parser = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); LocalDateTime parsedDateTime = LocalDateTime.parse(strToParse, parser); System.out.println("パース結果: " + parsedDateTime); }
}
パターン1: 2025年07月20日 09時05分08秒
パターン2: 2025/07/20(日)
パース結果: 2025-07-20T09:05:08
注意点: パースする場合、パース対象の文字列と `ofPattern` で指定したパターンは厳密に一致している必要があります。スペースの有無や区切り文字が異なると、DateTimeParseException
が発生します。
第4章: 一歩進んだ使い方 – `DateTimeFormatterBuilder`
ofPattern
は非常に便利ですが、表現できない、または表現が難しいケースも存在します。例えば、「パースする際、大文字と小文字を区別したくない」「ある要素が任意で存在したりしなかったりする」といった、より動的で複雑な要件に応えるのが DateTimeFormatterBuilder
です。
これは、フォーマッタの部品を一つずつ組み立てていくビルダーパターンのクラスです。
`DateTimeFormatterBuilder` の主なメソッド
appendPattern(String)
:ofPattern
と同様のパターン文字列を追加します。appendLiteral(String)
: 固定の文字列リテラルを追加します。appendValue(TemporalField)
: 年や月などの数値をそのまま追加します。appendText(TemporalField)
: 月の名前(”July”)や曜日名(”Sunday”)など、テキスト表現を追加します。optionalStart() / optionalEnd()
: この2つで囲まれた部分は、パース時に任意(存在しなくても良い)のセクションとなります。parseCaseInsensitive()
: 大文字と小文字を区別しないパースモードに設定します。parseCaseSensitive()
: 大文字と小文字を区別するパースモード(デフォルト)に設定します。toFormatter()
: 組み立てたビルダーから最終的なDateTimeFormatter
インスタンスを生成します。
使用例:オプションの秒とミリ秒をパースする
入力される日時の文字列に、秒やミリ秒が含まれている場合と含まれていない場合の両方に対応したいケースを考えます。
<?xml version="1.0" encoding="UTF-8"?>
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
public class BuilderExample { public static void main(String[] args) { // ビルダーを使ってフォーマッタを組み立てる DateTimeFormatter formatter = new DateTimeFormatterBuilder() // "uuuu-MM-dd HH:mm" の部分は必須 .appendPattern("uuuu-MM-dd HH:mm") // ここからがオプション部分 .optionalStart() // 秒は必須 .appendLiteral(':') .appendValue(ChronoField.SECOND_OF_MINUTE, 2) // さらにミリ秒もオプション .optionalStart() .appendFraction(ChronoField.NANO_OF_SECOND, 3, 3, true) // 3桁のミリ秒 .optionalEnd() .optionalEnd() // ビルダーからフォーマッタを生成 .toFormatter(); // パース対象の文字列 String str1 = "2025-07-20 10:30"; // 秒・ミリ秒なし String str2 = "2025-07-20 10:30:55"; // 秒あり String str3 = "2025-07-20 10:30:55.123"; // 秒・ミリ秒あり // パースを実行 LocalDateTime dt1 = LocalDateTime.parse(str1, formatter); LocalDateTime dt2 = LocalDateTime.parse(str2, formatter); LocalDateTime dt3 = LocalDateTime.parse(str3, formatter); System.out.println("パース結果1: " + dt1); System.out.println("パース結果2: " + dt2); System.out.println("パース結果3: " + dt3); }
}
パース結果1: 2025-07-20T10:30
パース結果2: 2025-07-20T10:30:55
パース結果3: 2025-07-20T10:30:55.123
このように DateTimeFormatterBuilder
を使うことで、ofPattern
だけでは実現が難しい、柔軟なフォーマットとパースのルールをプログラムで構築できます。
第5章: 国際化と厳密さの制御 – ロケールとリゾルバースタイル
DateTimeFormatter
の能力は、単にフォーマットを定義するだけにとどまりません。国際化対応のための「ロケール」設定や、パースの挙動を細かく制御する「リゾルバースタイル」など、より高度な機能も備えています。
5.1 ロケール (`withLocale`)
月名や曜日名をテキストで表示する場合、どの言語で表示するかを指定するのがロケールです。`withLocale(Locale)` メソッドを使うことで、フォーマッタに特定のロケールを設定した新しいインスタンスを生成できます。
<?xml version="1.0" encoding="UTF-8"?>
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
public class LocaleExample { public static void main(String[] args) { LocalDateTime dateTime = LocalDateTime.of(2025, 8, 8, 20, 0); // パターン "MMMM d, yyyy (EEEE)" を使用 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM d, yyyy (EEEE)"); // 日本語ロケール String japanese = dateTime.format(formatter.withLocale(Locale.JAPAN)); System.out.println("日本 (JAPAN): " + japanese); // アメリカ英語ロケール String usEnglish = dateTime.format(formatter.withLocale(Locale.US)); System.out.println("米国 (US): " + usEnglish); // フランス語ロケール String french = dateTime.format(formatter.withLocale(Locale.FRANCE)); System.out.println("フランス (FRANCE): " + french); // ofLocalizedDateTimeを使ったロケール依存のフォーマット DateTimeFormatter localizedFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL); System.out.println("日本のFULLスタイル: " + dateTime.format(localizedFormatter.withLocale(Locale.JAPAN))); }
}
日本 (JAPAN): 8月 8, 2025 (金曜日)
米国 (US): August 8, 2025 (Friday)
フランス (FRANCE): août 8, 2025 (vendredi)
日本のFULLスタイル: 2025年8月8日金曜日 20時00分00秒
パターン文字(`MMMM` や `EEEE`)は同じでも、ロケールを変えるだけで出力される言語が切り替わることがわかります。グローバルなアプリケーションを開発する際には必須の機能です。
5.2 リゾルバースタイル (`withResolverStyle`)
リゾルバースタイルは、文字列をパースする際に、存在しない日付や曖昧な値をどのように解釈するかを定義するものです。これは、withResolverStyle(ResolverStyle)
メソッドで設定します。ResolverStyle
は以下の3つのモードを持つ列挙型です。
スタイル | 説明 |
---|---|
STRICT | 厳密モード。 存在しない日付(例: “2025-02-30″)や、暦の上でありえない値を一切許可しません。少しでも矛盾があれば DateTimeParseException をスローします。最も安全なモードです。 |
SMART | スマートモード(デフォルト)。 日付や時刻の値を検証し、妥当な範囲で解決します。例えば、”2025-04-31″(4月は30日まで)はエラーになりますが、うるう年の計算などは正しく行います。日常的な利用ではこのモードで十分な場合が多いです。 |
LENIENT | 寛容モード。 値を非常に柔軟に解釈します。例えば、”2025-01-32″ は「1月31日の次の日」と解釈され、”2025-02-01″ になります。また、”13月” は翌年の1月として扱われます。予期せぬ日付変換が発生する可能性があるため、利用には注意が必要です。 |
実際に `STRICT` モードと `LENIENT` モードの違いを見てみましょう。
<?xml version="1.0" encoding="UTF-8"?>
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
public class ResolverStyleExample { public static void main(String[] args) { String invalidDateStr = "2025-02-30"; // 存在しない日付 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd"); // --- STRICTモード --- try { LocalDate.parse(invalidDateStr, formatter.withResolverStyle(ResolverStyle.STRICT)); } catch (Exception e) { System.out.println("STRICTモードでのエラー: " + e.getClass().getName()); System.out.println(" => " + e.getMessage()); } // --- LENIENTモード --- try { LocalDate lenientDate = LocalDate.parse(invalidDateStr, formatter.withResolverStyle(ResolverStyle.LENIENT)); System.out.println("LENIENTモードでのパース結果: " + lenientDate); } catch (Exception e) { System.out.println("LENIENTモードで予期せぬエラー: " + e); } }
}
STRICTモードでのエラー: java.time.format.DateTimeParseException
=> Text ‘2025-02-30’ could not be parsed: Invalid date ‘FEBRUARY 30’
LENIENTモードでのパース結果: 2025-03-02
`STRICT` モードでは例外が発生したのに対し、`LENIENT` モードでは2月30日を「2月28日の2日後」と解釈し、3月2日としてパースしていることがわかります。外部から受け取るデータの品質が保証できない場合は、意図しない日付変換を防ぐために `STRICT` モードを指定するのが堅実な選択です。
第6章: 実践的なユースケースとエラーハンドリング
これまでに学んだ知識を、実際の開発シーンでどのように活用できるか、そして避けては通れないエラーにどう対処するかを見ていきましょう。
6.1 実践的なユースケース
- ログ出力: ログにタイムスタンプを記録する際、ISO 8601形式(特に `ISO_OFFSET_DATE_TIME`)は、タイムゾーン情報が含まれ、かつ機械可読性が高いため非常に適しています。これにより、異なるサーバーや地域のログを横断的に分析する際に時刻のずれを防ぐことができます。
DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now())
- APIレスポンス/リクエスト: REST APIなどでJSON形式のデータをやり取りする際、日付・時刻は文字列として表現されます。ここでも国際標準であるISO 8601形式がデファクトスタンダードです。クライアントとサーバーでフォーマットを統一しておくことで、無用なパースエラーを避けられます。
- ファイル名への利用: 日次バッチの出力ファイル名などに日付を含める場合、「`yyyyMMdd_backup.zip`」のような形式がよく使われます。ただし、ファイル名に使えない文字(例: `:`)を含めないようにパターンを工夫する必要があります。
DateTimeFormatter.ofPattern("uuuuMMdd_HHmmss").format(LocalDateTime.now())
6.2 エラーハンドリング: `DateTimeParseException` との戦い
`DateTimeFormatter` を使う上で最も頻繁に遭遇するのが DateTimeParseException
です。これは、与えられた文字列が指定されたフォーマットでパースできない場合にスローされる非チェック例外です。
主な発生原因
- フォーマットの不一致: 最も多い原因です。期待するパターン(例: “uuuu/MM/dd”)と実際の文字列(例: “2025-07-20″)の区切り文字や順序が違う。
- 存在しない日付・時刻: `ResolverStyle` が `STRICT` や `SMART` の場合に、”2025-02-30″ のような暦の上で存在しない日付をパースしようとした。
- 情報の不足: `LocalDateTime` にパースしようとしたが、文字列に時刻情報が含まれていなかった(例: “2025-01-02″)。
適切な対処法
ユーザー入力や外部ファイルなど、信頼性の低いデータソースから文字列をパースする場合は、必ず `try-catch` ブロックで囲み、例外が発生した場合の代替処理を記述する必要があります。
<?xml version="1.0" encoding="UTF-8"?>
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
public class ErrorHandlingExample { public static void main(String[] args) { String inputStr = "20-07-2025"; // "uuuu-MM-dd" とは異なるフォーマット DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd"); LocalDate date = null; try { date = LocalDate.parse(inputStr, formatter); System.out.println("パースに成功しました: " + date); } catch (DateTimeParseException e) { // エラーメッセージから原因を探る System.err.println("日付のパースに失敗しました。入力文字列のフォーマットを確認してください。"); System.err.println("入力: \"" + inputStr + "\""); System.err.println("期待するフォーマット: \"uuuu-MM-dd\""); // 詳細なエラー情報をログに出力するとデバッグに役立つ // e.printStackTrace(); // ここでデフォルト値を設定したり、ユーザーに再入力を促す処理を行う date = LocalDate.of(1970, 1, 1); // フォールバック値 } System.out.println("最終的な日付: " + date); }
}
日付のパースに失敗しました。入力文字列のフォーマットを確認してください。 入力: "20-07-2025" 期待するフォーマット: "uuuu-MM-dd"最終的な日付: 1970-01-01
例外を握りつぶすのではなく、エラーの原因をユーザーに分かりやすく伝えたり、ログに記録したり、安全なデフォルト値で処理を継続したりといった、堅牢なエラーハンドリングを実装することが重要です。
まとめ
本記事では、Java 8以降のモダンな日付・時刻フォーマットAPIである java.time.format.DateTimeFormatter
について、その基本から応用までを網羅的に解説しました。
SimpleDateFormat
が抱えていたスレッドセーフティや可変性の問題を克服した DateTimeFormatter
は、堅牢で保守性の高いアプリケーションを構築するための強力なツールです。
要点の振り返り
- フォーマットとパースが基本操作であり、事前定義フォーマッタとカスタムパターン(
ofPattern
)を使い分ける。 - より複雑な要件には
DateTimeFormatterBuilder
を活用する。 - 国際化対応には
withLocale
、パースの厳密さ制御にはwithResolverStyle
を使用する。 DateTimeParseException
を適切にハンドリングすることが、安定したアプリケーションの鍵となる。
java.time
パッケージとそのフォーマット機能は、Java開発者にとって必須の知識です。このガイドが、皆さんのコードをより安全で、よりクリーンなものにするための一助となれば幸いです。