Java Tips | 基本ユーティリティ:日付フォーマット

Java Java
スポンサーリンク

日付フォーマットは「人間とシステムの橋渡し」

業務システムでは、日付や日時を「システムが扱いやすい形」と「人間が読みやすい形」の間で何度も行き来します。
DB では DATETIMESTAMP、Java では LocalDateLocalDateTime、画面や CSV では "2025-01-14""2025/01/14 09:30" のような文字列。
この「日付 ↔ 文字列」の変換をきちんと設計するのが、日付フォーマットのユーティリティです。

ここを場当たり的に書くと、「画面 A と画面 B でフォーマットが違う」「パースできたりできなかったりする」「タイムゾーンがずれる」といった、地味にキツいバグが量産されます。
だからこそ、最初に「正しい道具」と「統一ルール」を押さえておくのが大事です。


まずは新しい日付 API(java.time)を使うのが大前提

Date / Calendar はもう卒業していい

昔の Java には java.util.Datejava.util.Calendar しかなく、フォーマットには SimpleDateFormat を使っていました。
これは API が分かりにくく、スレッドセーフでもなく、バグの温床になりがちです。

Java 8 以降では、java.time パッケージ(LocalDate, LocalDateTime, ZonedDateTime, DateTimeFormatter など)が導入されました。
今から新しく書く業務コードでは、基本的にこの新しい日付 API を使う、と決めてしまって構いません。

LocalDate / LocalDateTime のイメージ

ざっくり言うと、こう覚えると楽です。

LocalDate は「日付だけ」(例: 2025-01-14)。
LocalDateTime は「日付+時刻」(例: 2025-01-14T09:30:00)。

タイムゾーンまで含めて扱いたいときは ZonedDateTimeOffsetDateTime を使いますが、まずは LocalDate / LocalDateTime とフォーマットの組み合わせから押さえれば十分です。


DateTimeFormatter を使った基本のフォーマット

LocalDate → 文字列のフォーマット

一番基本の例からいきます。

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

public class DateFormatExample {

    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2025, 1, 14);

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
        String formatted = date.format(formatter);

        System.out.println(formatted);  // 2025/01/14
    }
}
Java

ポイントは二つです。

一つ目は、「フォーマットパターンを文字列で指定する」ということ。
"yyyy/MM/dd" のように、y が年、M が月、d が日を表します(大文字・小文字に意味があります)。

二つ目は、「DateTimeFormatter を毎回 new せず、再利用できる」ということ。
DateTimeFormatter は不変(イミュータブル)でスレッドセーフなので、static final にして共通ユーティリティとして使い回してよいクラスです。

LocalDateTime → 文字列のフォーマット

時刻まで含めたい場合は、パターンを少し伸ばすだけです。

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

public class DateTimeFormatExample {

    private static final DateTimeFormatter DATE_TIME_FORMATTER =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        LocalDateTime dateTime = LocalDateTime.of(2025, 1, 14, 9, 30, 15);

        String formatted = dateTime.format(DATE_TIME_FORMATTER);

        System.out.println(formatted);  // 2025-01-14 09:30:15
    }
}
Java

ここで深掘りしておきたいのは、「フォーマットパターンを文字列でベタ書きするのではなく、定数として名前を付ける」という設計です。
"yyyy-MM-dd HH:mm:ss" とだけ書かれていると、「これはどこ向けのフォーマットか?」がコードから読み取りにくいですが、

private static final DateTimeFormatter DB_TIMESTAMP_FORMAT =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
Java

のように名前を付けておけば、「これは DB 用のタイムスタンプ形式だな」と一目で分かります。


文字列 → LocalDate / LocalDateTime のパース

文字列から LocalDate に変換する

フォーマットとセットで必ず出てくるのが「パース」です。
画面や CSV から "2025/01/14" のような文字列を受け取り、それを LocalDate に変換するパターンです。

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

public class DateParseExample {

    private static final DateTimeFormatter DATE_FORMAT =
            DateTimeFormatter.ofPattern("yyyy/MM/dd");

    public static LocalDate parseDate(String text) {
        return LocalDate.parse(text, DATE_FORMAT);
    }

    public static void main(String[] args) {
        LocalDate date = parseDate("2025/01/14");
        System.out.println(date);  // 2025-01-14
    }
}
Java

ここで重要なのは、「フォーマットとパースで同じ DateTimeFormatter を使う」ということです。
フォーマットは "yyyy/MM/dd" なのに、パースは "yyyy-MM-dd" でやっている、というような不一致があると、どこかで必ずバグになります。

パース失敗時の扱いをユーティリティに閉じ込める

LocalDate.parse は、形式が合わない文字列を渡すと DateTimeParseException を投げます。
業務コードのあちこちでこれを try-catch するのはつらいので、ユーティリティ側で吸収してしまうのが定番です。

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

public final class Dates {

    private static final DateTimeFormatter DATE_FORMAT =
            DateTimeFormatter.ofPattern("yyyy/MM/dd");

    private Dates() {}

    public static LocalDate parseOrNull(String text) {
        if (text == null || text.isBlank()) {
            return null;
        }
        try {
            return LocalDate.parse(text, DATE_FORMAT);
        } catch (DateTimeParseException e) {
            return null;
        }
    }
}
Java

呼び出し側はこうなります。

String input = getInputDate();  // 画面からの入力

LocalDate date = Dates.parseOrNull(input);
if (date == null) {
    // エラーメッセージを出すなど
}
Java

