タイムゾーンとは - UTC 基準の時差の仕組みと各国の標準時を解説
タイムゾーンの基本概念から UTC を基準とした時差の決まり方、世界各国の標準時の採用状況までわかりやすく解説します。
Java 8 で標準ライブラリに組み込まれた java.time パッケージは、JSR-310 として設計され、Joda-Time を作ったスティーブン・コルバーンが主導しました。中核となるクラスは Instant、OffsetDateTime、ZonedDateTime、LocalDateTime、LocalDate の 5 つで、それぞれが扱う情報の粒度と意味が異なります。クラスを正しく選ぶことが、タイムゾーン関連のバグを根絶する最大の鍵です。
選び方の原則は「必要十分な情報量を持つクラスを選ぶ」ことです。タイムゾーン情報が不要なら LocalDate や LocalDateTime、UTC オフセットだけ知りたければ OffsetDateTime、IANA タイムゾーンを保持して将来のサマータイムに追従したいなら ZonedDateTime、絶対時刻だけ扱うなら Instant が正解です。曖昧さを避けるためにあえて表現力の低いクラスを選ぶのが熟練者の流儀です。
Instant は 1970-01-01T00:00:00Z (Unix エポック) からの経過秒とナノ秒のペアで時刻を表現します。タイムゾーンの概念を持たず、地球上のあらゆる場所で同じ瞬間を一意に指す「マシンタイム」です。ログのタイムスタンプ、イベントの発生時刻、API のレスポンス時刻のように、人間が住む地域に依存しないタイムスタンプには Instant が最適です。
Instant の演算は単純で、plus(Duration.ofHours(1)) のように物理時間ベースの加算を行います。サマータイム境界をまたいでも常に正確に 1 時間後を返すため、計算結果の予測可能性が高いのが利点です。一方、Instant 単体では「日本時間で何月何日」という人間向けの表示はできず、表示時に ZoneId を組み合わせる必要があります。
ZonedDateTime は LocalDateTime と ZoneId (例: Asia/Tokyo) を組み合わせた最も情報量の多いクラスです。IANA タイムゾーン名を保持しているため、未来のサマータイム変更や歴史的なタイムゾーン規則変更にも追従できます。例えば 2026-11-01T01:30 を America/New_York として持っていれば、サマータイム終了時の曖昧な時刻も withEarlierOffsetAtOverlap() / withLaterOffsetAtOverlap() で明示的に解決できます。
ZonedDateTime の plusHours(1) は、サマータイム境界をまたぐと「壁時計上は 2 時間進む」挙動になります。これは人間にとって直感的な「明日の同じ時刻」を表現するのに適しています。一方、物理時間で 1 時間を進めたい場合は ZonedDateTime を一旦 Instant に変換してから演算する必要があります。Duration を使うか Period を使うかで結果が変わるため、テストでサマータイム境界を必ず通過させる設計が重要です。
OffsetDateTime は ZonedDateTime と似ていますが、保持するのが IANA タイムゾーン名ではなく UTC からのオフセット (+09:00 など) のみです。「2026-05-19T10:00+09:00」のような表現で、その時刻における UTC 差は確定していますが、未来のサマータイム規則を遡って変更する能力はありません。データベース格納や API のシリアライズに向いており、ISO 8601 や RFC 3339 と直接対応するのが利点です。
PostgreSQL の TIMESTAMP WITH TIME ZONE は内部的に UTC で保存されますが、JDBC ドライバは OffsetDateTime や Instant にマッピングします。アプリケーション側で永続化用の DTO を OffsetDateTime にしておけば、DB から読み出した時点で曖昧さが残らず、各種ビジネスロジックは Instant に変換して計算するパターンが安全です。一度 ZonedDateTime に変換してしまうと、過去データが将来のタイムゾーン規則変更で意味がずれる懸念が残ります。
Spring Boot 3.x と Hibernate 6 では、Instant や OffsetDateTime をエンティティのフィールド型として直接使えます。古い実装で java.util.Date や Calendar を使っているコードを置き換える際は、データベース側のカラム型 (TIMESTAMP, TIMESTAMPTZ) と Java 側の型を合わせて選定するのが鉄則です。MySQL の DATETIME と TIMESTAMP は挙動が異なり、TIMESTAMP は UTC で保存されますが DATETIME はタイムゾーンを保持しないので、Java 側で何を使うかによって振る舞いが変わります。
Jackson による JSON シリアライズも要注意です。デフォルトでは Instant がエポック秒の数値になることがあり、API 仕様書とのずれが生じます。spring.jackson.serialization.write-dates-as-timestamps=false を設定し、ISO 8601 文字列として出力するのが現代的な API 設計です。フロントエンド側の Day.js や Luxon もこの形式を想定して設計されているため、相互運用がスムーズに進みます。
実務でクラスを選ぶ際の判断フローは次のとおりです。まず「人間が住む地域の概念が必要か」を問います。ログやイベントのように地域非依存なら Instant です。次に「将来のタイムゾーン規則変更に追従する必要があるか」を問います。ユーザーの将来の予定 (リマインダーなど) なら ZonedDateTime、すでに発生した取引や履歴なら OffsetDateTime が適切です。最後に「タイムゾーンが完全に意味を持たないか」を問い、生年月日や祝日のような日付のみなら LocalDate を使います。
アプリケーション全体で複数のクラスが混在するのは正常な状態です。「すべて ZonedDateTime に統一する」と決めると、ログや履歴で過剰な情報を持ちすぎ、シリアライズ仕様も無用に複雑化します。クラスごとに役割を明確に分け、API 境界・DB カラム・内部ロジックの 3 層でどのクラスを使うかを設計時に決めておくと、後からのリファクタリングコストが大幅に下がります。タイムゾーンバグの 9 割は「型を曖昧にしたまま放置した」ことに起因します。
この記事は役に立ちましたか?
タイムゾーンの基本概念から UTC を基準とした時差の決まり方、世界各国の標準時の採用状況までわかりやすく解説します。
国際日付変更線の位置と役割、なぜジグザグに引かれているのか、日付変更線を越えるとどうなるのかを解説します。
ソフトウェア開発における UTC 変換の正しい実装方法を解説。保存は UTC、表示はローカルの原則から、JavaScript/Python での具体的な実装パターン、よくあるバグの原因まで網羅します。