Javaにおける日時処理は、かつてはjava.util.Datejava.util.Calendar、そしてSimpleDateFormatを用いるのが一般的でした。

しかし、これらのクラスにはスレッドセーフではない、可変(Mutable)であるといった設計上の課題が多く存在していました。

Java 8以降、これらの問題を解決するためにDate and Time API(java.timeパッケージ)が導入され、現在の日時操作のデファクトスタンダードとなっています。

本記事では、この現代的な日時操作において中心的な役割を担うDateTimeFormatterクラスを中心に、日時のフォーマット(文字列への変換)やパース(文字列からの変換)の方法、そして実務で役立つパターン一覧を網羅的に解説します。

現代のJavaにおける日時処理の標準

Java 8で導入されたjava.timeパッケージは、JSR-310として仕様化された非常に堅牢なライブラリです。

このパッケージに含まれるクラス群は、不変(Immutable)であり、マルチスレッド環境下でも安全に使用できるという特徴を持っています。

java.timeパッケージの主要クラス

日時を扱う際には、そのデータが「タイムゾーンを含むのか」「日付のみなのか」といった用途に応じて適切なクラスを選択する必要があります。

クラス名保持する情報用途
LocalDate日付(年、月、日)誕生日、記念日など時刻を必要としない場合
LocalTime時刻(時、分、秒、ナノ秒)開店時間など日付を必要としない場合
LocalDateTime日付と時刻タイムゾーンを考慮しないシステム内部の日時
ZonedDateTime日付、時刻、タイムゾーン世界各地の時間を扱うグローバルな処理
OffsetDateTime日付、時刻、UTCからのオフセット通信プロトコルやデータベース連携

これらのクラスを文字列として出力したり、逆に文字列からインスタンスを生成したりする際に不可欠なのが、DateTimeFormatterです。

DateTimeFormatterの基本概念

DateTimeFormatterは、日時の書式を定義するためのクラスです。

従来のSimpleDateFormatと最も異なる点は、インスタンスが不変(Immutable)であり、スレッドセーフであるという点です。

これにより、一度定義したフォーマッタをstaticな定数として保持し、アプリケーション全体で共有しても安全に動作します。

スレッドセーフという利点

従来のSimpleDateFormatはスレッドセーフではなかったため、複数のスレッドから同時にアクセスすると内部状態が壊れ、誤った日時が出力されるリスクがありました。

そのため、メソッド内で毎回インスタンスを生成するか、ThreadLocalを使用するなどの工夫が必要でした。

DateTimeFormatterではそのような心配がなくなり、パフォーマンスと安全性が大幅に向上しています。

日時フォーマットの基本パターン一覧

日時の書式を指定する際には、特定の記号(パターン文字)を組み合わせて記述します。

よく使われる主要なパターンを整理して解説します。

年・月・日の指定

日付部分を指定するためのパターン文字です。

パターン意味出力例
u年(符号付き)2026, -0001
y年(紀元内)2026
M月(数字)3, 03
L月(独立形式)3, 03
d5, 05
E曜日木, Thursday

補足:uとyの違い 通常の使用ではyで問題ありませんが、uは紀元前を負の数で表現できるため、より厳密な学術的・歴史的計算に向いています。

また、Mは「1月」などの文脈、Lは単独の「1月」という名称として使われますが、日本語環境では大きな差はありません。

時・分・秒・ナノ秒の指定

時刻部分を指定するためのパターン文字です。

パターン意味出力例
H時(0-23)0, 15
h時(1-12)3, 11
a午前/午後AM, PM, 午前, 午後
m9, 09
s5, 05
S秒の小数(ナノ秒まで)1, 123
nナノ秒987654321

タイムゾーン・オフセットの指定

タイムゾーン情報を扱うためのパターン文字です。

パターン意味出力例
VタイムゾーンIDAsia/Tokyo
zタイムゾーン名JST
xオフセット+09, +0930
Zオフセット+0900

