- JAVA
- 技術 💻
Javaの日付処理|LocalDateTimeの使い方と7つの落とし穴
Javaで日付や時刻を扱うときは、Java 8で導入された java.time パッケージ(LocalDate・LocalDateTime・ZonedDateTime・Instantなど)を使うのが基本です。古い Date や Calendar、SimpleDateFormat は、値を書き換えられてしまう、スレッドセーフでない、月が0から始まるなどの扱いにくさがあり、新しいコードでは使う理由がほとんどありません。
この記事では、クラスの使い分けと基本操作に加えて、書式の YYYY で年がずれる、月末の加算で日付が変わる、2月30日が通ってしまうといった、実務でつまずく7つの落とし穴を、実際の実行結果つきで解説します。
クラスの使い分け
| クラス | 表すもの | 例 | 主な用途 |
|---|---|---|---|
LocalDate |
日付だけ | 2026-09-27 | 誕生日、締め日、営業日 |
LocalTime |
時刻だけ | 10:30 | 営業開始時刻 |
LocalDateTime |
日付と時刻(タイムゾーンなし) | 2026-09-27T10:30 | 予約日時など、地域が決まっているもの |
ZonedDateTime |
日付と時刻+タイムゾーン | 2026-09-27T10:30+09:00[Asia/Tokyo] | 地域の時間のルール(夏時間)を考慮した計算 |
OffsetDateTime |
日付と時刻+UTCとの時差 | 2026-09-27T10:30+09:00 | APIの送受信、DBへの保存 |
Instant |
世界共通の一瞬(UTC) | 2026-09-27T01:30:00Z | ログ、作成日時・更新日時の記録 |
選び方の目安は次のとおりです。
- 「いつ起きたか」を記録する(作成日時・更新日時・ログ) →
Instant(またはOffsetDateTime) - カレンダー上の日付(誕生日・締め日) →
LocalDate - 地域の中で完結する日時(店舗の予約時刻) →
LocalDateTime - 複数のタイムゾーンをまたぐ、夏時間がある地域 →
ZonedDateTime
LocalDateTime はどの地域の時刻かの情報を持たないため、世界中のユーザーが使うサービスで「発生時刻」を記録する用途には向きません。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Javaの日付処理|LocalDateTimeの使い方と7つの落とし穴」
基本の操作
現在日時の取得と、指定した日時の作成
LocalDate today = LocalDate.now();
LocalDateTime now = LocalDateTime.now();
ZonedDateTime tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
Instant instant = Instant.now();
LocalDate date = LocalDate.of(2026, 9, 27); // 月は1から始まる(Calendarと違う)
LocalDateTime dt = LocalDateTime.of(2026, 9, 27, 10, 30);
now() は、サーバーのデフォルトのタイムゾーンを使います。サーバーがUTCで動いていると、日本時間とは9時間ずれるため、日本時間が必要な場合は ZoneId.of("Asia/Tokyo") を明示するのが安全です。
文字列との変換
DateTimeFormatter f = DateTimeFormatter.ofPattern("uuuu/MM/dd HH:mm");
String s = LocalDateTime.of(2026, 9, 27, 10, 30).format(f); // "2026/09/27 10:30"
LocalDateTime parsed = LocalDateTime.parse("2026/09/27 10:30", f);
LocalDate iso = LocalDate.parse("2026-09-27"); // ISO形式ならフォーマッター不要
| 記号 | 意味 | 例 |
|---|---|---|
uuuu・yyyy |
年 | 2026 |
MM |
月(2桁) | 09 |
M |
月 | 9 |
dd |
日(2桁) | 27 |
HH |
時(0〜23) | 15 |
hh |
時(1〜12、午前午後) | 03 |
mm |
分 | 05 |
ss |
秒 | 00 |
E |
曜日 | 日(Locale.JAPANの場合) |
日付の計算
LocalDate d = LocalDate.of(2026, 9, 27);
d.plusDays(10); // 2026-10-07
d.minusMonths(1); // 2026-08-27
d.plusYears(1); // 2027-09-27
d.with(TemporalAdjusters.lastDayOfMonth()); // 2026-09-30(月末)
d.with(TemporalAdjusters.next(DayOfWeek.MONDAY)); // 次の月曜日
java.timeのクラスは不変(イミュータブル)です。計算メソッドは新しいオブジェクトを返すので、結果を変数で受け取る必要があります。
d.plusDays(10); // × dは変わらない
d = d.plusDays(10); // ○
比較と差分
LocalDate a = LocalDate.of(2026, 1, 1);
LocalDate b = LocalDate.of(2026, 12, 31);
a.isBefore(b); // true
a.isEqual(b); // false
ChronoUnit.DAYS.between(a, b); // 364(日数)
Period.between(a, b); // P11M30D(11か月30日)
Duration.between(t1, t2); // 時間の差(LocalDateTime・Instantなど)
| クラス | 表すもの | 使いどころ |
|---|---|---|
Period |
年・月・日の差 | 「何か月と何日」「年齢」 |
Duration |
秒・ナノ秒の差 | 「何時間何分」「処理時間」 |
ChronoUnit.XXX.between |
1つの単位での差 | 「合計何日」「合計何分」 |
実務でつまずく7つの落とし穴
1. YYYYで年がずれる
LocalDate d = LocalDate.of(2024, 12, 30);
d.format(DateTimeFormatter.ofPattern("YYYY/MM/dd")); // "2025/12/30" ← 1年ずれる
d.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); // "2024/12/30"
大文字の YYYY は、「週を基準にした年」を表します。年末の数日は「翌年の第1週」に含まれることがあるため、12月の最後の数日だけ、年が1つ進んで表示されます。ふだんは正しく表示されるため、テストで気づかず、年末に不具合として表面化する典型的なバグです。
同様に、大文字の DD は「年の中での通算日」です。日付は小文字の dd を使います。
2. MMとmm、HHとhhを取り違える
LocalDateTime t = LocalDateTime.of(2026, 9, 27, 15, 5);
t.format(DateTimeFormatter.ofPattern("yyyy/mm/dd")); // "2026/05/27" ← 月の位置に「分」が入る
t.format(DateTimeFormatter.ofPattern("hh:mm")); // "03:05" ← 15時が3時になる
t.format(DateTimeFormatter.ofPattern("HH:mm")); // "15:05"
大文字のMMが月、小文字のmmが分、大文字のHHが24時間表記、小文字のhhが12時間表記です。hhを使う場合は、午前・午後を表す a と組み合わせないと、午前3時と午後3時の区別がつきません。
3. 2月30日が、エラーにならずに通ってしまう
DateTimeFormatter f = DateTimeFormatter.ofPattern("uuuu/MM/dd");
LocalDate.parse("2026/02/30", f); // 2026-02-28 ← エラーにならず、月末に丸められる
ofPattern で作ったフォーマッターは、既定では存在しない日付を近い日付に丸めて解析します(SMARTモード)。入力チェックに使うと、誤った日付を受け付けてしまいます。厳密に検査するには、STRICTモードを指定します。
DateTimeFormatter strict = DateTimeFormatter.ofPattern("uuuu/MM/dd")
.withResolverStyle(ResolverStyle.STRICT);
LocalDate.parse("2026/02/30", strict); // DateTimeParseException
STRICTモードでは、年を yyyy ではなく uuuu で書く必要があります。yyyy は「紀元の中での年」を表すため、STRICTでは紀元(西暦・紀元前)の情報が足りず、正しい日付でも解析に失敗します。
DateTimeFormatter ng = DateTimeFormatter.ofPattern("yyyy/MM/dd")
.withResolverStyle(ResolverStyle.STRICT);
LocalDate.parse("2026/02/28", ng); // DateTimeParseException(正しい日付なのに失敗)
なお、LocalDate.parse("2026-02-30") のようにISO形式の標準のフォーマッターは、はじめからSTRICTなので例外になります。
4. 月末の加算で日付が変わる
LocalDate.of(2026, 1, 31).plusMonths(1); // 2026-02-28
LocalDate.of(2024, 1, 31).plusMonths(1); // 2024-02-29(うるう年)
LocalDate.of(2026, 1, 31).plusMonths(1).plusMonths(1); // 2026-03-28
LocalDate.of(2026, 1, 31).plusMonths(2); // 2026-03-31
存在しない日付(2月31日)になる場合、その月の末日に調整されます。そのため、1か月ずつ足していくと、一度28日に丸められた日付が元に戻りません。毎月の引き落とし日や契約の更新日を計算するときは、「基準日から何か月後か」で毎回計算するか、「月末」という指定なら TemporalAdjusters.lastDayOfMonth() を使います。
5. 夏時間のある地域で、1日が24時間ではない
日本には夏時間がありませんが、海外のユーザーやサーバーを扱う場合は注意が必要です。
ZoneId ny = ZoneId.of("America/New_York");
ZonedDateTime t = ZonedDateTime.of(2026, 3, 8, 1, 30, 0, 0, ny); // 夏時間が始まる日
t.plusHours(1); // 2026-03-08T03:30-04:00 ← 2時台は存在しない
t.plusDays(1); // 2026-03-09T01:30-04:00 ← 「翌日の同じ時刻」
Duration.between(t, t.plusDays(1)); // PT23H ← この日は23時間しかない
plusDays(1) は「翌日の同じ時刻」、plusHours(24) は「24時間後」で、夏時間の切り替わりの日には結果が変わります。どちらの意味が必要かを考えて使い分けます。LocalDateTime では夏時間を考慮できないため、こうした計算には ZonedDateTime を使います。
6. テストで「今日」が変わってしまう
LocalDate.now() を処理の中で直接呼ぶと、テストを実行する日によって結果が変わり、月末や年末だけ失敗するテストが生まれます。Clock を外から渡せるようにしておくと、テストで日時を固定できます。
public class BillingService {
private final Clock clock;
public BillingService(Clock clock) { this.clock = clock; }
public LocalDate closingDate() {
return LocalDate.now(clock).with(TemporalAdjusters.lastDayOfMonth());
}
}
// 本番
new BillingService(Clock.system(ZoneId.of("Asia/Tokyo")));
// テスト:日時を固定する
Clock fixed = Clock.fixed(Instant.parse("2026-12-31T00:00:00Z"), ZoneId.of("Asia/Tokyo"));
new BillingService(fixed);
7. 年齢の計算でうるう年を考慮し忘れる
LocalDate birthday = LocalDate.of(2000, 2, 29);
Period.between(birthday, LocalDate.of(2026, 2, 28)).getYears(); // 25
年齢は Period.between(誕生日, 基準日).getYears() で計算できます。ただし、2月29日生まれの人が、うるう年でない年のいつ年を取るかは、業務のルールによって扱いが異なる場合があります。年齢で料金や資格が変わるシステムでは、どの日を基準にするかを仕様として確認します。
データベースとの対応
JDBC 4.2以降、java.timeのクラスをそのまま読み書きできます。
| DBの型 | 対応するJavaのクラス |
|---|---|
| DATE | LocalDate |
| TIME | LocalTime |
| TIMESTAMP(タイムゾーンなし) | LocalDateTime |
| TIMESTAMP WITH TIME ZONE | OffsetDateTime(PostgreSQLでは Instant も可) |
ps.setObject(1, LocalDate.of(2026, 9, 27));
LocalDate d = rs.getObject("birthday", LocalDate.class);
作成日時・更新日時のように「いつ起きたか」を表す列は、タイムゾーンを含む型(またはUTCで統一したTIMESTAMP)で保存すると、サーバーの設定が変わっても意味がずれません。
古いDate・Calendarとの変換
古いライブラリとのやりとりで、Dateが必要になる場合は、Instantを経由して変換します。
// Date → LocalDateTime
LocalDateTime ldt = LocalDateTime.ofInstant(date.toInstant(), ZoneId.of("Asia/Tokyo"));
// LocalDateTime → Date
Date date = Date.from(ldt.atZone(ZoneId.of("Asia/Tokyo")).toInstant());
SimpleDateFormatはスレッドセーフではありません。static変数などで共有して複数のスレッドから使うと、日付が壊れることがあります。DateTimeFormatterは不変でスレッドセーフなので、定数として共有できます。
和暦で表示する
JapaneseDate jd = JapaneseDate.of(2026, 9, 27);
jd.format(DateTimeFormatter.ofPattern("GGGGy年M月d日", Locale.JAPAN)); // 令和8年9月27日
よくある質問
Q. Spring BootでJSONに変換すると、日付が配列や数値になります。
A. Jacksonの設定で、日付を文字列(ISO形式)で出力するようにします。Spring Bootでは spring.jackson.serialization.write-dates-as-timestamps=false を設定するか、項目に @JsonFormat(pattern = "uuuu-MM-dd") を付けます。
Q. LocalDateTimeとZonedDateTimeは、どちらを使うべきですか? A. 日本国内だけで完結し、夏時間を考慮する必要がない業務なら、LocalDateTimeで十分なことが多いです。複数の国・地域のユーザーを扱う、または「世界共通の一瞬」を記録する場合は、ZonedDateTimeやInstantを使います。
Q. 2つの日時の差を「○時間○分」で表示したい。
A. Duration.between(t1, t2) で差を求め、toHours() と toMinutesPart()(Java 9以降)で取り出します。
Q. 営業日(土日祝を除く日)の計算はできますか?
A. 土日は getDayOfWeek() で判定できますが、祝日は標準のAPIに含まれていません。祝日の一覧をデータとして持ち、それと組み合わせて判定します。
まとめ
- 日付処理は java.time を使う。Date・Calendar・SimpleDateFormatは新しいコードでは使わない
- 記録はInstant、カレンダーの日付はLocalDate、地域内の日時はLocalDateTime、タイムゾーンの計算はZonedDateTime
- 書式は
uuuu(またはyyyy)・MM・dd・HH・mm。YYYYとDDは別の意味 - 入力チェックは STRICTモード+
uuuu。既定では2月30日が月末に丸められる - 月末の加算は丸められる。繰り返し足さず、基準日から計算する
- 夏時間のある地域では、plusDays(1)とplusHours(24)は別物
- 現在日時は Clock を渡して、テストで固定できるようにする
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Javaの日付処理|LocalDateTimeの使い方と7つの落とし穴」
株式会社RJCでは、日付処理のような「一見かんたんで、奥が深い」テーマこそ、実際に動かして確かめることを大切にしています。こうした知識を仲間と共有しながら、一緒に成長を楽しんでいます。
株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。