java.time.temporalを使いこなす!Javaの日時操作をマスターするための詳細ガイド

この記事から得られる知識

  • java.time.temporalパッケージの全体像と、Javaの日時APIにおけるその役割を理解できます。
  • TemporalTemporalAccessorといった基本インターフェースの違いと、それらの具体的な使い方を学べます。
  • ChronoFieldを用いて、特定の日時フィールド(年、月、日など)へ正確にアクセスする方法を習得できます。
  • ChronoUnitを使いこなし、日付や時間の単位に基づいた精密な計算(加算、減算、差の算出)を実行できるようになります。
  • TemporalAdjustersが提供する便利なメソッドを活用し、「次の金曜日」や「月の初日」といった複雑な日付調整を簡単に行う方法をマスターできます。
  • TemporalQueryを利用して、日時オブジェクトから特定の情報を柔軟に抽出する方法を学びます。

はじめに: なぜ `java.time.temporal` を学ぶのか?

Java 8で導入されたDate and Time API(java.timeパッケージ)は、それまでのjava.util.Datejava.util.Calendarが抱えていた多くの問題を解決し、開発者にとって直感的で扱いやすい日時操作機能を提供しました。LocalDateLocalDateTimeといったクラスは、多くの日常的なユースケースで非常に便利です。

しかし、さらに一歩進んだ、より柔軟で強力な日時操作が求められる場面では、java.timeパッケージの低レベルAPIである`java.time.temporal`パッケージの理解が不可欠になります。

このパッケージは、日時を構成する要素(フィールドや単位)に直接アクセスしたり、複雑な日付調整ロジックをカプセル化したりするための、フレームワークレベルのインターフェースやクラスを提供します。一見すると複雑に感じるかもしれませんが、その仕組みを理解することで、これまで煩雑だった日時処理を驚くほどシンプルかつ堅牢に記述できるようになります。

この記事では、java.time.temporalパッケージの核心的な要素を一つずつ丁寧に解説し、具体的なコード例を交えながら、その強力な機能をマスターするための手引きとなることを目指します。


すべての基本: `Temporal` と `TemporalAccessor`

java.time.temporalパッケージを理解する上で、まず押さえるべき最も基本的なインターフェースがTemporalAccessorTemporalです。これらは、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
    }
}
フレームワークレベルのインターフェース

公式ドキュメントでは、TemporalTemporalAccessorはフレームワークレベルのインターフェースであり、アプリケーションコードで広範囲にわたって使用することは推奨されていません。通常はLocalDateのような具象クラスの変数として扱い、必要に応じてこれらのインターフェースのメソッドを利用するのが一般的です。


日時の構成要素を扱う: `TemporalField` と `ChronoField`

日時を「年」や「月」、「日」、「時」、「分」といった具体的な構成要素に分解して扱うために用意されているのがTemporalFieldインターフェースです。

そして、その標準的な実装が`ChronoField`というenumです。このChronoFieldには、私たちが日時を扱う上で必要となるほとんどのフィールドが網羅的に定義されています。

`ChronoField` の具体的な使い方

ChronoFieldは、TemporalAccessorget()/getLong()メソッドやTemporalwith()メソッドと組み合わせて使用します。これにより、極めて明確に「どのフィールドを操作したいのか」をコードで表現できます。

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は、主にTemporalplus()/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
SECONDS1秒
MINUTES60秒
HOURS60分
HALF_DAYS半日(12時間)12時間
DAYS24時間
WEEKS7日
MONTHS約30.4日
YEARS365.2425日
DECADES10年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`は日時オブジェクトから特定の「情報」を問い合わせて抽出するためのインターフェースです。

TemporalFieldlong型の値を返すのに限定されているのに対し、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を使えば、日時に関する複雑なビジネスロジックを再利用可能なコンポーネントとして綺麗に分離・実装することができます。


まとめ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です