この記事から得られる知識
java.time.temporal
パッケージの全体像と、Javaの日時APIにおけるその役割を理解できます。Temporal
やTemporalAccessor
といった基本インターフェースの違いと、それらの具体的な使い方を学べます。ChronoField
を用いて、特定の日時フィールド(年、月、日など)へ正確にアクセスする方法を習得できます。ChronoUnit
を使いこなし、日付や時間の単位に基づいた精密な計算(加算、減算、差の算出)を実行できるようになります。TemporalAdjusters
が提供する便利なメソッドを活用し、「次の金曜日」や「月の初日」といった複雑な日付調整を簡単に行う方法をマスターできます。TemporalQuery
を利用して、日時オブジェクトから特定の情報を柔軟に抽出する方法を学びます。
はじめに: なぜ `java.time.temporal` を学ぶのか?
Java 8で導入されたDate and Time API(java.time
パッケージ)は、それまでのjava.util.Date
やjava.util.Calendar
が抱えていた多くの問題を解決し、開発者にとって直感的で扱いやすい日時操作機能を提供しました。LocalDate
やLocalDateTime
といったクラスは、多くの日常的なユースケースで非常に便利です。
しかし、さらに一歩進んだ、より柔軟で強力な日時操作が求められる場面では、java.time
パッケージの低レベルAPIである`java.time.temporal`パッケージの理解が不可欠になります。
このパッケージは、日時を構成する要素(フィールドや単位)に直接アクセスしたり、複雑な日付調整ロジックをカプセル化したりするための、フレームワークレベルのインターフェースやクラスを提供します。一見すると複雑に感じるかもしれませんが、その仕組みを理解することで、これまで煩雑だった日時処理を驚くほどシンプルかつ堅牢に記述できるようになります。
この記事では、java.time.temporal
パッケージの核心的な要素を一つずつ丁寧に解説し、具体的なコード例を交えながら、その強力な機能をマスターするための手引きとなることを目指します。
すべての基本: `Temporal` と `TemporalAccessor`
java.time.temporal
パッケージを理解する上で、まず押さえるべき最も基本的なインターフェースがTemporalAccessor
とTemporal
です。これらは、java.time
パッケージのほとんどの日時クラスが実装する、いわば土台となる存在です。
読み取り専用の `TemporalAccessor`
TemporalAccessor
は、時間的オブジェクト(日付、時間、オフセットなど)への読み取り専用アクセスを定義するインターフェースです。このインターフェースの主な役割は、日時を構成するフィールドの値を取得することです。中心となるメソッドは以下の通りです。
isSupported(TemporalField field)
: 指定されたフィールドをサポートしているかどうかを判定します。get(TemporalField field)
: 指定されたフィールドの値をint
型で取得します。getLong(TemporalField field)
: 指定されたフィールドの値をlong
型で取得します。
例えば、LocalDate
オブジェクトから「年」や「月」の値を取得する際に、内部的にはこのインターフェースの機能が利用されています。
import java.time.LocalDate;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
public class TemporalAccessorExample {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2025, 7, 12);
TemporalAccessor accessor = date;
// 年を取得
if (accessor.isSupported(ChronoField.YEAR)) {
int year = accessor.get(ChronoField.YEAR);
System.out.println("Year: " + year); // Year: 2025
}
// 月を取得
long month = accessor.getLong(ChronoField.MONTH_OF_YEAR);
System.out.println("Month: " + month); // Month: 7
// 時刻フィールドはサポートしていないため、例外が発生するか isSupported が false を返す
System.out.println("Is HOU_OF_DAY supported: " + accessor.isSupported(ChronoField.HOUR_OF_DAY)); // Is HOU_OF_DAY supported: false
}
}
読み書き可能な `Temporal`
一方、Temporal
インターフェースはTemporalAccessor
を継承し、さらにオブジェクトを変更する機能(書き込みアクセス)を追加します。ただし、Date and Time APIのクラスは不変(Immutable)であるため、実際には元のオブジェクトを変更するのではなく、変更された新しいインスタンスを返します。
Temporal
インターフェースは、日時の加算・減算や特定フィールドの変更といった操作を定義します。主要なメソッドは以下の通りです。
with(TemporalField field, long newValue)
: 指定されたフィールドを新しい値に変更したオブジェクトを返します。plus(long amountToAdd, TemporalUnit unit)
: 指定された単位の量を加算したオブジェクトを返します。minus(long amountToSubtract, TemporalUnit unit)
: 指定された単位の量を減算したオブジェクトを返します。
LocalDate
, LocalTime
, LocalDateTime
など、実際に操作を行うほとんどのクラスがこのTemporal
インターフェースを実装しています。
import java.time.LocalDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
public class TemporalExample {
public static void main(String[] args) {
LocalDateTime dateTime = LocalDateTime.of(2025, 7, 12, 10, 30, 0);
Temporal temporal = dateTime;
// 年を2026に変更する
Temporal temporal1 = temporal.with(ChronoField.YEAR, 2026);
System.out.println("Original: " + temporal); // Original: 2025-07-12T10:30
System.out.println("With Year 2026: " + temporal1); // With Year 2026: 2026-07-12T10:30
// 3日加算する
Temporal temporal2 = temporal.plus(3, ChronoUnit.DAYS);
System.out.println("Plus 3 Days: " + temporal2); // Plus 3 Days: 2025-07-15T10:30
// 5時間減算する
Temporal temporal3 = temporal.minus(5, ChronoUnit.HOURS);
System.out.println("Minus 5 Hours: " + temporal3); // Minus 5 Hours: 2025-07-12T05:30
}
}
公式ドキュメントでは、Temporal
やTemporalAccessor
はフレームワークレベルのインターフェースであり、アプリケーションコードで広範囲にわたって使用することは推奨されていません。通常はLocalDate
のような具象クラスの変数として扱い、必要に応じてこれらのインターフェースのメソッドを利用するのが一般的です。
日時の構成要素を扱う: `TemporalField` と `ChronoField`
日時を「年」や「月」、「日」、「時」、「分」といった具体的な構成要素に分解して扱うために用意されているのがTemporalField
インターフェースです。
そして、その標準的な実装が`ChronoField`というenumです。このChronoField
には、私たちが日時を扱う上で必要となるほとんどのフィールドが網羅的に定義されています。
`ChronoField` の具体的な使い方
ChronoField
は、TemporalAccessor
のget()
/getLong()
メソッドやTemporal
のwith()
メソッドと組み合わせて使用します。これにより、極めて明確に「どのフィールドを操作したいのか」をコードで表現できます。
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
public class ChronoFieldExample {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.now();
// 各フィールドの値を取得
System.out.println("年: " + zdt.get(ChronoField.YEAR));
System.out.println("月: " + zdt.get(ChronoField.MONTH_OF_YEAR));
System.out.println("日: " + zdt.get(ChronoField.DAY_OF_MONTH));
System.out.println("時 (0-23): " + zdt.get(ChronoField.HOUR_OF_DAY));
System.out.println("分: " + zdt.get(ChronoField.MINUTE_OF_HOUR));
System.out.println("曜日 (1-7): " + zdt.get(ChronoField.DAY_OF_WEEK)); // 1:月曜日, 7:日曜日
System.out.println("年初からの日数: " + zdt.get(ChronoField.DAY_OF_YEAR));
System.out.println("エポック秒: " + zdt.getLong(ChronoField.INSTANT_SECONDS));
// 月の最終日に変更
ZonedDateTime endOfMonth = zdt.with(ChronoField.DAY_OF_MONTH, zdt.range(ChronoField.DAY_OF_MONTH).getMaximum());
System.out.println("月の最終日: " + endOfMonth.toLocalDate());
}
}
また、range(TemporalField field)
メソッドを使えば、あるフィールドが取りうる値の範囲(ValueRange
オブジェクト)を取得できます。これは、例えばある月の日数が28日なのか30日なのか、それとも31日なのかを動的に知りたい場合に非常に便利です。
主要な `ChronoField` の一覧
以下に、よく利用されるChronoField
のメンバーをいくつか紹介します。
ChronoFieldメンバー | 説明 | 値の範囲の例 (ISO暦) |
---|---|---|
YEAR |
年を表します。 | -999,999,999 から 999,999,999 |
MONTH_OF_YEAR |
年における月を表します。 | 1 から 12 |
DAY_OF_MONTH |
月における日を表します。 | 1 から 28/29/30/31 |
DAY_OF_WEEK |
週における曜日を表します。月曜日が1で、日曜日が7です。 | 1 から 7 |
DAY_OF_YEAR |
年における日(通日)を表します。 | 1 から 365/366 |
HOUR_OF_DAY |
1日における時を表します。 | 0 から 23 |
MINUTE_OF_HOUR |
1時間における分を表します。 | 0 から 59 |
SECOND_OF_MINUTE |
1分における秒を表します。 | 0 から 59 |
NANO_OF_SECOND |
1秒におけるナノ秒を表します。 | 0 から 999,999,999 |
INSTANT_SECONDS |
1970-01-01T00:00:00Zからの秒数を表します。 | longの最小値から最大値 |
時間の単位を定義する: `TemporalUnit` と `ChronoUnit`
日時の計算を行う際には、「何を」加算・減算するのか、つまり「時間の単位」を指定する必要があります。この単位を表現するのがTemporalUnit
インターフェースであり、その標準的な実装が`ChronoUnit`というenumです。
ChronoUnit
はナノ秒から世紀、さらには「永遠(FOREVER)」まで、非常に広範な時間の単位を定義しています。
`ChronoUnit` を用いた日時計算
ChronoUnit
は、主にTemporal
のplus()
/minus()
メソッドや、2つの日時オブジェクト間の差を計算するuntil()
メソッドと組み合わせて使用されます。
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
public class ChronoUnitExample {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
// 2週間後を計算
LocalDate twoWeeksLater = today.plus(2, ChronoUnit.WEEKS);
System.out.println("今日: " + today);
System.out.println("2週間後: " + twoWeeksLater);
// 10年前を計算
LocalDate tenYearsAgo = today.minus(10, ChronoUnit.YEARS);
System.out.println("10年前: " + tenYearsAgo);
// 2つの日付間の日数を計算
LocalDate startDate = LocalDate.of(2025, 1, 1);
LocalDate endDate = LocalDate.of(2025, 12, 31);
long daysBetween = ChronoUnit.DAYS.between(startDate, endDate);
System.out.println(startDate + " と " + endDate + " の間の日数は " + daysBetween + " 日です。");
// 上記は startDate.until(endDate, ChronoUnit.DAYS) と同等
// 2つの日時の間の時間数を計算
LocalDateTime startDateTime = LocalDateTime.of(2025, 7, 12, 9, 0);
LocalDateTime endDateTime = LocalDateTime.of(2025, 7, 13, 18, 30);
long hoursBetween = ChronoUnit.HOURS.between(startDateTime, endDateTime);
System.out.println(startDateTime + " と " + endDateTime + " の間の時間は " + hoursBetween + " 時間です。");
}
}
between()
メソッドは、開始日時を含み、終了日時を含まない形で期間を計算します。また、単位に満たない端数は切り捨てられる点に注意が必要です。
主要な `ChronoUnit` の一覧
以下に、よく利用される`ChronoUnit`のメンバーを示します。
ChronoUnitメンバー | 説明 | おおよその期間 |
---|---|---|
NANOS | ナノ秒 | 1秒の10億分の1 |
MICROS | マイクロ秒 | 1秒の100万分の1 |
MILLIS | ミリ秒 | 1秒の1000分の1 |
SECONDS | 秒 | 1秒 |
MINUTES | 分 | 60秒 |
HOURS | 時 | 60分 |
HALF_DAYS | 半日(12時間) | 12時間 |
DAYS | 日 | 24時間 |
WEEKS | 週 | 7日 |
MONTHS | 月 | 約30.4日 |
YEARS | 年 | 365.2425日 |
DECADES | 10年 | 10年 |
CENTURIES | 世紀(100年) | 100年 |
MILLENNIA | 千年紀(1000年) | 1000年 |
ERAS | 紀元 | 西暦など |
FOREVER | 無限の期間 | – |
魔法のような日付調整: `TemporalAdjuster` と `TemporalAdjusters`
「次の日曜日」「今月の最終日」「来年の元日」といった、単純な加算・減算では実現が難しい日付操作は頻繁に発生します。このような複雑な日付調整ロジックを扱うために提供されているのが`TemporalAdjuster`インターフェースです。
TemporalAdjuster
は、あるTemporal
オブジェクトを受け取り、調整された新しいTemporal
オブジェクトを返す、という単一のメソッド(adjustInto
)を持つ関数型インターフェースです。
そして、このインターフェースの真価を発揮させるのが、便利なアジャスターを静的メソッドとして多数提供している`TemporalAdjusters`ユーティリティクラスです。これを利用することで、複雑な日付計算をメソッド呼び出し一つで実現できます。
`TemporalAdjusters` の実践的な使い方
`TemporalAdjuster`は、`Temporal`の`with()`メソッドに渡すことで使用します。コードの可読性が非常に高くなるのが特徴です。
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
public class TemporalAdjustersExample {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2025, 7, 12); // 土曜日
System.out.println("基準日: " + date + " (" + date.getDayOfWeek() + ")");
// 月の初日
LocalDate firstDayOfMonth = date.with(TemporalAdjusters.firstDayOfMonth());
System.out.println("月の初日: " + firstDayOfMonth);
// 月の最終日
LocalDate lastDayOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());
System.out.println("月の最終日: " + lastDayOfMonth);
// 次の月曜日
LocalDate nextMonday = date.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
System.out.println("次の月曜日: " + nextMonday);
// 次の、または同じ月曜日 (基準日が月曜ならその日を返す)
LocalDate nextOrSameMonday = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
System.out.println("次の、または同じ月曜日: " + nextOrSameMonday);
// その月の第2火曜日
LocalDate secondTuesday = date.with(TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.TUESDAY));
System.out.println("今月の第2火曜日: " + secondTuesday);
// 年の最終日
LocalDate lastDayOfYear = date.with(TemporalAdjusters.lastDayOfYear());
System.out.println("年の最終日: " + lastDayOfYear);
}
}
独自の `TemporalAdjuster` を作成する
TemporalAdjuster
は関数型インターフェースなので、ラムダ式を使って独自の調整ロジックを簡単に作成できます。例えば、「次の営業日(土日を除く)」を計算するアジャスターは以下のように実装できます。
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
public class CustomAdjusterExample {
public static void main(String[] args) {
// 次の営業日を計算するアジャスター
TemporalAdjuster nextWorkingDay = temporal -> {
LocalDate result = (LocalDate) temporal;
do {
result = result.plusDays(1);
} while (result.getDayOfWeek() == DayOfWeek.SATURDAY || result.getDayOfWeek() == DayOfWeek.SUNDAY);
return result;
};
LocalDate today = LocalDate.of(2025, 7, 11); // 金曜日
LocalDate friday = today.with(nextWorkingDay);
System.out.println(today + " の次の営業日は " + friday); // -> 2025-07-14 (月曜日)
LocalDate saturday = LocalDate.of(2025, 7, 12); // 土曜日
LocalDate nextDayFromSat = saturday.with(nextWorkingDay);
System.out.println(saturday + " の次の営業日は " + nextDayFromSat); // -> 2025-07-14 (月曜日)
}
}
日時から情報を抽出する: `TemporalQuery`
これまで見てきた機能が日時を「操作」するものだったのに対し、`TemporalQuery`は日時オブジェクトから特定の「情報」を問い合わせて抽出するためのインターフェースです。
TemporalField
がlong
型の値を返すのに限定されているのに対し、TemporalQuery
は任意の型のオブジェクトを返すことができます。これも関数型インターフェースであり、`TemporalAccessor`の`query()`メソッドに渡して使用します。
`TemporalQueries` の使い方
TemporalAdjusters
と同様に、よく使われるクエリを静的メソッドとして提供する`TemporalQueries`ユーティリティクラスが存在します。
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalQueries;
import java.time.temporal.TemporalQuery;
import java.time.temporal.ChronoUnit;
public class TemporalQueryExample {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.now();
LocalDateTime ldt = LocalDateTime.now();
LocalDate ld = LocalDate.now();
// 定義済みのクエリを使用
TemporalQuery<ZoneId> zoneQuery = TemporalQueries.zone();
System.out.println("ZoneId: " + zdt.query(zoneQuery)); // 例: Asia/Tokyo
// LocalDateTime はゾーン情報を持たないので null
System.out.println("ZoneId from LocalDateTime: " + ldt.query(zoneQuery));
TemporalQuery<LocalDate> dateQuery = TemporalQueries.localDate();
System.out.println("LocalDate from ZonedDateTime: " + zdt.query(dateQuery));
TemporalQuery<ChronoUnit> precisionQuery = TemporalQueries.precision();
System.out.println("Precision of ZonedDateTime: " + zdt.query(precisionQuery)); // NANOS
System.out.println("Precision of LocalDate: " + ld.query(precisionQuery)); // DAYS
}
}
独自の `TemporalQuery` を作成する
独自のクエリもラムダ式で簡単に作成できます。例えば、ある日付が国民の祝日かどうかを判定する(ロジックは簡略化)クエリを考えてみましょう。
import java.time.LocalDate;
import java.time.Month;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQuery;
public class CustomQueryExample {
// 祝日かどうかを判定するクエリ
public static class IsHolidayQuery implements TemporalQuery<Boolean> {
@Override
public Boolean queryFrom(TemporalAccessor temporal) {
LocalDate date = LocalDate.from(temporal);
// 簡単な祝日判定ロジック
if (date.getMonth() == Month.JANUARY && date.getDayOfMonth() == 1) {
return true; // 元日
}
if (date.getMonth() == Month.MAY && date.getDayOfMonth() == 5) {
return true; // こどもの日
}
// ... 他の祝日判定
return false;
}
}
public static void main(String[] args) {
IsHolidayQuery isHoliday = new IsHolidayQuery();
LocalDate date1 = LocalDate.of(2025, 1, 1);
System.out.println(date1 + " は祝日ですか? " + date1.query(isHoliday)); // true
LocalDate date2 = LocalDate.of(2025, 7, 12);
System.out.println(date2 + " は祝日ですか? " + date2.query(isHoliday)); // false
}
}
このように、TemporalQuery
を使えば、日時に関する複雑なビジネスロジックを再利用可能なコンポーネントとして綺麗に分離・実装することができます。
まとめ
java.time.temporal
パッケージは、JavaのDate and Time APIの心臓部とも言える、強力で柔軟な機能群を提供します。
- `Temporal` / `TemporalAccessor` は、日時オブジェクトの読み書きの基本となるインターフェースです。
- `ChronoField` を使うことで、年、月、日などの個々のフィールドに正確にアクセスできます。
- `ChronoUnit` を使えば、さまざまな単位での精密な日時計算が可能になります。
- `TemporalAdjusters` は、「次の金曜日」のような複雑な日付調整を驚くほど簡単に実現してくれます。
- `TemporalQuery` を利用すれば、日時から必要な情報を抽出するロジックをカプセル化できます。
これらの低レベルAPIを理解し、使いこなすことで、あなたはJavaにおける日時操作のエキスパートへと一歩近づくことができるでしょう。まずはLocalDate
やLocalDateTime
と、便利なTemporalAdjusters
から使い始め、より複雑な要件に直面したときに、この記事で解説した他のコンポーネントを活用していくのがおすすめです。