概要

タイムゾーン起因の不具合は、開発環境では再現せず本番で初めて発覚することが多い、質の悪い部類の問題です。日本国内向けのシステムでも、クラウド上のサーバが UTC で動いていたために保存時刻が9時間ずれていた、という話は珍しくありません。Java 8 以降の ZonedDateTime と ZoneId を使えば時差の変換自体は難しくありませんが、本当に重要なのは「どの時点で、どのタイムゾーンとして扱うか」という設計の方です。この記事では、JST・UTC・海外拠点間の変換の基本操作を確認したうえで、UTC で保存して表示時に変換する定石パターン、サマータイムの切り替わりが絡むときの注意点までを整理します。

使いどころ

UTC で動くクラウドサーバと日本のクライアント間で、注文日時をずれなく保存・表示する

米国拠点とのファイル連携で、先方のタイムスタンプ(EST/EDT)を JST に変換して照合する

障害調査で、アプリログ(JST)とクラウド監視サービスのログ(UTC)の時刻を突き合わせる

コード例

複数タイムゾーンの時刻を比較する
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class TimezoneCompare {

    public static void main(String[] args) {
        var fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z");

        // 日本時間 2025-03-10 09:00 を各拠点の時刻へ変換
        var jst = ZonedDateTime.of(2025, 3, 10, 9, 0, 0, 0,
            ZoneId.of("Asia/Tokyo"));
        for (var zone : java.util.List.of(
                "Asia/Tokyo", "UTC", "America/New_York")) {
            System.out.printf("%-18s %s%n", zone,
                jst.withZoneSameInstant(ZoneId.of(zone)).format(fmt));
        }
        // Asia/Tokyo        2025-03-10 09:00 JST
        // UTC               2025-03-10 00:00 UTC
        // America/New_York  2025-03-09 20:00 EDT

        // サマータイム境界: NY は 2025/3/9 に夏時間入りするため、
        // 前々日の同時刻では JST との時差が 1 時間広い
        var beforeDst = ZonedDateTime.of(2025, 3, 8, 9, 0, 0, 0,
            ZoneId.of("Asia/Tokyo"));
        System.out.println(beforeDst
            .withZoneSameInstant(ZoneId.of("America/New_York"))
            .format(fmt));
        // → 2025-03-07 19:00 EST(時差 14 時間。夏時間中は 13 時間)
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

基本的な使い方は Java 8 と同じ。switch 式で出力フォーマットの切り替えが簡潔に書ける。

Java 17
// Java 17: DB へは UTC で保存し、表示時に JST へ変換する
var now = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
// DB保存は UTC、表示は JST パターン
String dbValue = now.withZoneSameInstant(ZoneId.of("UTC"))
    .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
// 取得時に表示用へ変換
var fromDb = ZonedDateTime.parse(dbValue);
String display = fromDb
    .withZoneSameInstant(ZoneId.of("Asia/Tokyo"))
    .format(DateTimeFormatter
        .ofPattern("yyyy/MM/dd HH:mm"));

Library Comparison

Joda-TimeJava 7 以前の保守案件。Java 8 以降は標準 API で十分。
ThreeTen-Extra追加の時間量型や Interval が必要な場合。標準の ZonedDateTime で大半のケースは十分。

注意点

DB には UTC で保存し、表示時にユーザーのタイムゾーンに変換するのが安全。

サマータイムの切り替え時に時刻が重複・欠落する場合がある。

ZoneId.of() に不正な文字列を渡すと DateTimeException が発生する。

DB のタイムスタンプを UTC で保存しているつもりが、JDBC ドライバのデフォルト設定でサーバのローカルタイムに変換されていたケースがある。MySQL / MariaDB の接続 URL には serverTimezone=UTC を明示すること。

FAQ

国内向けシステムでもタイムゾーンを意識する必要がありますか?

あります。クラウドのサーバやコンテナは UTC がデフォルトのことが多く、JVM のデフォルトタイムゾーン頼みのコードは実行環境が変わった瞬間に9時間ずれます。

DB にはどの形式で日時を保存すべきですか?

UTC に統一して保存し、表示時にユーザーのタイムゾーンへ変換するのが定石です。JDBC ドライバの接続設定でもタイムゾーンを明示しておくと安全です。

ZonedDateTime と OffsetDateTime はどう使い分けますか?

サマータイムの規則ごと扱いたいなら ZonedDateTime、+09:00 のような固定オフセットでの保存・データ交換には OffsetDateTime が向きます。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます