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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?