ZonedDateTime — その概要、タイムゾーンと日時の操作

著者: IT Sectr 公開日: 2026-07-13 読了時間: 10 分

ZonedDateTime はjava.timeパッケージの不変(immutable)クラスで、タイムゾーン情報(ZoneId)とともに日付と時刻を格納します。LocalDateTimeとは異なり、ZonedDateTimeはタイムライン上の瞬間を一意に識別します。Oracle Java 17 (2024)の仕様によると、このクラスはIANA Time Zone Databaseのゾーンルールを通じて夏時間(DST)の移行を正しく処理します。

重要なポイント

  • ZonedDateTime は日付、時刻、タイムゾーン(ZoneId)を1つのオブジェクトに統合する不変クラスです。
  • LocalDateTimeとは異なり、ZonedDateTime はタイムライン上の瞬間を一意に定義し、グローバルシステムに適しています。
  • このクラスはIANA Time Zone Databaseのルールに従って夏時間(DST)の移行を自動的に処理します。
  • タイムゾーン間の変換には withZoneSameInstant(ZoneId) メソッドを使用します。
  • データベースへのZonedDateTimeの保存は OffsetDateTime またはTIMESTAMP WITH TIME ZONEを介して推奨されます。

ZonedDateTimeとは?

ZonedDateTime はjava.timeパッケージの主要なクラスの1つで、完全なタイムゾーン情報とともに日付と時刻を表現します。これは3つのコンポーネントを組み合わせます: LocalDateTime(日付と時刻)、ZoneId(ゾーン識別子)、ZoneOffset(UTCからのオフセット)。

LocalDateTime がタイムゾーンの結びつけなしに壁時計時間(wall-clock time)のみを格納するのとは異なり、ZonedDateTimeは瞬間を一意に識別します。異なるタイムゾーンにある2つの同一のLocalDateTimeインスタンスは、時間的に異なる瞬間を表します。2つの同一のZonedDateTimeインスタンスは同じ瞬間を表します。

このクラスは完全に不変でスレッドセーフです。すべての算術演算は新しいオブジェクトを返します。ZonedDateTimeは ChronoZonedDateTime インターフェースを実装しており、Javaでゾーン時間の処理が必要な場所ならどこでも使用できます。

Oracle Java 17の仕様によると、ZonedDateTimeは600以上のタイムゾーンを含むIANA Time Zone Databaseの任意のゾーンでの操作をサポートしています。

ZonedDateTime vs LocalDateTime: 違いは何ですか?

主な違い — ZonedDateTime にはタイムゾーンが含まれていますが、LocalDateTime には含まれていません。この基本的な違いが各クラスの適用範囲を決定します。

LocalDateTimeはローカルイベントに使用されます: コンサートの時間、授業のスケジュール、生年月日。イベントがモスクワで15:00に発生する場合、LocalDateTimeは結びつけなしに15:00を記録します。サーバーをニューヨークに移動しても、時間は15:00のままですが、それは異なる物理的な瞬間になります。

ZonedDateTimeはグローバルデータに使用されます: サーバーログ、APIのタイムスタンプ、国際会議。会議が15:00 MSKに予定されている場合、ZonedDateTimeは時間とゾーンの両方を保持します。ニューヨークでは8:00 ESTとして正しく表示されます。Baeldung (2024)によると、LocalDateTimeとZonedDateTimeの選択は、日付を扱う際の最も一般的なアーキテクチャ上の決定です。

実用的なルール: データが1つの地域に対して保存される場合 — LocalDateTimeを使用します。データがタイムゾーンの境界を越える場合 — ZonedDateTimeを使用します。絶対的な瞬間を表現する必要がある場合 — Instant を使用します。

java.timeでのタイムゾーンの仕組み

java.timeのタイムゾーンは ZoneId クラスによって表現されます。ZoneIdは「大陸/地域」形式のゾーン識別子で、例えば「Europe/Moscow」「America/New_York」「Asia/Tokyo」などです。ZoneIdは静的メソッドof(String zoneId)またはシステムのデフォルトタイムゾーンを通じて取得されます。

ZoneIdは2つのタイプに分類されます: fixed offset(固定オフセット、例「+03:00」)と region-based(地域ゾーン、例「Europe/London」)。地域ゾーンには夏時間移行ルールと歴史的な変更が含まれています。Fixed offsetは単なる固定オフセットです。

特定の瞬間におけるZoneIdの現在のオフセットを取得するには、getRules() メソッドを使用します。これはZoneRulesを返します。ZoneRulesには、特定のゾーンのすべての移行とオフセットが含まれています。これは正しいDST処理のための重要なメカニズムです。

すべてのタイムゾーンは tzdata ファイル(IANA Time Zone Database)を介してJDKに同梱されており、定期的に更新されます。Androidでは、tzdataのバージョンはGoogle Play Servicesを介したシステムアップデートに依存します。

ZonedDateTimeの作成

ZonedDateTimeはいくつかの方法で作成できます。最も簡単なのは now() で、システムのタイムゾーンでの現在時刻を返します。now(ZoneId)のバリアントは、指定されたゾーンでの現在時刻を取得できます。

of(LocalDateTime, ZoneId) メソッドは、ローカル時刻とゾーンからZonedDateTimeを作成します。of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone)のバリアントはコンポーネントから作成します。

LocalDateTimeは atZone(ZoneId) メソッドを介してZonedDateTimeに変換できます。Instantは Instant.atZone(ZoneId) を介して変換できます。DateはDate.toInstant().atZone(ZoneId)を介して変換できます。

kotlin
val moscowZone = ZoneId.of("Europe/Moscow")

val nowInMoscow = ZonedDateTime.now(moscowZone)

val fromComponents = ZonedDateTime.of(
    2026, 7, 21, 15, 30, 0, 0, moscowZone
)

val fromLocal = LocalDateTime.now().atZone(moscowZone)

val fromInstant = Instant.now().atZone(moscowZone)

タイムゾーン間の変換

主要な変換メソッドは withZoneSameInstant(ZoneId) です。これは同じ瞬間を維持しながらZonedDateTimeを別のタイムゾーンに変換します。例: 15:00 MSK → 8:00 EST。withZoneSameLocal(ZoneId)メソッドはローカル時刻を維持しながらゾーンを変更します — これは異なる瞬間をもたらします。

UTCからのオフセットを取得するには、getOffset() メソッドを使用します。これはZoneOffsetを返します。ZoneOffsetはZoneIdのサブクラスで、「+HH:mm」または「-HH:mm」形式の固定オフセットを表します。

Instantへの変換は toInstant() メソッドを介して行われます。Instantはタイムゾーンに依存しない、時間の絶対的な瞬間です。逆変換はInstant.atZone(ZoneId)です。

kotlin
val moscow = ZonedDateTime.of(
    2026, 7, 21, 15, 0, 0, 0,
    ZoneId.of("Europe/Moscow")
)

val newYork = moscow.withZoneSameInstant(
    ZoneId.of("America/New_York")
)

val utcInstant = moscow.toInstant()
val backToMoscow = utcInstant.atZone(ZoneId.of("Europe/Moscow"))

夏時間(DST)の取り扱い

夏時間の移行は2つの問題を引き起こします: ギャップ(gap)オーバーラップ(overlap)です。ギャップは春に時計が進められたときに発生し — 特定の時刻が存在しなくなります。オーバーラップは秋に時計が戻されたときに発生し — 同じ時刻が2回発生します。

ZonedDateTimeは 解決(resolve) 戦略を通じてこれらの状況を処理します。ギャップ中にオブジェクトを作成する場合、java.timeは自動的にオフセット量だけ時刻をシフトします。オーバーラップ中に作成する場合、最初のオプション(移行前)が選択されます。この動作はwithZoneSameInstantで変更できます。

