LocalDate で日付を扱う
このレッスンで分かること
LocalDate.of(2024, 1, 15)で日付 (年月日) を生成。タイムゾーンや時刻は持たないnow()で今日、parse("2024-01-15")で ISO 形式から生成DateCalendarは古い API。新規コードはjava.time.LocalDateを使うLocalDateはイミュータブル ─plusDays(7)などは新しいインスタンスを返す
LocalDate で日付を扱う とは
Java 8 で導入された java.time.LocalDate を使って、日付の生成・加減算・整形を学び、年月日から ISO 形式の文字列を返すコードを書いてみよう。
なぜ今さら LocalDate を学ぶのか
業務システムを作っていると、必ず一度はぶつかるのが「日付の扱い」です。注文日、誕生日、ログのタイムスタンプ、月末締めの請求日。Java のアプリで日付を扱おうと検索すると、古い記事には java.util.Date や Calendar の例がたくさん出てきます。しかし現代の Java (バージョン 8 以降) では、日付の正解は java.time.LocalDate です。本章ではこの LocalDate を中心に、新しい日時 API の使い方を一気に身につけていきます。
Java 8(2014 年) でjava.timeパッケージが追加されてからもう 10 年以上経ちます。それ以前のDateCalendarは設計に難があり、可変オブジェクトでスレッドセーフでなく、月が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) オブジェクト
もう一つの大事な特徴が 不変性 です。LocalDate の plusDays 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); のように代入し直すか、別変数に受けます。
メソッドチェーンで LocalDate.of(2026, 5, 19).plusDays(7).minusMonths(1).toString() のように書けば、一行で連続した日付計算ができます。元の LocalDate は変わらず、毎回新しいオブジェクトが返ってくるので、副作用を気にせずに繋げていけるのが気持ちいいところです。
toString() は ISO 形式の YYYY-MM-DD
LocalDate の toString() は 既定で 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) のクラス。ほとんどのメソッドが@Deprecatedjava.util.Calendar—Calendar.getInstance()で取ってadd(Calendar.MONTH, 1)のように使う。月が0始まりで間違えやすいjava.text.SimpleDateFormat— 整形クラス。スレッドセーフでない ため、staticフィールドに置くとマルチスレッドで壊れる
これらに対する java.time 側の対応は下記のとおりです。
| やりたいこと | 旧 API (使わない) | 新 API (java.time) |
|---|---|---|
| 日付だけ | java.util.Date | LocalDate |
| 日時 | Date + Calendar | LocalDateTime |
| タイムゾーン込み | Calendar + TimeZone | ZonedDateTime |
| 整形 | SimpleDateFormat | DateTimeFormatter |
| 期間 | 自前で計算 | Period / Duration |
既存プロジェクトの保守で
Dateが出てきたら、date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()のように一度LocalDateに変換してから扱うのが安全です。新規ロジックを旧 API で書き足すのは負債を増やすだけなので避けましょう。
よくある間違い
初学者が LocalDate を使い始めたときにつまずきやすいポイントを 3 つ挙げます。
java.util.Dateを使ってしまう — IDE の自動importでDateを選ぶと、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 日後」「月初」「月末」のような計算を
plusDayswithDayOfMonth(1)で表現 - 予約システム ─ 「今日以降の日付しか入れられない」ようなバリデーションを
isAfterisBeforeで書く - 契約期間の管理 ─
Period.between(start, end)で「契約 N 年 M ヶ月」を計算 - バッチの日付パラメータ ─
--date=2024-01-15のような CLI 引数をLocalDate.parseで受け取る
コードレビューで指摘されがちなポイント
PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。
- まだ
DateCalendarを使っている ─ 新規コードではjava.timeを使う。SimpleDateFormatではなくDateTimeFormatter LocalDateをmutableだと思って.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 は使わない。
やってみよう
それでは今回の課題に挑戦しましょう。やることは次のとおりです。
- ファイル先頭で
import java.time.LocalDate;を書く Solution.formatDate(int year, int month, int day)を完成させる- メソッド内で
LocalDate.of(year, month, day)を呼び、その結果のtoString()をreturnする
例として formatDate(2026, 5, 19) を呼ぶと "2026-05-19" という文字列を返してほしいです。LocalDate の toString() は自動でゼロ埋めしてくれるので、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 を眺めてみるのもおすすめです。
次のレッスンでは、LocalDate を DateTimeFormatter で整形して 2026/05/19 や 2026年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 つができることを確認しましょう。
- LocalDate の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. LocalDate とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- ファイル先頭で
import java.time.LocalDate;を書くこと (java.util.Dateは使わない) LocalDate.of(year, month, day)でLocalDateインスタンスを生成すること- 生成した
LocalDateのtoString()の結果 (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"