ここで深掘りしたいのは、「フォーマットとバリデーションをセットで考える」という感覚です。
「この画面では yyyy/MM/dd 形式で入力してもらう」と決めたら、その形式でしかパースしない。
パースできなければ「形式が違う」としてエラーにする。
このルールをユーティリティに閉じ込めておくと、画面ごとにバラバラなチェックを書く必要がなくなります。


実務で使いやすい日付フォーマットユーティリティの形

用途ごとにフォーマッタを分ける

業務では、次のように「用途ごとにフォーマットが違う」ことがよくあります。

画面表示用:yyyy/MM/dd
CSV 出力用:yyyy-MM-dd
ログ用:yyyy-MM-dd HH:mm:ss.SSS
API(JSON)用:ISO 8601(2025-01-14T09:30:15 など)

これを毎回 ofPattern で書くのではなく、ユーティリティクラスにまとめておくと、コードの意図がとても読みやすくなります。

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

public final class DateFormats {

    public static final DateTimeFormatter VIEW_DATE =
            DateTimeFormatter.ofPattern("yyyy/MM/dd");

    public static final DateTimeFormatter CSV_DATE =
            DateTimeFormatter.ofPattern("yyyy-MM-dd");

    public static final DateTimeFormatter LOG_DATE_TIME =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");

    public static final DateTimeFormatter ISO_DATE_TIME =
            DateTimeFormatter.ISO_LOCAL_DATE_TIME;

    private DateFormats() {}

    public static String formatForView(LocalDate date) {
        return date != null ? date.format(VIEW_DATE) : "";
    }

    public static String formatForCsv(LocalDate date) {
        return date != null ? date.format(CSV_DATE) : "";
    }

    public static String formatForLog(LocalDateTime dateTime) {
        return dateTime != null ? dateTime.format(LOG_DATE_TIME) : "";
    }
}
Java

呼び出し側は、用途に応じてメソッド名だけ見れば意図が分かります。

String viewText = DateFormats.formatForView(birthDate);
String csvText  = DateFormats.formatForCsv(orderDate);
String logText  = DateFormats.formatForLog(now);
Java

ここでのキモは、「フォーマットパターンを散らさない」ことです。
プロジェクトのどこか一箇所に「日付フォーマットの正解」を集約しておくと、「画面ごとに微妙に違う」「誰かが勝手に別の形式を使い始める」といった事故を防げます。


タイムゾーンと ZonedDateTime の話を少しだけ

ローカル時間とタイムゾーン付き時間の違い

LocalDateTime は「タイムゾーンを持たない日付+時刻」です。
「2025-01-14 09:30」という情報だけで、「それは日本時間か?UTC か?」という情報は含まれていません。

一方、ZonedDateTime は「タイムゾーン付きの日時」です。
「2025-01-14 09:30 JST」のように、「どこの時間か」まで含めて扱えます。

ログや外部 API では、「UTC で出す」「タイムゾーンを明示する」といったポリシーを決めておくと、時差による混乱を防げます。

ZonedDateTime のフォーマット例

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

public class ZonedDateTimeExample {

    private static final DateTimeFormatter ISO_ZONED =
            DateTimeFormatter.ISO_ZONED_DATE_TIME;

    public static void main(String[] args) {
        ZonedDateTime nowJst = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));

        String text = nowJst.format(ISO_ZONED);
        System.out.println(text);  // 例: 2025-01-14T09:30:15.123+09:00[Asia/Tokyo]
    }
}
Java

ISO 系のフォーマッタ(ISO_LOCAL_DATE_TIME, ISO_ZONED_DATE_TIME など)は、標準的な形式を自動で使ってくれるので、API やログでとても重宝します。


旧 API(Date / SimpleDateFormat)と付き合うときの注意点

SimpleDateFormat はスレッドセーフではない

レガシーなコードや古いライブラリでは、まだ DateSimpleDateFormat が使われていることがあります。
ここで一番危険なのは、「SimpleDateFormat を static で共有してマルチスレッドから使う」パターンです。

SimpleDateFormat は内部状態を持つため、複数スレッドから同時に使うと、フォーマット結果が壊れたり、例外が出たりします。
どうしても使わざるを得ない場合は、毎回 new するか、ThreadLocal でスレッドごとに持つ必要があります。

Date と LocalDateTime の相互変換

新旧 API が混在する場合、DateLocalDateTime / Instant の相互変換が必要になります。

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

public class LegacyConversion {

    public static LocalDateTime toLocalDateTime(Date date) {
        return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
    }

    public static Date toDate(LocalDateTime dateTime) {
        Instant instant = dateTime.atZone(ZoneId.systemDefault()).toInstant();
        return Date.from(instant);
    }
}
Java

ただし、新しく書くコードでは、できるだけ早い段階で java.time に寄せてしまい、
Date は「外部との境界でだけ使う」という方針にしておくと、日付周りのバグがかなり減ります。


まとめ:日付フォーマットで初心者が身につけるべき感覚

日付フォーマットは、「とりあえず文字列にする」ではなく、「誰に、どこで、どの形式で見せるか」を設計する作業です。

新しい日付 API(java.time)と DateTimeFormatter を使うのを基本にする。
フォーマットパターンをあちこちに書かず、用途ごとのフォーマッタをユーティリティに集約する。
フォーマットとパースをセットで考え、「この形式で出したものは、この形式でしか受けない」というルールを決める。
タイムゾーンや旧 API との変換は、「境界でだけ頑張る」方針にして、内側は LocalDate / LocalDateTime で統一する。

タイトルとURLをコピーしました