zone.getRules().isDaylightSavings(instant) を使用して、時刻がDST内にあるかどうかを確認できます。getOffset()メソッドは特定の瞬間の実際のオフセットを表示し、getRules().getDaylightSavings(instant) はミリ秒単位のDST調整量を表示します。

kotlin
fun checkDST(zdt: ZonedDateTime) {
    val rules = zdt.getZone().getRules()
    val instant = zdt.toInstant()

    if (rules.isDaylightSavings(instant)) {
        val dstAmount = rules.getDaylightSavings(instant)
        Log.d("夏時間(DST)", "DSTオフセット: $dstAmount")
    }
}

ZonedDateTimeのフォーマット

ZonedDateTimeをフォーマットするには DateTimeFormatter を使用します。標準のISO形式には日付、時刻、オフセットが含まれます: 「2026-07-21T15:30:00+03:00[Europe/Moscow]」。定義済みフォーマット: ISO_ZONED_DATE_TIME、ISO_OFFSET_DATE_TIME、ISO_INSTANT。

ローカライズされた出力には DateTimeFormatter.ofLocalizedDateTime(FormatStyle) を使用します。FormatStyleはSHORT、MEDIUM、LONG、FULLに設定できます。LONGにはゾーン名(「MSK」)が含まれ、FULLには完全な名前(「Moscow Standard Time」)が含まれます。

重要: ZonedDateTimeで文字列を解析する場合、形式にはゾーンまたはオフセット情報が含まれている必要があります。ゾーンが指定されていない場合は、LocalDateTime.parse()を使用してからatZone()を使用します。

kotlin
val zdt = ZonedDateTime.now(ZoneId.of("Europe/Moscow"))

val iso = zdt.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)

val custom = DateTimeFormatter
    .ofPattern("dd.MM.yyyy HH:mm z")
val formatted = zdt.format(custom)

val parsed = ZonedDateTime.parse(
    "2026-07-21T15:30:00+03:00",
    DateTimeFormatter.ISO_OFFSET_DATE_TIME
)

AndroidでのZonedDateTime: 実践例

最初の例 — ユーザーのローカルタイムゾーンで会議時間を表示します。サーバーはUTCでZonedDateTimeを送信し、クライアントはデバイスのローカルタイムゾーンに変換します。

kotlin
fun displayMeetingTime(
    serverUtc: ZonedDateTime
): String {
    val deviceZone = ZoneId.systemDefault()
    val localTime = serverUtc.withZoneSameInstant(deviceZone)
    val formatter = DateTimeFormatter
        .ofPattern("dd.MM.yyyy HH:mm z")
    return localTime.format(formatter)
}

2番目の例 — タイムゾーンを考慮して次のイベントまでの時間を計算します。サーバー時間には ZonedDateTime を、差分の計算には Duration.between() を使用します。

kotlin
fun timeUntilEvent(eventTime: ZonedDateTime): String {
    val now = ZonedDateTime.now()
    val duration = Duration.between(now, eventTime)

    val hours = duration.toHours()
    val minutes = duration.toMinutes() % 60
    return "Remaining $hours h $minutes min"
}

3番目の例 — Retrofit APIを使用します。サーバーはゾーン付きのISO-8601文字列を返します。ZonedDateTime に変換するためにカスタムデシリアライザーを使用します。

kotlin
data class EventResponse(
    @JsonAdapter(ZonedDateTimeAdapter::class)
    val eventTime: ZonedDateTime
)

class ZonedDateTimeAdapter : JsonAdapter<ZonedDateTime>() {
    override fun fromJson(reader: JsonReader): ZonedDateTime? {
        return ZonedDateTime.parse(
            reader.nextString()
        )
    }
}

タイムゾーン操作におけるよくある間違い

