java.timeを使いこなす!Javaの日時操作の決定版ガイド

この記事を読むことで、あなたは以下の知識を習得できます。
  • java.time APIの基本的な考え方と、旧来のAPIに対するメリット
  • LocalDate, LocalTime, LocalDateTimeを使った日付と時刻の基本的な扱い方
  • タイムゾーンや夏時間を正確に扱うためのZonedDateTimeの使い方
  • 「3週間後」や「2時間30分後」といった時間の間隔を表すPeriodDurationの明確な使い分け
  • 日付や時刻を任意の書式で文字列に変換(フォーマット)、または文字列から解析(パース)する方法
  • 古いjava.util.Datejava.util.Calendarとの間で相互に変換する方法

第1章: なぜ`java.time` APIは生まれたのか?


Java 8が2014年にリリースされるまで、Java開発者は日付と時刻の扱いに長年頭を悩ませてきました。その原因は、初期のJavaから提供されていたjava.util.Dateクラスと、その後追加されたjava.util.Calendarクラスにありました。

旧API(DateCalendar)が抱えていた問題点

  • ミュータブル(可変)であること: DateCalendarのインスタンスは、その値を後から変更できてしまいます。これは、複数のスレッドから同時にアクセスされた場合に予期せぬ結果を引き起こす原因となり、スレッドセーフではありませんでした。
  • 直感的でないAPI: Calendarで月を扱う際、1月が0、2月が1…というように0から始まるインデックスでした。これは多くの開発者にとって混乱の元でした。また、年や月を加算するメソッドの挙動も直感的とは言えませんでした。
  • 設計の問題: Dateクラスはその名前にもかかわらず、実際には特定の日付だけでなく時刻の情報(ミリ秒まで)も内包していました。さらに、タイムゾーンに関する情報も暗黙的に持っており、クラスの責務が曖昧でした。
  • タイムゾーン処理の複雑さ: タイムゾーンをまたいだ日時の扱いは非常に煩雑で、多くの定型的なコードを記述する必要がありました。

これらの問題を解決するために、JSR 310という仕様改善提案のもとで、全く新しい日時APIが開発されました。それが、Java 8で標準搭載された`java.time`パッケージです。

`java.time` APIは、以下のような優れた設計思想に基づいています。

`java.time` APIの設計思想

イミュータブル(不変): `java.time`パッケージの主要なクラスはすべて不変です。一度作成したインスタンスの値は変更できず、日付計算などを行うメソッドは必ず新しいインスタンスを返します。これにより、スレッドセーフなプログラムを容易に記述できます。

ドメイン駆動設計: 日付、時刻、タイムゾーン付き日時、期間など、それぞれの概念に対応する専用のクラスが用意されています。これにより、APIの意図が明確になり、コードの可読性が向上しました。

明確で流れるようなAPI: メソッド名がその役割を明確に表しており(例: `plusDays()`, `withMonth()`)、メソッドチェーンを使って直感的に日時を操作できます。

第2章: 基本的な日付と時刻の操作


`java.time`の最も基本的なクラスは、タイムゾーン情報を持たないローカルな日付・時刻を扱います。

`LocalDate` – 日付のみを扱う

`LocalDate`は、タイムゾーン情報を含まない「年月日」を表現するクラスです。誕生日や記念日など、時刻が関係ない日付を扱うのに適しています。

<?xml version="1.0" encoding="UTF-8"?>
import java.time.LocalDate;
import java.time.Month;

public class LocalDateExample {
    public static void main(String[] args) {
        // 1. 現在の日付を取得
        LocalDate today = LocalDate.now();
        System.out.println("今日の日付: " + today); // 例: 2025-07-12

        // 2. 特定の日付を生成
        LocalDate specificDate = LocalDate.of(2025, Month.DECEMBER, 25);
        System.out.println("特定の日付: " + specificDate); // 2025-12-25

        // 3. 日付の計算 (イミュータブルなので新しいインスタンスが返る)
        LocalDate nextWeek = today.plusWeeks(1);
        System.out.println("一週間後の日付: " + nextWeek);

        LocalDate previousMonth = today.minusMonths(1);
        System.out.println("一ヶ月前の日付: " + previousMonth);

        // 4. 年や月、日を取得
        int year = today.getYear();
        Month month = today.getMonth(); // Month列挙型
        int dayOfMonth = today.getDayOfMonth();
        System.out.println("年: " + year + ", 月: " + month.getValue() + ", 日: " + dayOfMonth);
    }
}

`LocalTime` – 時刻のみを扱う

`LocalTime`は、日付情報を含まない「時分秒ナノ秒」を表現します。開店時間やアラームの時刻設定など、日付が関係ない時刻の表現に利用します。

<?xml version="1.0" encoding="UTF-8"?>
import java.time.LocalTime;

public class LocalTimeExample {
    public static void main(String[] args) {
        // 1. 現在の時刻を取得
        LocalTime now = LocalTime.now();
        System.out.println("現在の時刻: " + now); // 例: 22:10:30.123456789

        // 2. 特定の時刻を生成
        LocalTime lunchTime = LocalTime.of(12, 30);
        System.out.println("ランチタイム: " + lunchTime); // 12:30

        // 3. 時刻の計算
        LocalTime after30Minutes = now.plusMinutes(30);
        System.out.println("30分後の時刻: " + after30Minutes);

        // 4. 時、分、秒を取得
        int hour = now.getHour();
        int minute = now.getMinute();
        System.out.println("時: " + hour + ", 分: " + minute);
    }
}

`LocalDateTime` – 日付と時刻の両方を扱う

`LocalDateTime`は、`LocalDate`と`LocalTime`を組み合わせたもので、タイムゾーン情報を含まない「年月日・時分秒ナノ秒」を表現します。 会議の予約やシステムのログ記録など、特定の日時をタイムゾーンなしで扱う場合に最もよく使われるクラスです。

<?xml version="1.0" encoding="UTF-8"?>
import java.time.LocalDateTime;
import java.time.Month;

public class LocalDateTimeExample {
    public static void main(String[] args) {
        // 1. 現在の日時を取得
        LocalDateTime currentDateTime = LocalDateTime.now();
        System.out.println("現在の日時: " + currentDateTime);

        // 2. 特定の日時を生成
        LocalDateTime eventDateTime = LocalDateTime.of(2026, Month.JANUARY, 1, 10, 0, 0);
        System.out.println("イベント日時: " + eventDateTime);

        // 3. LocalDateとLocalTimeから生成
        LocalDate datePart = LocalDate.of(2025, 8, 15);
        LocalTime timePart = LocalTime.of(19, 0);
        LocalDateTime combinedDateTime = LocalDateTime.of(datePart, timePart);
        System.out.println("結合した日時: " + combinedDateTime);

        // 4. 日時の計算
        LocalDateTime twoHoursLater = currentDateTime.plusHours(2);
        System.out.println("2時間後の日時: " + twoHoursLater);

        // 5. 日付部分や時刻部分の抽出
        LocalDate extractedDate = currentDateTime.toLocalDate();
        LocalTime extractedTime = currentDateTime.toLocalTime();
        System.out.println("抽出した日付: " + extractedDate);
        System.out.println("抽出した時刻: " + extractedTime);
    }
}

第3章: タイムゾーンを扱う`ZonedDateTime`


グローバルなアプリケーションや、異なる地域のユーザーを相手にするシステムでは、タイムゾーンの考慮が不可欠です。`java.time`では、`ZonedDateTime`クラスがこの役割を担います。

`ZonedDateTime`は、`LocalDateTime`に加えてタイムゾーン情報(例: `Asia/Tokyo`)と、UTCからの時差(オフセット、例: `+09:00`)を保持します。これにより、サマータイム(夏時間)の変更などを自動的に処理できます。

`ZoneId`と`ZoneOffset`

  • `ZoneId`: `Asia/Tokyo`や`Europe/London`のような地域ベースのタイムゾーン識別子です。サマータイムのルールを含みます。
  • `ZoneOffset`: `+09:00`や`-05:00`のように、協定世界時(UTC)からの固定の時差を表します。サマータイムのルールは含みません。

通常は、サマータイムを正しく扱うために`ZoneId`を使用することが推奨されます。

<?xml version="1.0" encoding="UTF-8"?>
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public class ZonedDateTimeExample {
    public static void main(String[] args) {
        // 1. 特定のタイムゾーンで現在日時を取得
        ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
        ZonedDateTime tokyoTime = ZonedDateTime.now(tokyoZone);
        System.out.println("東京の現在日時: " + tokyoTime);

        ZoneId londonZone = ZoneId.of("Europe/London");
        ZonedDateTime londonTime = ZonedDateTime.now(londonZone);
        System.out.println("ロンドンの現在日時: " + londonTime);

        // 2. LocalDateTimeにZoneIdを付与して生成
        LocalDateTime localDateTime = LocalDateTime.of(2025, 7, 20, 10, 0);
        ZonedDateTime eventInTokyo = ZonedDateTime.of(localDateTime, tokyoZone);
        System.out.println("東京でのイベント日時: " + eventInTokyo);

        // 3. タイムゾーンの変換
        // withZoneSameInstantは、同じ「瞬間」を別のタイムゾーンで表現します。
        // 東京の午前10時は、ロンドンの何時かを計算できます。
        ZonedDateTime eventInLondon = eventInTokyo.withZoneSameInstant(londonZone);
        System.out.println("同じ瞬間のロンドン日時: " + eventInLondon);

        // withZoneSameLocalは、ローカル日時を維持したままタイムゾーンを変更します。
        // 各都市で「現地時間の10時」にイベントを開始する場合などに使えます。
        ZonedDateTime localEventInLondon = eventInTokyo.withZoneSameLocal(londonZone);
        System.out.println("現地時間10時のロンドン日時: " + localEventInLondon);
    }
}

`withZoneSameInstant`は、国際電話の時間を調整するようなケースで非常に強力です。一方、`withZoneSameLocal`は、各拠点でのローカル時間を基準にした処理で役立ちます。

第4章: 時間の長さを表す`Duration`と`Period`


「2つの日時の間の時間」や「ある時点から3週間後」といった時間の長さを扱うために、`java.time`は2つの専用クラスを提供します。これらは明確に使い分ける必要があります。

`Duration` – 時間ベースの期間

`Duration`は、秒やナノ秒を単位とした、より精密な時間の長さを表します。 主に`LocalTime`, `LocalDateTime`, `Instant`, `ZonedDateTime`といった、時刻情報を持つクラス間の差を計算するために使用されます。

`Duration` の使用例

<?xml version="1.0" encoding="UTF-8"?>
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;

public class DurationExample {
    public static void main(String[] args) {
        LocalDateTime start = LocalDateTime.of(2025, 1, 1, 10, 0, 0);
        LocalDateTime end = LocalDateTime.of(2025, 1, 1, 12, 30, 15);

        // 1. 2つのLocalDateTime間のDurationを計算
        Duration duration = Duration.between(start, end);
        System.out.println("Duration: " + duration); // PT2H30M15S (ISO-8601形式)

        // 2. 時間、分、秒単位で取得
        long hours = duration.toHours();
        long minutes = duration.toMinutes();
        System.out.println("合計時間: " + hours); // 2
        System.out.println("合計分: " + minutes); // 150

        // 3. 特定の単位でDurationを生成
        Duration twoHours = Duration.ofHours(2);
        LocalTime time = LocalTime.of(9, 0);
        LocalTime newTime = time.plus(twoHours);
        System.out.println("2時間後の時刻: " + newTime); // 11:00
    }
}

`Period` の使用例

<?xml version="1.0" encoding="UTF-8"?>
import java.time.LocalDate;
import java.time.Period;

public class PeriodExample {
    public static void main(String[] args) {
        LocalDate startDate = LocalDate.of(2025, 2, 10);
        LocalDate endDate = LocalDate.of(2026, 4, 25);

        // 1. 2つのLocalDate間のPeriodを計算
        Period period = Period.between(startDate, endDate);
        System.out.println("Period: " + period); // P1Y2M15D (ISO-8601形式)

        // 2. 年、月、日の単位で取得
        int years = period.getYears();
        int months = period.getMonths();
        int days = period.getDays();
        System.out.println(years + "年 " + months + "ヶ月 " + days + "日");

        // 3. 特定の単位でPeriodを生成
        Period threeWeeks = Period.ofWeeks(3);
        LocalDate today = LocalDate.now();
        LocalDate afterThreeWeeks = today.plus(threeWeeks);
        System.out.println("3週間後の日付: " + afterThreeWeeks);
    }
}

注意点

`LocalDateTime`に対して`Period`を加算することは可能ですが、`Duration`はサマータイムを考慮し、`Period`は考慮しないという違いがあります。例えば、サマータイム開始日に`Period.ofDays(1)`を加算すると時刻は変わりませんが、`Duration.ofDays(1)`(=24時間)を加算すると時刻が1時間ずれる可能性があります。

第5章: 機械的な時間 `Instant`


`Instant`クラスは、人間が直感的に理解しやすい「年月日時分秒」とは異なり、コンピュータにとって扱いやすい単一のタイムライン上の点を表現します。 具体的には、エポック(1970年1月1日 00:00:00 UTC)からの経過時間をナノ秒精度で保持します。

これは、旧来の`java.util.Date`がミリ秒精度で保持していた値と概念的に似ており、主にシステムのタイムスタンプや、イベントの発生順序を記録するような、機械的な時間の扱いに適しています。

<?xml version="1.0" encoding="UTF-8"?>
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public class InstantExample {
    public static void main(String[] args) {
        // 1. 現在のInstantを取得 (UTC基準)
        Instant now = Instant.now();
        System.out.println("現在のInstant: " + now);

        // 2. エポック秒から生成
        Instant fromEpochSecond = Instant.ofEpochSecond(1735689600); // 2025-01-01 00:00:00 UTC
        System.out.println("エポック秒から: " + fromEpochSecond);

        // 3. InstantとZonedDateTimeの変換
        ZonedDateTime zonedDateTime = now.atZone(ZoneId.of("Asia/Tokyo"));
        System.out.println("東京時間での表現: " + zonedDateTime);

        Instant fromZonedDateTime = zonedDateTime.toInstant();
        System.out.println("ZonedDateTimeからInstantへ: " + fromZonedDateTime);

        // 4. タイムスタンプとして利用
        long epochMilli = now.toEpochMilli();
        System.out.println("エポックからのミリ秒: " + epochMilli);
    }
}

`Instant`は常にUTC基準であり、タイムゾーン情報を持たないため、タイムゾーンを意識した日時に変換するには`atZone()`メソッドを使って`ZonedDateTime`にする必要があります。

第6章: フォーマットとパース (`DateTimeFormatter`)


日時オブジェクトを「2025年07月12日」のような特定の書式の文字列に変換したり、その逆を行ったりする処理は非常によく発生します。この役割を担うのが`DateTimeFormatter`クラスです。

`DateTimeFormatter`もイミュータブルでスレッドセーフです。一度フォーマッタを作成すれば、複数のスレッドから安全に利用できます。

書式指定の方法

フォーマッタを作成するには、主に3つの方法があります。

  1. 事前定義された定数を使用する: `ISO_LOCAL_DATE` (`2011-12-03`) や `ISO_DATE_TIME` (`2011-12-03T10:15:30`) など、ISO標準形式のフォーマッタが定数として用意されています。
  2. パターン文字を使用してカスタム定義する: `ofPattern()`メソッドに`”yyyy/MM/dd HH:mm:ss”`のようなパターン文字列を渡して、独自のフォーマッタを作成します。
  3. ローカライズされたスタイルを使用する: `ofLocalizedDate()`などに`FormatStyle`(`FULL`, `LONG`, `MEDIUM`, `SHORT`)を指定して、ロケールに応じたフォーマットを作成します。

フォーマット(日時 → 文字列)とパース(文字列 → 日時)

<?xml version="1.0" encoding="UTF-8"?>
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;

public class DateTimeFormatterExample {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();

        // --- フォーマット (日時オブジェクトを文字列に) ---
        // 1. 事前定義フォーマッタ
        String isoFormatted = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        System.out.println("ISOフォーマット: " + isoFormatted);

        // 2. カスタムパターン
        DateTimeFormatter customFormatter1 = DateTimeFormatter.ofPattern("yyyy年MM月dd日(E) HH時mm分ss秒");
        String customFormatted1 = now.format(customFormatter1);
        System.out.println("カスタムフォーマット1: " + customFormatted1); // 例: 2025年07月12日(土) 10時30分55秒

