AIによる要約
LocalDateは不変オブジェクトです。plusDaysは元のdateを書き換えず、計算後の新しいLocalDateを返します。そのためdate.plusDays(1)だけではdateは変わりません。date = date.plusDays(1)のように戻り値を受け取ります。月末やうるう年はAPIが処理しますが、営業日、締日、タイムゾーンは別の業務ルールなので、単純な日数加算と混同しないことが重要です。
この記事はXへ投稿したJavaクイズの詳しい解説です。まず問題を解きたい方は、Xの元ポストを開くことができます。


日付を1日進めるコードは、バッチ対象日、請求期限、検索範囲などで頻繁に使います。plusDaysという名前から、変数の中身がその場で更新されるように感じますが、LocalDateは不変です。
戻り値を受け取らないコードはコンパイルも通り、例外も出ません。そのため障害時に気付きにくく、ループなら同じ日付を処理し続ける可能性もあります。日付APIの契約と業務日付のルールを分けて確認します。
この記事のポイント
- LocalDateは不変オブジェクト
- plusDaysは新しいLocalDateを返す
- 戻り値を代入しないと元の日付は変わらない
- 月末・うるう年はAPIが処理する
- 営業日や締日は別の業務ルールとして実装する
まずはXのJavaクイズを確認する

LocalDate date = LocalDate.of(2026, 8, 13);
date.plusDays(1);
System.out.println(date);答えは2026-08-13です。plusDaysは翌日のLocalDateを返しますが、元のdateは変更しません。

