日付フォーマットは「人間とシステムの橋渡し」
業務システムでは、日付や日時を「システムが扱いやすい形」と「人間が読みやすい形」の間で何度も行き来します。
DB では DATE や TIMESTAMP、Java では LocalDate や LocalDateTime、画面や CSV では "2025-01-14" や "2025/01/14 09:30" のような文字列。
この「日付 ↔ 文字列」の変換をきちんと設計するのが、日付フォーマットのユーティリティです。
ここを場当たり的に書くと、「画面 A と画面 B でフォーマットが違う」「パースできたりできなかったりする」「タイムゾーンがずれる」といった、地味にキツいバグが量産されます。
だからこそ、最初に「正しい道具」と「統一ルール」を押さえておくのが大事です。
まずは新しい日付 API(java.time)を使うのが大前提
Date / Calendar はもう卒業していい
昔の Java には java.util.Date と java.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)。
タイムゾーンまで含めて扱いたいときは ZonedDateTime や OffsetDateTime を使いますが、まずは 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]
}
}
JavaISO 系のフォーマッタ(ISO_LOCAL_DATE_TIME, ISO_ZONED_DATE_TIME など)は、標準的な形式を自動で使ってくれるので、API やログでとても重宝します。
旧 API(Date / SimpleDateFormat)と付き合うときの注意点
SimpleDateFormat はスレッドセーフではない
レガシーなコードや古いライブラリでは、まだ Date と SimpleDateFormat が使われていることがあります。
ここで一番危険なのは、「SimpleDateFormat を static で共有してマルチスレッドから使う」パターンです。
SimpleDateFormat は内部状態を持つため、複数スレッドから同時に使うと、フォーマット結果が壊れたり、例外が出たりします。
どうしても使わざるを得ない場合は、毎回 new するか、ThreadLocal でスレッドごとに持つ必要があります。
Date と LocalDateTime の相互変換
新旧 API が混在する場合、Date と LocalDateTime / 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 で統一する。