実践:日時のフォーマット(LocalDateTimeからString)

それでは、実際にプログラムを動かして日時を文字列に変換する方法を見ていきましょう。

定義済みフォーマッタの使用

DateTimeFormatterには、ISO 8601規格に基づいた標準的な書式が定数としてあらかじめ用意されています。

Java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Main {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();

        // ISO 8601形式 (2026-03-05T15:30:45.123)
        String isoDateTime = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        System.out.println("ISO形式: " + isoDateTime);

        // ISO日付形式 (2026-03-05)
        String isoDate = now.format(DateTimeFormatter.ISO_LOCAL_DATE);
        System.out.println("ISO日付: " + isoDate);
    }
}
実行結果
ISO形式: 2026-03-05T15:30:45.123456789
ISO日付: 2026-03-05

カスタムパターンによるフォーマット

実務で最も頻繁に利用されるのは、ofPatternメソッドを使用して独自の書式を定義する方法です。

Java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class FormatExample {
    // スレッドセーフなので定数として定義可能
    private static final DateTimeFormatter CUSTOM_FORMATTER = 
        DateTimeFormatter.ofPattern("uuuu/MM/dd HH:mm:ss");

    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();

        // 独自の書式で出力
        String formatted = now.format(CUSTOM_FORMATTER);
        System.out.println("カスタムフォーマット: " + formatted);
        
        // メソッドチェーンでの記述も可能
        String simple = LocalDateTime.now()
            .format(DateTimeFormatter.ofPattern("yyyy年MM月dd日(E)"));
        System.out.println("日本語フォーマット: " + simple);
    }
}
実行結果
カスタムフォーマット: 2026/03/05 15:30:45
日本語フォーマット: 2026年03月05日(木)

実践:日時の解析(StringからLocalDateTime)

文字列として受け取った日時データを、Javaのオブジェクトに変換することを「パース(解析)」と呼びます。

パースの基本手順

parseメソッドを使用します。

このとき、解析対象の文字列とフォーマットが一致していないと例外が発生します。

Java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class ParseExample {
    public static void main(String[] args) {
        String dateString = "2026-03-05 10:15:30";
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

        // 文字列からLocalDateTimeへ変換
        LocalDateTime dateTime = LocalDateTime.parse(dateString, formatter);

        System.out.println("解析後の日時: " + dateTime);
        System.out.println("1日後の日時: " + dateTime.plusDays(1));
    }
}
実行結果
解析後の日時: 2026-03-05T10:15:30
1日後の日時: 2026-03-06T10:15:30

パース時の例外処理

指定したフォーマットと文字列が一致しない場合、DateTimeParseExceptionがスローされます。

ユーザー入力などを解析する場合は、必ずtry-catchブロックによる例外処理を検討してください。

Java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

public class ExceptionHandling {
    public static void main(String[] args) {
        String invalidDate = "2026/13/40 25:00:00"; // 存在しない日時
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");

        try {
            LocalDateTime.parse(invalidDate, formatter);
        } catch (DateTimeParseException e) {
            System.err.println("エラー: 日時の形式が正しくありません。" + e.getMessage());
        }
    }
}

ロケールと言語設定の反映

曜日や月名を日本語や英語で出力したい場合、withLocaleメソッドを使用してロケールを指定します。

Java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class LocaleExample {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        
        // 日本語ロケール
        DateTimeFormatter jpFormatter = DateTimeFormatter.ofPattern("EEEE", Locale.JAPANESE);
        // 英語ロケール
        DateTimeFormatter enFormatter = DateTimeFormatter.ofPattern("EEEE", Locale.ENGLISH);

        System.out.println("日本語: " + now.format(jpFormatter));
        System.out.println("英語: " + now.format(enFormatter));
    }
}
実行結果
日本語: 木曜日
英語: Thursday

和暦(JapaneseDate)の扱い

日本のビジネスシーンでは和暦(令和など)が必要になることがあります。