1つ目の間違い — サーバーコードで ZoneId.systemDefault() を使用すること。サーバーのタイムゾーンはクライアントと異なる可能性があり、サーバーでシステムゾーンを使用すると誤った計算につながります。常にゾーンを明示的に指定するか、UTCを基準として使用してください。

2つ目の間違い — 期間の計算時にDSTを無視すること。Duration.between()は移行を正しく処理しますが、手動でタイムスタンプを減算すると、夏時間が1時間の誤差を引き起こす可能性があります。手動計算の代わりに ChronoUnit.HOURS.between() を使用してください。

3つ目の間違い — withZoneSameInstantとwithZoneSameLocalを混同すること。前者は瞬間を維持しながらゾーンを変更し — 時間がシフトします。後者はローカル時刻を維持しながらゾーンを変更し — 瞬間が変わります。SonarSource (2024) によると、誤ったメソッドの選択は最も一般的な間違いの1つです。

4つ目の間違い — デバイスのタイムゾーンが常にユーザーのタイムゾーンと同じであると想定すること。ユーザーは旅行中で、現在のゾーンではなく「自宅」のタイムゾーンで時間を表示するようアプリに期待する場合があります。この場合は、インターフェースを通じてゾーンの選択を提供してください。

よくある質問

ZonedDateTimeとOffsetDateTimeの違いは何ですか?

ZonedDateTimeには地域ゾーン識別子(例: 「Europe/Moscow」)が含まれ、DSTを処理します。OffsetDateTime は地域ルールなしに固定オフセット(+03:00)のみを格納します。データベース保存にはOffsetDateTimeが推奨されます。

ZonedDateTimeでUTCの現在時刻を取得するには?

ZonedDateTime.now(ZoneOffset.UTC) または Instant.now().atZone(ZoneOffset.UTC) を使用します。どちらのオプションもゼロオフセットで現在の瞬間を返します。単純なタイムスタンプには、ゾーンの結びつけなしでInstant.now()を使用します。

ZonedDateTimeはGsonやMoshiでシリアライズできますか?

はい、ただしカスタムアダプターが必要です。GsonはデフォルトではZonedDateTimeをサポートしていません。MoshiはRfc3339DateJsonAdapterを介してサポートしています。Jacksonには Kotlinx Serialization または JavaTimeModule ライブラリの使用が推奨されます。

時刻がDSTギャップに該当する場合の対処方法は?

java.timeは自動的にオフセット量だけ時刻を進めます。例えば、時計が03:00に進められたときに02:30が存在しない場合、ZonedDateTime は03:30でオブジェクトを作成します。ZoneRules.getTransition(instant)でギャップを確認できます。

SQLデータベースにZonedDateTimeが推奨されない理由は?

JDBC 4.2はOffsetDateTimeをサポートしていますが、ZonedDateTimeを直接サポートしていません。ZonedDateTime にはSQLに相当するものがない地域ゾーンが含まれています。OffsetDateTimeまたはInstantを保存し、ゾーンは別のカラムに保存することをお勧めします。

まとめ

  • ZonedDateTime はタイムゾーン付きの日付と時刻のための不変クラスで、IANA Time Zone Databaseを通じてDSTを正しく処理します。
  • LocalDateTime との主な違いはゾーンの存在であり、ZonedDateTimeは時間の瞬間の一意の識別子となります。
  • ゾーン間の変換には、瞬間を保持する withZoneSameInstant() を使用し、withZoneSameLocalは使用しないでください。
  • 夏時間の移行中、java.timeは組み込みのゾーンルールを通じてギャップとオーバーラップを自動的に解決します。
  • データベース保存には OffsetDateTime を使用するか、InstantとZoneIdを別々に保存します。
  • AndroidでZonedDateTimeをデバイスのローカル時刻に変換するには、ZoneId.systemDefault() をwithZoneSameInstantと組み合わせて使用します。
  • JSONシリアライゼーションにはカスタムアダプターが必要です — Kotlinx SerializationまたはJackson JavaTimeModuleを使用します。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください