LocalDate で日付を扱う

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • LocalDate.of(2024, 1, 15) で日付 (年月日) を生成。タイムゾーンや時刻は持たない
  • now() で今日、parse("2024-01-15") で ISO 形式から生成
  • Date Calendar は古い API。新規コードは java.time.LocalDate を使う
  • LocalDate はイミュータブル ─ plusDays(7) などは新しいインスタンスを返す

LocalDate で日付を扱う とは

Java 8 で導入された java.time.LocalDate を使って、日付の生成・加減算・整形を学び、年月日から ISO 形式の文字列を返すコードを書いてみよう。

なぜ今さら LocalDate を学ぶのか

業務システムを作っていると、必ず一度はぶつかるのが「日付の扱い」です。注文日、誕生日、ログのタイムスタンプ、月末締めの請求日。Java のアプリで日付を扱おうと検索すると、古い記事には java.util.DateCalendar の例がたくさん出てきます。しかし現代の Java (バージョン 8 以降) では、日付の正解は java.time.LocalDate です。本章ではこの LocalDate を中心に、新しい日時 API の使い方を一気に身につけていきます。

Java 8 (2014 年) で java.time パッケージが追加されてからもう 10 年以上経ちます。それ以前の Date Calendar は設計に難があり、可変オブジェクトでスレッドセーフでなく、月が 0 始まりなど落とし穴だらけでした。新しい API はその反省を踏まえて作り直されたもので、今から書く新規コードは必ず java.time を使うべきです。

LocalDate は「年・月・日」の 3 要素だけを持つ、タイムゾーンも時刻も含まない純粋な日付 を表すクラスです。たとえば「誕生日」や「祝日」は時刻を持ちません。そういう用途にぴったり収まる軽量なクラスが LocalDate です。時刻も含めたいなら LocalDateTime、タイムゾーン込みなら ZonedDateTime という兄弟クラスもあります。

LocalDate の基本生成方法

LocalDate のインスタンスは new LocalDate(...) のようには作れません。代わりに static ファクトリメソッド を使います。覚えるのは of now parse の 3 つです。

Java