その場合は、JapaneseChronologyを設定します。

Java
import java.time.chrono.JapaneseDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class JapaneseCalendarExample {
    public static void main(String[] args) {
        JapaneseDate now = JapaneseDate.now();
        
        // GGGGGは「令」などの1文字略称、GGGGは「令和」
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("GGGGy年M月d日", Locale.JAPAN);
        
        System.out.println("和暦表示: " + now.format(formatter));
    }
}
実行結果
和暦表示: 令和8年3月5日

タイムゾーンを考慮したフォーマット

グローバルなアプリケーションでは、タイムゾーン情報を付与したフォーマットが必要になります。

この場合、ZonedDateTimeを使用します。

Java
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

public class TimeZoneExample {
    public static void main(String[] args) {
        // 東京の現在日時
        ZonedDateTime tokyoTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
        
        // タイムゾーン名(z)とオフセット(Z)を含むフォーマット
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z (Z)");
        
        System.out.println("東京: " + tokyoTime.format(formatter));
        
        // ニューヨークの時間に変換して表示
        ZonedDateTime nyTime = tokyoTime.withZoneSameInstant(ZoneId.of("America/New_York"));
        System.out.println("NY: " + nyTime.format(formatter));
    }
}
実行結果
東京: 2026-03-05 15:30:45 JST (+0900)
NY: 2026-03-05 01:30:45 EST (-0500)

注意すべきポイントとハマりどころ

日時フォーマットの実装において、開発者が陥りやすいミスがいくつかあります。

yyyyとYYYYの違い

最も有名な間違いの一つが、年の指定におけるyYの混同です。

y (Year)

通常の暦の年。

Y (Week-based-year)

「週単位の年」。

その週がどの年に属するかで計算されます。

「YYYY」を使用すると、年末年始に意図しない年(前年や翌年)が表示されるバグの原因になります。

特別な理由がない限り、常に小文字のyyyyまたはuを使用してください。

厳格な解析モードの設定

デフォルトのDateTimeFormatterは、ある程度「スマート」に変換を行います。

例えば、2月30日という不正な日付をパースしようとすると、自動的に2月末日に調整されることがあります。

これを厳密にエラーとして扱いたい場合は、ResolverStyle.STRICTを使用します。

Java
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;

public class StrictExample {
    public static void main(String[] args) {
        // STRICTを使用する場合、年は'u'で指定する必要がある
        DateTimeFormatter strictFormatter = DateTimeFormatter.ofPattern("uuuu/MM/dd")
            .withResolverStyle(ResolverStyle.STRICT);

        try {
            LocalDate.parse("2026/02/30", strictFormatter);
        } catch (Exception e) {
            System.out.println("厳密なチェックによりエラーを検知しました。");
        }
    }
}

秒の精度(ナノ秒)

LocalDateTime.now()などで取得したデータには、OSの精度に応じたナノ秒が含まれています。

ログ出力などでナノ秒が不要な場合は、明示的にtruncatedTo(ChronoUnit.SECONDS)を使用するか、フォーマッタで秒までを指定するようにしてください。

まとめ

Java 8以降のDateTimeFormatterは、それ以前のSimpleDateFormatと比較して、安全性と機能性の両面で飛躍的な進化を遂げました。

本記事のポイントを振り返ります:

スレッドセーフ

staticな定数として定義し、共有して利用できる。

不変性

インスタンスの状態が変わらないため、バグが混入しにくい。

多様なパターン

yyyyMMddなどの記号を組み合わせることで柔軟な表現が可能。

ロケールとタイムゾーン

多言語対応や国際的なシステムにも標準機能で対応可能。

特に「yyyy(年)」と「YYYY(週単位の年)」の違いのような、実務上の重大なトラブルにつながる仕様については、正しく理解しておくことが重要です。

最新のjava.time APIを使いこなし、安全でメンテナンス性の高いコードを記述しましょう。