日付パースは「文字列をちゃんとした日付オブジェクトにする」作業
業務システムでは、画面入力、CSV、外部 API などから、日付はまず「文字列」として届きます。"2025/01/14" や "2025-01-14 09:30" のような文字列を、そのままでは計算や比較に使えないので、Java の日付型(LocalDate や LocalDateTime)に変換する必要があります。
この「文字列 → 日付オブジェクト」への変換が、日付パースです。
ここを雑にやると、「一部の形式だけ通る」「エラー時の扱いがバラバラ」「タイムゾーンがずれる」といった、地味に厄介なバグが増えていきます。
だからこそ、最初に「正しい道具」と「統一ルール」を押さえておくのが大事です。
大前提:java.time(LocalDate / LocalDateTime)+ DateTimeFormatter を使う
古い Date / SimpleDateFormat ではなく、新しい API を使う
Java 8 以降では、日付・時刻は基本的に java.time パッケージを使うのが前提です。LocalDate(日付だけ)、LocalDateTime(日付+時刻)、ZonedDateTime(タイムゾーン付き)などがあり、フォーマット/パースには DateTimeFormatter を使います。
昔の Date や Calendar、SimpleDateFormat もまだ存在しますが、API が分かりづらく、スレッドセーフでもないため、
新しく書く業務コードでは「まず java.time を使う」と決めてしまって構いません。
LocalDate と LocalDateTime のイメージを固める
ざっくりこうイメージしておくと楽です。
LocalDate は「日付だけ」(例: 2025-01-14)。LocalDateTime は「日付+時刻」(例: 2025-01-14T09:30:00)。
画面で「生年月日」を扱うなら LocalDate、
「注文日時」「更新日時」のように時刻まで必要なら LocalDateTime を使う、という使い分けが基本です。
基本形:固定フォーマットの文字列を LocalDate にパースする
yyyy/MM/dd 形式の文字列を LocalDate に変換する
まずは一番よくある、「yyyy/MM/dd 形式の文字列を LocalDate に変換する」例からいきます。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class DateParseBasic {
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ここで押さえておきたいポイントは二つです。
一つ目は、「フォーマットパターンを文字列で指定する」ということ。"yyyy/MM/dd" のように、y が年、M が月、d が日を表します(大文字・小文字に意味があります)。
二つ目は、「DateTimeFormatter を static final で再利用する」ということ。DateTimeFormatter は不変でスレッドセーフなので、毎回 ofPattern するのではなく、共通の定数として持っておくと、コードがスッキリし、パフォーマンス的にも安定します。
パース失敗時に例外が飛ぶことを体感する
LocalDate.parse は、形式が合わない文字列を渡すと DateTimeParseException を投げます。
LocalDate.parse("2025-01-14", DATE_FORMAT); // パターンは yyyy/MM/dd → 例外
LocalDate.parse("2025/13/01", DATE_FORMAT); // 13 月は存在しない → 例外
LocalDate.parse("abc", DATE_FORMAT); // 全く違う → 例外
Javaここで大事なのは、「日付パースは普通に失敗し得る」という感覚を持つことです。
ユーザー入力や外部データを相手にするとき、「常に成功する」と思い込むのは危険です。
実務で使う「安全な日付パースユーティリティ」
例外を外に漏らさない parseOrNull パターン
業務コードのあちこちで try-catch を書くのはつらいので、ユーティリティ側で例外を吸収してしまうのが定番です。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
public final class Dates {
private static final DateTimeFormatter VIEW_DATE_FORMAT =
DateTimeFormatter.ofPattern("yyyy/MM/dd");
private Dates() {}
public static LocalDate parseViewDateOrNull(String text) {
if (text == null || text.isBlank()) {
return null;
}
try {
return LocalDate.parse(text, VIEW_DATE_FORMAT);
} catch (DateTimeParseException e) {
return null;
}
}
}
Java呼び出し側はこう書けます。
String input = getBirthDateInput(); // 画面からの入力
LocalDate birthDate = Dates.parseViewDateOrNull(input);
if (birthDate == null) {
// 「日付の形式が正しくありません」などのエラー表示
}
Javaここで深掘りしたいのは、「例外を業務ロジックにばらまかず、ユーティリティで『成功/失敗』の二値に変換する」という設計です。
これにより、画面やバッチのコードは「null なら失敗」というシンプルな前提で書けるようになります。
デフォルト値を返すパターン
「パースできなければ今日の日付にする」「パースできなければ最小値にする」といった要件がある場合は、デフォルト値を返す版を用意しておくと便利です。
public static LocalDate parseOrDefault(String text, LocalDate defaultValue) {
LocalDate result = parseViewDateOrNull(text);
return result != null ? result : defaultValue;
}
Java呼び出し側はこうなります。
LocalDate date = Dates.parseOrDefault(input, LocalDate.now());
Java「失敗したらどうするか」をユーティリティのメソッド名とシグネチャに乗せておくと、呼び出し側の意図がとても読みやすくなります。
日付+時刻(LocalDateTime)のパース
yyyy-MM-dd HH:mm:ss 形式を LocalDateTime に変換する
次は、「日時文字列 → LocalDateTime」のパターンです。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
public final class DateTimes {
private static final DateTimeFormatter DATE_TIME_FORMAT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private DateTimes() {}
public static LocalDateTime parseOrNull(String text) {
if (text == null || text.isBlank()) {
return null;
}
try {
return LocalDateTime.parse(text, DATE_TIME_FORMAT);
} catch (DateTimeParseException e) {
return null;
}
}
}
Java例として、DB の TIMESTAMP を文字列で受け取ってパースするような場面をイメージすると分かりやすいと思います。
ここでも重要なのは、「フォーマットとパースで同じフォーマッタを使う」ことです。format するときは "yyyy-MM-dd HH:mm:ss" なのに、parse するときは "yyyy/MM/dd HH:mm:ss" になっている、というような不一致があると、どこかで必ずハマります。
用途ごとにフォーマット/パースをセットで定義する
画面用・CSV 用・API 用を分ける
実務では、次のように「用途ごとに日付形式が違う」ことがよくあります。
画面表示・入力用:yyyy/MM/dd
CSV 入出力用:yyyy-MM-dd
ログ用:yyyy-MM-dd HH:mm:ss.SSS
API(JSON)用:ISO 8601(2025-01-14T09:30:15 など)
これを毎回バラバラに書くのではなく、「用途ごとにフォーマッタとパーサをセットでユーティリティ化する」と、コードの意図がとてもクリアになります。
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
public final class DateFormatters {
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");
private DateFormatters() {}
public static LocalDate parseViewDateOrNull(String text) {
return parseLocalDateOrNull(text, VIEW_DATE);
}
public static LocalDate parseCsvDateOrNull(String text) {
return parseLocalDateOrNull(text, CSV_DATE);
}
private static LocalDate parseLocalDateOrNull(String text, DateTimeFormatter formatter) {
if (text == null || text.isBlank()) return null;
try {
return LocalDate.parse(text, formatter);
} catch (DateTimeParseException e) {
return null;
}
}
public static LocalDateTime parseLogDateTimeOrNull(String text) {
if (text == null || text.isBlank()) return null;
try {
return LocalDateTime.parse(text, LOG_DATE_TIME);
} catch (DateTimeParseException e) {
return null;
}
}
}
Javaこうしておくと、「画面から来た日付は parseViewDateOrNull」「CSV から来た日付は parseCsvDateOrNull」と、
呼び出し側のコードだけ見ても「どの形式を期待しているか」が分かります。
ここで深掘りしたいのは、「フォーマットとパースを『用途』でグルーピングする」という発想です。
「画面用」「CSV 用」「ログ用」といった単位でまとめておくと、仕様変更(例: 画面の日付形式を変えたい)があったときに、どこを直せばいいかが一目で分かります。
タイムゾーン付きの文字列をパースする(軽く触れておく)
ISO 8601 形式の日時文字列を ZonedDateTime にパース
外部 API では、"2025-01-14T09:30:15+09:00" のような ISO 8601 形式の日時文字列がよく使われます。
これはタイムゾーン情報(オフセット)を含んでいるので、ZonedDateTime や OffsetDateTime で扱うのが自然です。
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class ZonedParseExample {
public static void main(String[] args) {
String text = "2025-01-14T09:30:15+09:00";
ZonedDateTime zdt = ZonedDateTime.parse(text, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
System.out.println(zdt); // 2025-01-14T09:30:15+09:00
}
}
JavaISO 系のフォーマッタ(ISO_LOCAL_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_ZONED_DATE_TIME など)は、
「形式を自分でパターン文字列で書かなくてよい」というメリットがあります。
API 連携では、まず「相手がどの ISO 形式を使っているか」を確認し、それに対応するフォーマッタを選ぶ、という流れになります。
旧 API(Date / SimpleDateFormat)からの移行と境界でのパース
SimpleDateFormat を使う場合の注意(レガシー対応)
レガシーなコードや古いライブラリでは、まだ SimpleDateFormat が使われていることがあります。
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class LegacyParseExample {
private static final SimpleDateFormat LEGACY_FORMAT =
new SimpleDateFormat("yyyy/MM/dd");
public static Date parseLegacy(String text) throws ParseException {
return LEGACY_FORMAT.parse(text);
}
}
Javaここで一番危険なのは、「SimpleDateFormat を static で共有してマルチスレッドから使う」ことです。SimpleDateFormat はスレッドセーフではないため、複数スレッドから同時に使うと、結果が壊れたり例外が出たりします。
どうしても使わざるを得ない場合は、毎回 new するか、ThreadLocal<SimpleDateFormat> でスレッドごとに持つ必要があります。
ただし、新しく書くコードでは、できるだけ早く java.time に寄せてしまい、Date/SimpleDateFormat は「外部との境界でだけ使う」方針にするのが安全です。
まとめ:日付パースで初心者が身につけるべき感覚
日付パースは、「なんとなく parse する」ではなく、「どの形式の文字列を、どの型に、どういうルールで変換するか」を設計する作業です。
新しい日付 API(java.time)と DateTimeFormatter を使うのを基本にする。
フォーマットとパースをセットで考え、同じフォーマッタを共有する。
用途ごと(画面、CSV、ログ、API)にフォーマッタとパーサをユーティリティに集約し、「この入口ではこの形式だけ受ける」というルールを決める。
パース失敗時の扱い(例外を投げる/null を返す/デフォルト値を返す)をユーティリティ側で統一し、業務ロジックに例外処理をばらまかない。
ここまでの感覚が身につけば、「日付文字列がうまく読めたり読めなかったりする」ストレスからかなり解放されます。