        DateTimeFormatter customFormatter2 = DateTimeFormatter.ofPattern("uuuu/MM/dd");
        System.out.println("カスタムフォーマット2: " + now.format(customFormatter2)); // 例: 2025/07/12

        // 3. ローカライズスタイル
        DateTimeFormatter localizedFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)
                                                                  .withLocale(Locale.JAPAN);
        System.out.println("日本ロケール(FULL): " + now.format(localizedFormatter));


        // --- パース (文字列を日時オブジェクトに) ---
        String dateTimeString = "2025/08/20 15:00";
        DateTimeFormatter parser = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");

        try {
            LocalDateTime parsedDateTime = LocalDateTime.parse(dateTimeString, parser);
            System.out.println("パース結果: " + parsedDateTime);
        } catch (java.time.format.DateTimeParseException e) {
            System.err.println("パースに失敗しました: " + e.getMessage());
        }
    }
}

`u`と`y`の違い:パターン文字では年を表すのに`u`と`y`が使えますが、`u`は暦年(西暦)、`y`は元号年(和暦などで使用)を表すという違いがあります。通常は`u`を使うのが安全です。

第7章: 実践的な応用と注意点


データベースとの連携

JDBC 4.2以降(Java 8と同時にリリース)では、`PreparedStatement`の`setObject`メソッドと`ResultSet`の`getObject`メソッドを使って、`java.time`の各クラスを直接データベースのカラムにマッピングできます。

  • `LocalDate` ↔ `DATE`型
  • `LocalTime` ↔ `TIME`型
  • `LocalDateTime` ↔ `TIMESTAMP`型
  • `OffsetDateTime` ↔ `TIMESTAMP WITH TIME ZONE`型

これにより、面倒な型変換なしに、Javaオブジェクトとデータベース間で日時データをやり取りできます。

旧API (`Date`, `Calendar`) との相互変換

既存のライブラリや古いコードとの互換性のために、旧APIとの変換が必要になることがあります。`Instant`を介して変換するのが最も簡単で確実です。

<?xml version="1.0" encoding="UTF-8"?>
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

public class ConversionExample {
    public static void main(String[] args) {
        // --- Date → LocalDateTime ---
        Date oldDate = new Date();
        Instant instant = oldDate.toInstant();
        LocalDateTime newDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
        System.out.println("DateからLocalDateTimeへ: " + newDateTime);

        // --- LocalDateTime → Date ---
        LocalDateTime now = LocalDateTime.now();
        Instant instantFromNew = now.atZone(ZoneId.systemDefault()).toInstant();
        Date newToOldDate = Date.from(instantFromNew);
        System.out.println("LocalDateTimeからDateへ: " + newToOldDate);
    }
}

改めて注意:イミュータブル(不変性)

`java.time`の操作に慣れる上で最も重要なのは、全ての操作メソッドが新しいインスタンスを返すことを常に意識することです。

<?xml version="1.0" encoding="UTF-8"?>
// 間違ったコード
LocalDate date = LocalDate.of(2025, 1, 1);
date.plusDays(10); // この戻り値を捨ててしまっている!
System.out.println(date); // 結果は "2025-01-01"。date自体の値は変わらない。

// 正しいコード
LocalDate date = LocalDate.of(2025, 1, 1);
LocalDate futureDate = date.plusDays(10); // 新しいインスタンスを受け取る
System.out.println(futureDate); // 結果は "2025-01-11"

この特性を理解することで、意図しないバグを防ぎ、安全で予測可能なコードを書くことができます。

まとめ


Java 8で導入された`java.time` APIは、それまでの日時操作の複雑さや曖昧さを一掃し、堅牢で直感的なプログラミングを可能にしました。

イミュータブルな設計によるスレッドセーフティの確保、目的に応じて使い分ける明確なクラス群、そして流れるようなAPIは、現代のJava開発において必須の知識です。 最初は多くのクラスがあって戸惑うかもしれませんが、`LocalDate`, `LocalDateTime`, `ZonedDateTime`といった中心的なクラスから使い方をマスターすれば、Javaでの日時処理が格段に楽になり、コードの品質も向上するでしょう。

コメントを残す

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