LocalDateは不変オブジェクト
不変オブジェクトは、作成後に内部の値が変わりません。LocalDateの年・月・日はsetterで変更できず、加算や変更メソッドは新しいインスタンスを返します。
LocalDate original = LocalDate.of(2026, 8, 13);
LocalDate next = original.plusDays(1);
System.out.println(original); // 2026-08-13
System.out.println(next); // 2026-08-14文字列のreplaceやtoUpperCaseでも同じ考え方があります。戻り値があるメソッドを呼んだら、その値を使っているか確認する習慣を付けます。
正しくは戻り値を代入する
同じ変数名で次の日付を使うなら、戻り値を再代入します。元の日付も必要なら別変数へ保持します。
date = date.plusDays(1);
// または意図が分かる別名にする
LocalDate nextDate = date.plusDays(1);メソッドチェーンもすべて新しい値を返します。途中の結果を読みやすくする必要があれば、1行へ詰め込まず変数を分けます。
ループで戻り値を捨てると処理が進まない
日次バッチで開始日から終了日までを1日1回ずつ処理します。current.plusDays(1) の戻り値を捨てると current が同じままなので、複数日の範囲では先頭の日を繰り返し処理します。処理済みの通知を送るバッチなら、同じ日の通知を何度も送ることになります。
開始日と終了日を含め、逆転した範囲は処理前に拒否する例です。DailyDateRunner.java の全文を掲載します。
package example.dates;
import java.time.LocalDate;
import java.util.function.Consumer;
public final class DailyDateRunner {
private DailyDateRunner() {}
public static void runInclusive(
LocalDate from, LocalDate to, Consumer<LocalDate> dailyService) {
if (from.isAfter(to)) {
throw new IllegalArgumentException("開始日は終了日以前にしてください");
}
LocalDate current = from;
while (true) {
dailyService.accept(current);
if (current.equals(to)) {
return;
}
current = current.plusDays(1);
}
}
}dailyService.accept(current) がその日付の処理です。終了日を処理したらその場で戻り、それ以外では current = current.plusDays(1) で次の日に進めます。開始日と終了日が同じ日なら1回で終わり、開始日が終了日より後なら1回も呼びません。
月末とうるう年はAPIに任せる
LocalDateはカレンダーを理解しているため、月末やうるう年を手計算する必要はありません。
System.out.println(LocalDate.of(2026, 1, 31).plusDays(1));
// 2026-02-01
System.out.println(LocalDate.of(2024, 2, 28).plusDays(1));
// 2024-02-29ただしplusMonths(1)の結果は『同じ日番号が存在しなければ月末へ調整』されます。1か月後と翌月末は業務上同じとは限らないため、期待例を仕様へ書きます。
営業日計算はplusDaysだけでは足りない
土日、祝日、会社休日、銀行営業日を飛ばす締切計算は、単純なplusDaysでは表せません。休日カレンダーを参照する業務サービスとして分けます。
LocalDate nextBusinessDay(LocalDate date) {
LocalDate candidate = date.plusDays(1);
while (holidayCalendar.isHoliday(candidate)) {
candidate = candidate.plusDays(1);
}
return candidate;
}祝日マスタの更新時期、地域差、臨時休日などもあります。日付APIの正しさと休日データの正しさは別に監視します。
LocalDateと日時・タイムゾーンを混同しない
LocalDateには時刻とタイムゾーンがありません。利用者の現地日付、サーバー日付、UTC基準の締切を扱う場合、LocalDateだけでは情報が足りないことがあります。
LocalDate todayInTokyo = LocalDate.now(ZoneId.of("Asia/Tokyo"));
LocalDate todayInUtc = LocalDate.now(ZoneOffset.UTC);テストでLocalDate.now()へ直接依存すると日付境界で不安定になります。現在日を使うServiceではClockを注入するなど、プロジェクトの方針に合わせて固定可能にします。
JUnitで月末と境界日を確認する
日付が変わったかは、plusDays(1) の結果だけでなく、日次処理へ渡された日付の一覧で確認します。2024年2月28日から3月1日なら、2月28日・2月29日・3月1日がこの順で1回ずつ渡ることを確かめます。
日付が進まないループをJUnitで再現する
ZIPには本文の DailyDateRunner.java、DailyDateRunnerTest.java、pom.xml、READMEを収録しています。JDK 17とMaven 3を用意し、展開先で mvn test を実行します。
DailyDateRunnerTest.javaの全文を見る
package example.dates;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
class DailyDateRunnerTest {
@Test
void runsSameDateOnce() {
LocalDate date = LocalDate.of(2026, 1, 31);
List<LocalDate> visited = new ArrayList<>();
DailyDateRunner.runInclusive(date, date, visited::add);
assertEquals(List.of(date), visited);
}
@Test
void runsThroughLeapDayInOrder() {
LocalDate from = LocalDate.of(2024, 2, 28);
LocalDate leapDay = LocalDate.of(2024, 2, 29);
LocalDate to = LocalDate.of(2024, 3, 1);
List<LocalDate> visited = new ArrayList<>();
DailyDateRunner.runInclusive(from, to, date -> {
if (visited.contains(date)) {
throw new AssertionError("同じ日を再処理しました " + date);
}
visited.add(date);
});
assertEquals(List.of(from, leapDay, to), visited);
}
@Test
void rejectsReversedRangeBeforeProcessing() {
LocalDate from = LocalDate.of(2026, 2, 1);
LocalDate to = LocalDate.of(2026, 1, 31);
List<LocalDate> visited = new ArrayList<>();
assertThrows(IllegalArgumentException.class,
() -> DailyDateRunner.runInclusive(from, to, visited::add));
assertTrue(visited.isEmpty());
}
@Test
void stopsAtMaximumDateWithoutAddingADay() {
List<LocalDate> visited = new ArrayList<>();
DailyDateRunner.runInclusive(LocalDate.MAX, LocalDate.MAX, visited::add);
assertEquals(List.of(LocalDate.MAX), visited);
}
}修正版では4件が成功します。同じ開始日と終了日で1回、うるう日を含む3日で順序どおり、逆転範囲で0回、LocalDate.MAX の1日分で翌日を計算せずに終了することを確認します。
代入忘れを再現するには、DailyDateRunner.java の current = current.plusDays(1); を current.plusDays(1); に変更して、もう一度 mvn test を実行します。うるう日のテストは2月28日が2回目に渡された時点で「同じ日を再処理しました 2024-02-28」と失敗します。無限ループを待たずに原因を確認できます。
現場レビューでよくある指摘
日付処理のレビューでは、戻り値の代入だけでなく、暦日、営業日、締日、タイムゾーンのどれを計算しているかを確認します。
// レビューコメント例
plusDaysは元のLocalDateを変更しません。
戻り値をdateへ代入するか、nextDateとして利用してください。
// レビューコメント例
このwhileではcurrentが更新されないため処理が終わりません。
ループ更新式でcurrent = current.plusDays(1)としてください。
// レビューコメント例
仕様は翌営業日ですが、plusDays(1)では休日を考慮できません。
休日カレンダーを使う業務メソッドへ分けてください。日付が1日ずれた障害は、データ修正や再請求につながります。手元の今日だけで確認せず、月末・年末・休日・タイムゾーンを含む具体例で説明します。
提出前のセルフチェック
レビュー前に確認すること
- plusDaysの戻り値を使っているか
- 元の日付を残す必要があるか
- ループで日付が必ず進むか
- 月末・年末・うるう年をテストしたか
- 暦日と営業日を区別したか
- 締日や月末調整の仕様を確認したか
- タイムゾーンが必要な日時ではないか
- 現在日に依存するテストを固定できるか
Javaの日付APIを基本から学ぶ参考書
LocalDate、LocalDateTime、ZoneIdの役割を体系的に整理しておくと、単純な日数加算と業務日付計算を混同しにくくなります。
スッキリわかるJava入門 実践編 第5版
基礎文法の次に必要な、現場寄りのJava知識を補う。
コレクション、ジェネリクス、ラムダ式、ストリームなど、業務コードで出会いやすい機能を入門編の次に学べます。
- Java基礎の次に何を学ぶか迷っている
- コレクションやStreamを整理したい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
LocalDateは不変オブジェクトなので、plusDaysは元の日付を書き換えず、新しいLocalDateを返します。日付を進めるなら戻り値を変数へ代入してください。
月末とうるう年はAPIへ任せられますが、営業日、締日、タイムゾーンは業務ルールです。ループの更新漏れと境界日をテストし、何の日付を計算しているか明確にします。



