日付の加減算
日付の加減算 とは
LocalDate の plusDays や minusDays を使って日付を前後に動かす方法を学び、不変オブジェクトとしての扱い方を身につけよう。
日付を前後に動かす — LocalDate の加減算
業務システムでは「予約日の 3 日前にリマインドを送る」「契約開始日から 30 日後の更新日を計算する」「過去 7 日間のログを集計する」など、日付を前後に動かしたい場面が山ほどあります。Java の LocalDate には、こうした計算をシンプルに書ける plusDays plusMonths plusYears といった一連のメソッドが用意されています。本章では、この日付の加減算をしっかり身につけて、月またぎやうるう年に怯えないコードを書けるようになりましょう。
昔の Java では
java.util.Calendarのadd(Calendar.DAY_OF_MONTH, 10)のような呼び出しで日付を動かしていました。書き方が冗長でCalendarのインスタンスが書き換わってしまうため、バグの温床として悪名高い API でした。Java8以降に登場したjava.timeパッケージのLocalDateは、それを反省して不変オブジェクトとして設計されています。
plusDays の基本
まずは一番よく使う plusDays から見ていきます。LocalDate のインスタンスに対して plusDays(n) を呼ぶと、n 日後の LocalDate が新しいオブジェクトとして返ってきます。
Java
import java.time.LocalDate;
public class Demo {
public static void main(String[] args) {
LocalDate today = LocalDate.of(2026, 5, 19);
LocalDate tenDaysLater = today.plusDays(10);
System.out.println(today); // 2026-05-19 (変わらない)
System.out.println(tenDaysLater); // 2026-05-29
}
}ここで大事なのは、today.plusDays(10) を呼んでも today 自身は 2026-05-19 のまま変わらない点です。新しい LocalDate が戻り値として返ってくるので、それを変数 tenDaysLater で受け取っています。LocalDate のような 不変 (immutable) オブジェクト は「中身を書き換える」のではなく「新しいインスタンスを作って返す」設計です。
plus* ファミリーと月またぎ・うるう年
plusDays の仲間には次のようなメソッドがあります。日 / 月 / 年 / 週それぞれを動かせます。
plusDays(long days)—days日後plusWeeks(long weeks)—weeks週後 (7日 ×weeks)plusMonths(long months)—monthsか月後plusYears(long years)—years年後
月またぎや年またぎは Java が自動で面倒を見てくれます。2024-01-31 に 1 日足せば 2024-02-01 になりますし、2024-02-28 に 1 日足せばうるう年なので 2024-02-29 になります。2025 年なら平年なので、2025-02-28 に 1 日足すと 2025-03-01 です。自分で「2 月は 28 日? 29 日?」と判定する必要はありません。
Java
LocalDate a = LocalDate.of(2024, 1, 31).plusDays(1); // 2024-02-01
LocalDate b = LocalDate.of(2024, 2, 28).plusDays(1); // 2024-02-29 (うるう年)
LocalDate c = LocalDate.of(2025, 2, 28).plusDays(1); // 2025-03-01 (平年)
LocalDate d = LocalDate.of(2026, 1, 31).plusMonths(1); // 2026-02-28 (2/31 は無いので末日に丸め)最後の plusMonths(1) は要注意です。1 月 31 日に 1 か月足すと、2 月 31 日は存在しないので、Java は自動的にその月の末日 (2/28 か 2/29) に丸めてくれます。便利な反面、「翌月の同じ日」を厳密に求めたいときには、plusMonths だけだと意図しない動きをすることがあるので頭の片隅に置いておきましょう。
LocalDateのplus*メソッドは、暦のルールをISO-8601(西暦のグレゴリオ暦) に従って正しく処理してくれます。和暦や旧暦などを扱う場合は別の API (JapaneseDateなど) が必要ですが、一般的な業務ではLocalDateで十分です。
minus* メソッドと、plus に負の数を渡す方法
日付を「前に戻す」場合は、対になる minus* メソッドを使うか、plus* に負の数を渡します。どちらでも結果は同じです。
Java
LocalDate today = LocalDate.of(2026, 5, 19);
LocalDate aWeekAgo1 = today.minusDays(7); // 2026-05-12
LocalDate aWeekAgo2 = today.plusDays(-7); // 2026-05-12 (同じ)
LocalDate lastYear = today.minusYears(1); // 2025-05-19
LocalDate lastMonth = today.minusMonths(1); // 2026-04-19コードの読みやすさを考えると、過去を表すときは minusDays(7)、未来を表すときは plusDays(7) のように、意図が分かりやすい書き方を選ぶのがおすすめです。ただし、UI から渡ってきた符号付きの数値 (+10 や -7) をそのまま渡したいときは plusDays(n) 一発で済むので、用途に応じて使い分けましょう。今回の課題でも、引数 n が負の値になり得る前提で plusDays(n) を使うのがシンプルです。
with* で部分置換
「日付の 日 の部分だけ 1 に揃えたい」「月だけ 12 に変えたい」というときには、with* メソッドが便利です。これも不変オブジェクトなので、新しい LocalDate を返します。
Java
LocalDate d = LocalDate.of(2026, 5, 19);
LocalDate firstOfMonth = d.withDayOfMonth(1); // 2026-05-01
LocalDate dec = d.withMonth(12); // 2026-12-19
LocalDate nextYear = d.withYear(2027); // 2027-05-19月初を求めたいときは withDayOfMonth(1)、月末を求めたいときは with(TemporalAdjusters.lastDayOfMonth()) のように TemporalAdjusters を使うと、その月の末日が自動で入ります。請求書の締め日計算など、月末を扱う処理で大活躍する API です。
よくある間違い
LocalDate の加減算は素直な API なのですが、それでも初学者が引っかかりがちな罠がいくつかあります。
- 戻り値を捨ててしまう —
today.plusDays(10);と書いただけで、戻り値を変数に代入し忘れるのが一番多いミスです。LocalDateは不変なので、戻り値を捨てると計算結果はどこにも残りません。必ずLocalDate later = today.plusDays(10);のように受け取りましょう - 月またぎを手で計算しようとする —
5/30に5日足したいから「日 =30+5=35、5月は31日までだから4を引いて6/4」のような計算は不要です。plusDays(5)に任せれば一発です。月またぎ・年またぎ・うるう年は API がすべて面倒を見てくれます - 不変オブジェクトを誤解する —
Calendar時代の感覚でtoday.plusDays(10)を呼んだらtoday自身が変わると思い込むパターンです。Stringのreplaceと同じく、LocalDateのplus*minus*with*はすべて新しいインスタンスを返します。元のオブジェクトは絶対に変わりません
もし
plusMonthsの月末丸め (1/31+1か月 →2/28) で困った場合は、LocalDate.plus(Period.ofMonths(1))でも同じ動きになります。厳密に「翌月の同じ日番号」を保証したい場合は、plusDays(30)のように日数で動かすか、月末判定を自前で組むことになります。
LocalDate.of と toString のおさらい
今回の課題で使う 2 つのメソッドも軽くおさらいしておきます。LocalDate.of(year, month, day) は、年・月・日 の 3 つの int から LocalDate を作る static ファクトリメソッドです。月は 1 〜 12 の自然な数値で指定でき、Calendar 時代の「0 起点」のような罠はありません。
そして toString() を呼ぶと "2026-05-29" のような ISO-8601 形式 (yyyy-MM-dd) の文字列が返ってきます。println に渡したときに自動でこの形式になるのも toString() が呼ばれているからです。今回の課題では、計算後の LocalDate に toString() を呼んで文字列として返すので、フォーマットのことは特に気にしなくて構いません。
やってみよう
それでは今回の課題に挑戦してみましょう。やることは次のとおりです。
- ファイル先頭で
import java.time.LocalDate;を書く Solution.addDays(int year, int month, int day, int n)の中でLocalDate.of(year, month, day)を呼んでLocalDateを作る- その
LocalDateに対してplusDays(n)を呼び、n日後 (もしくはnが負なら|n|日前) のLocalDateを取得する - 取得した
LocalDateのtoString()をreturnする
動作例は以下のとおりです。
addDays(2026, 5, 19, 10)→"2026-05-29"(10日後)addDays(2024, 1, 1, 30)→"2024-01-31"(うるう年の1月、30日後はまだ同月末)addDays(2024, 1, 31, 1)→"2024-02-01"(月またぎは自動)addDays(2026, 5, 19, -19)→"2026-04-30"(負の値で過去に戻る)
plusDays は負の数を渡しても正しく動くので、n の符号を if で場合分けする必要はありません。return LocalDate.of(year, month, day).plusDays(n).toString(); の 1 行で完成できます。完成したら、addDays(2024, 2, 28, 1) で "2024-02-29" (うるう年) が返るかなど、自分でも試してみてください。日付計算の便利さを実感できるはずです。
よくある質問
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 のコードを読む経験も大きく成長を促します。
次のレッスン
次は 日付と時刻 まとめクイズ で、日付と時刻 まとめクイズ を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- plus/minus の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. plus/minus とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- ファイル先頭で
import java.time.LocalDate;を書くこと LocalDate.of(year, month, day)でLocalDateを生成し、plusDays(n)でn日後のLocalDateを求めること- 求めた
LocalDateのtoString()の結果をreturnすること (yyyy-MM-dd形式の文字列になる)
入出力例
test-cases.txt
addDays(2026, 5, 19, 10) → "2026-05-29"
addDays(2024, 1, 1, 30) → "2024-01-31"
addDays(2024, 1, 31, 1) → "2024-02-01"
addDays(2026, 5, 19, -19) → "2026-04-30"
addDays(2024, 2, 28, 1) → "2024-02-29"