import java.time.LocalDate; public class Demo { public static void main(String[] args) { LocalDate a = LocalDate.of(2026, 5, 19); // 年, 月, 日 LocalDate b = LocalDate.now(); // 今日の日付 (システム時計) LocalDate c = LocalDate.parse("2024-01-01"); // ISO 文字列から復元 System.out.println(a); // 2026-05-19 System.out.println(b); // 例 2026-05-19 System.out.println(c); // 2024-01-01 } }

ここで重要なのが LocalDate.of(2026, 5, 19)月が 1 始まり という点です。5 を渡せばちゃんと 5 月 です。古い Calendar クラスでは Calendar.MAY (= 内部値 4) のように 0 始まりで、これが多くのバグを生んできました。LocalDate ではそんな心配は不要です。

LocalDate.now() はシステム時計を参照します。タイムゾーンが絡む場合は LocalDate.now(ZoneId.of("Asia/Tokyo")) のように明示できます。テストで now() を直接呼ぶと結果が再現しないので、本番コードでは Clock を注入する設計が推奨されますが、入門段階では now() を素直に使って大丈夫です。

LocalDate は不変 (immutable) オブジェクト

もう一つの大事な特徴が 不変性 です。LocalDateplusDays minusMonths などのメソッドは、自分自身を書き換えるのではなく新しい LocalDate を返します。これは関数型プログラミングの考え方を取り入れた設計で、副作用が少なく安全です。

Java

LocalDate today = LocalDate.of(2026, 5, 19); LocalDate tomorrow = today.plusDays(1); // 新しいオブジェクト LocalDate lastMonth = today.minusMonths(1); System.out.println(today); // 2026-05-19 (変わらない) System.out.println(tomorrow); // 2026-05-20 System.out.println(lastMonth); // 2026-04-19

この性質のおかげで、LocalDate はマルチスレッドで共有しても安全ですし、final フィールドに入れて永遠に書き換わらないことを保証できます。逆に、戻り値を捨ててしまうとせっかくの計算が消えるので注意してください。today.plusDays(1); と書いただけでは today は変わりません。必ず today = today.plusDays(1); のように代入し直すか、別変数に受けます。

diagram (will load when visible)

メソッドチェーンで LocalDate.of(2026, 5, 19).plusDays(7).minusMonths(1).toString() のように書けば、一行で連続した日付計算ができます。元の LocalDate は変わらず、毎回新しいオブジェクトが返ってくるので、副作用を気にせずに繋げていけるのが気持ちいいところです。

toString() は ISO 形式の YYYY-MM-DD

LocalDatetoString()既定で YYYY-MM-DD 形式の ISO 8601 文字列 を返します。これは国際標準の日付表記で、ゼロ埋めされた 4 桁の年・2 桁の月・2 桁の日をハイフンで繋いだものです。

  • LocalDate.of(2026, 5, 19).toString()"2026-05-19"
  • LocalDate.of(2024, 1, 1).toString()"2024-01-01"
  • LocalDate.of(2000, 12, 31).toString()"2000-12-31"

月や日は 自動で 2 桁にゼロ埋め されるので、自分で String.format("%04d-%02d-%02d", ...) のような整形をする必要はありません。今回の課題ではまさにこの toString() の結果をそのまま返してもらいます。

他のフォーマット (例えば 2026/05/19令和8年5月19日) で出力したい場合は DateTimeFormatter を併用しますが、それは別レッスンで詳しく扱います。標準形式で良いなら toString() 一発で OK です。

Java

import java.time.LocalDate; import java.time.format.DateTimeFormatter; public class FormatDemo { public static void main(String[] args) { LocalDate d = LocalDate.of(2026, 5, 19); System.out.println(d.toString()); // 2026-05-19 System.out.println(d.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"))); // 2026/05/19 System.out.println(d.format(DateTimeFormatter.BASIC_ISO_DATE)); // 20260519 } }

java.util.Date との違い (旧 API は使わない)

ここで、Java の歴史的な経緯を簡単に整理しておきます。新しいコードで 絶対に使ってはいけない のが、次に挙げる旧 API です。

  • java.util.Date — 「日付」と名乗りつつ実は long のミリ秒を持つ瞬間 (instant) のクラス。ほとんどのメソッドが @Deprecated
  • java.util.CalendarCalendar.getInstance() で取って add(Calendar.MONTH, 1) のように使う。月が 0 始まりで間違えやすい
  • java.text.SimpleDateFormat — 整形クラス。スレッドセーフでない ため、static フィールドに置くとマルチスレッドで壊れる

これらに対する java.time 側の対応は下記のとおりです。

やりたいこと旧 API (使わない)新 API (java.time)
日付だけjava.util.DateLocalDate
日時Date + CalendarLocalDateTime
タイムゾーン込みCalendar + TimeZoneZonedDateTime
整形SimpleDateFormatDateTimeFormatter
期間自前で計算Period / Duration

既存プロジェクトの保守で Date が出てきたら、date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() のように一度 LocalDate に変換してから扱うのが安全です。新規ロジックを旧 API で書き足すのは負債を増やすだけなので避けましょう。

よくある間違い

初学者が LocalDate を使い始めたときにつまずきやすいポイントを 3 つ挙げます。

  • java.util.Date を使ってしまう — IDE の自動 importDate を選ぶと、java.util.Date が候補に出ます。今からの新規コードでは必ず java.time.LocalDate の方を選びましょう。import 文を確認する癖をつけると安全です
  • 月を 0 始まりだと思い込むCalendar 時代の名残で「5 月4」と書いてしまう人がいます。LocalDate.of(2026, 5, 19)5 はそのまま 5 月 です。0 を渡すと DateTimeException が出ます
  • 戻り値を捨てるdate.plusDays(1); と書いても、戻り値を受け取らないと意味がありません。LocalDate は不変なので、必ず date = date.plusDays(1); のように再代入するか、別の変数に受けてください

もうひとつ、null を入れた LocalDate 変数を使うと NullPointerException になります。LocalDate today = null; の後に today.plusDays(1) のように呼ぶと一発で落ちるので、null を意図的に許容するなら Optional<LocalDate> を使うか、初期化時にちゃんとした値を入れるようにしましょう。

例外の話に関連して、LocalDate.of(2026, 2, 30) のように 存在しない日付 を渡すと DateTimeException がスローされます。2 月 30 日 は存在しないので、コンパイラは止められませんが実行時にしっかりエラーになります。ユーザー入力から日付を組み立てるときは try-catch で囲んでおくと安全です。

parse で文字列から LocalDate を復元する

外部から日付を受け取るとき、たとえば JSON"birthday": "1990-04-01" のような文字列を扱うときは LocalDate.parse が便利です。ISO 8601 形式 (YYYY-MM-DD) の文字列ならそのまま渡せます。

Java

LocalDate d = LocalDate.parse("1990-04-01"); System.out.println(d.getYear()); // 1990 System.out.println(d.getMonthValue()); // 4 System.out.println(d.getDayOfMonth()); // 1

別のフォーマット (例 1990/04/01) を読みたい場合は DateTimeFormatter.ofPattern("yyyy/MM/dd") を第 2 引数に渡します。Web フォームや CSV ファイルから日付を取り込むときに重宝するので、覚えておくと役に立ちます。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • 勤怠管理 / シフト管理 ─ 「今日から 7 日後」「月初」「月末」のような計算を plusDays withDayOfMonth(1) で表現
  • 予約システム ─ 「今日以降の日付しか入れられない」ようなバリデーションを isAfter isBefore で書く
  • 契約期間の管理Period.between(start, end) で「契約 N 年 M ヶ月」を計算
  • バッチの日付パラメータ--date=2024-01-15 のような CLI 引数を LocalDate.parse で受け取る

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • まだ Date Calendar を使っている ─ 新規コードでは java.time を使う。SimpleDateFormat ではなく DateTimeFormatter
  • LocalDatemutable だと思って .plusDays(7) の戻り値を捨てている ─ イミュータブルなので戻り値の代入必須
  • ハードコードした文字列で年月日を組み立てているString.format("%d-%d-%d", ...) ではなく LocalDate.of を使う
  • タイムゾーン不要なのに ZonedDateTime を持ち回している ─ 日付だけなら LocalDate、時刻も必要なら LocalDateTime

パフォーマンス考慮事項

  • 生成コストは小さいLocalDate.now() は 1μs 未満。ループ内でも気にならないレベル
  • String → LocalDate のパースは正規表現付き ─ パース呼び出しの累積はそこそこ重い。大量データではフォーマッタを使い回す
  • DateTimeFormatter はスレッドセーフSimpleDateFormat と違い、static final に持って共有できる
  • Period の計算は月の長さ次元で複雑 ─ 単純な日数差なら ChronoUnit.DAYS.between(a, b) のほうが直感的
この章のポイント

ここまでの要点 LocalDate.of / now / parse で生成、イミュータブルで plusDays 等は新インスタンスを返す。Date Calendar は使わない。

やってみよう

それでは今回の課題に挑戦しましょう。やることは次のとおりです。

  1. ファイル先頭で import java.time.LocalDate; を書く
  2. Solution.formatDate(int year, int month, int day) を完成させる
  3. メソッド内で LocalDate.of(year, month, day) を呼び、その結果の toString()return する

例として formatDate(2026, 5, 19) を呼ぶと "2026-05-19" という文字列を返してほしいです。LocalDatetoString() は自動でゼロ埋めしてくれるので、formatDate(2024, 1, 1)"2024-01-01" になります ("2024-1-1" ではありません)。String.format を使う必要はなく、LocalDate.of(year, month, day).toString() の一行で OK です。

慣れてきたら、plusDays minusMonths plusYears などのメソッドも試してみてください。たとえば「100 日後の日付を返すメソッド」や「来月の同じ日を返すメソッド」を自作してみると、LocalDate の感覚が一気に身につきます。getDayOfWeek() で曜日を取り出したり、isLeapYear() でうるう年判定をしたりと、便利メソッドもたくさん用意されているので、Javadoc を眺めてみるのもおすすめです。

次のレッスンでは、LocalDateDateTimeFormatter で整形して 2026/05/192026年5月19日 といった任意のフォーマットに変換する方法を学びます。まずはここで「of で作って toString() で文字列化する」という基本の流れを身につけておきましょう。

よくある質問

Q. 中級の内容は実務でどれくらい使いますか?

A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。

Q. Stream と for ループはどっちで書くべき?

A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。

Q. 次のステップでは何を学ぶべきですか?

A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。

次のレッスン

次は LocalDateTime で時刻も扱う で、LocalDateTime で時刻も扱う を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. LocalDate の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. LocalDate とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. ファイル先頭で import java.time.LocalDate; を書くこと (java.util.Date は使わない)
  2. LocalDate.of(year, month, day)LocalDate インスタンスを生成すること
  3. 生成した LocalDatetoString() の結果 (ISO 8601 形式 YYYY-MM-DD) を return すること

入出力例

test-cases.txt

formatDate(2026, 5, 19)"2026-05-19" formatDate(2024, 1, 1)"2024-01-01" formatDate(2000, 12, 31)"2000-12-31" formatDate(1990, 4, 1)"1990-04-01"

ヒント

main.java
main.java
学習モード

メモ

LocalDate で日付を扱う

⌘S で保存