1. タイムゾーンの概念
世界には多数のタイムゾーンがあります。ミンスクが正午のとき、ニューヨークは朝、東京はすでに夕方です。タイムゾーンを考慮せずに日付と時刻を保存すると簡単に混乱が生じます。例えば、サーバーがドイツにあり、ユーザーがウラジオストクにいる場合、時刻表示 "2025-06-01 12:00" は両者にとってまったく異なる意味になります。
タイムゾーン(timezone)とは、グリニッジ標準時(UTC)に対して、特定地域の「現地」時刻を得るために何時間(何分)加減するかを定める規則のことです。
Java ではタイムゾーンを扱うために ZoneId クラスを使用します。ゾーン識別子の例をいくつか挙げます:
- "Europe/Minsk"
- "UTC"
- "America/New_York"
- "Asia/Tokyo"
なぜ重要か?
- 各国のユーザーに正しい時刻を表示できる。
- イベントの発生時刻を正確に記録できる(例: ログ、チケット予約、締め切り)。
- 夏時間/冬時間への切り替えを考慮できる(ヨーロッパの皆さん、ありがとう)。
2. ZonedDateTime — タイムゾーン込みの日時
ZonedDateTime は、日付と時刻に加えてタイムゾーン情報を保持するクラスです。LocalDateTime に似ていますが、どの地域なのかも「知っている」点が異なります。
ZonedDateTime の作成
システムのタイムゾーンでの現在日時
import java.time.ZonedDateTime;
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now); // 例: 2025-06-01T15:30:00+03:00[Europe/Minsk]
特定のタイムゾーンでの時刻
import java.time.ZoneId;
ZonedDateTime MinskTime = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println("ミンスク: " + MinskTime);
System.out.println("ニューヨーク: " + newYorkTime);
LocalDateTime から作成
import java.time.LocalDateTime;
LocalDateTime meeting = LocalDateTime.of(2025, 6, 1, 18, 0);
ZonedDateTime meetingInMinsk = meeting.atZone(ZoneId.of("Europe/Minsk"));
System.out.println(meetingInMinsk); // 2025-06-01T18:00+03:00[Europe/Minsk]
タイムゾーンの取得と設定
ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
ZonedDateTime tokyoTime = ZonedDateTime.now(tokyoZone);
System.out.println("東京: " + tokyoTime);
ゾーン間の変換: withZoneSameInstant()
同一の出来事が別のゾーンではどう見えるか知りたいときには、withZoneSameInstant() を使います。
ZonedDateTime MinskMeeting = ZonedDateTime.of(2025, 6, 1, 18, 0, 0, 0, ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkMeeting = MinskMeeting.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("ミンスクでの会議の時刻: " + MinskMeeting);
System.out.println("同じ出来事(ニューヨーク): " + newYorkMeeting);
注意: withZoneSameInstant() は、同じ瞬間になるように時刻を別のゾーンへ変換します。withZoneSameLocal() を使うと日付と時刻はそのままでゾーンだけが変わるため、ほとんどの場合は誤りです。
3. Instant — 絶対的な時点
Instant は、タイムゾーンに依存しない絶対的な瞬間を表すクラスです。技術的には、1970年1月1日の UTC から経過した秒数とナノ秒で表現されます。もし時刻にパスポートがあるなら、Instant はその番号のようなものです。
Instant の作成
import java.time.Instant;
Instant now = Instant.now();
System.out.println(now); // 例: 2025-06-01T12:30:00.123Z
末尾の Z は「Zulu time」、すなわち UTC を意味します。
UNIX エポックからの秒数から作成
Instant fromEpoch = Instant.ofEpochSecond(1685616000L);
System.out.println(fromEpoch); // 2023-06-01T00:00:00Z
Instant ↔ ZonedDateTime/LocalDateTime の変換
ZonedDateTime から Instant へ
ZonedDateTime zoned = ZonedDateTime.now();
Instant instant = zoned.toInstant();
System.out.println(instant);
Instant から ZonedDateTime へ
ZoneId zone = ZoneId.of("Europe/Minsk");
ZonedDateTime fromInstant = Instant.now().atZone(zone);
System.out.println(fromInstant);
Instant から LocalDateTime へ
import java.time.LocalDateTime;
import java.time.Instant;
import java.time.ZoneId;
LocalDateTime local = LocalDateTime.ofInstant(Instant.now(), ZoneId.of("Europe/Minsk"));
System.out.println(local);
4. 実践: 複数タイムゾーンの現在時刻、ゾーン間の変換
複数のタイムゾーンでの現在時刻の取得
ミンスク、ニューヨーク、東京の現在時刻を表示するミニアプリを作ります。
import java.time.ZonedDateTime;
import java.time.ZoneId;
public class TimeZonesDemo {
public static void main(String[] args) {
ZonedDateTime Minsk = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYork = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println("ミンスク: " + Minsk);
System.out.println("ニューヨーク: " + newYork);
System.out.println("東京: " + tokyo);
}
}
タイムゾーン間の変換
例えば、ミンスクの18:00に予定されたイベントがあるとします。ニューヨークや東京では何時になるでしょうか?
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class MeetingTime {
public static void main(String[] args) {
LocalDateTime eventTime = LocalDateTime.of(2025, 6, 1, 18, 0);
ZonedDateTime minskEvent = eventTime.atZone(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkEvent = minskEvent.withZoneSameInstant(ZoneId.of("America/New_York"));
ZonedDateTime tokyoEvent = minskEvent.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("ミンスクでの会議: " + minskEvent);
System.out.println("ニューヨークでは: " + newYorkEvent);
System.out.println("東京では: " + tokyoEvent);
}
}
LocalDateTime を ZonedDateTime に変換し、元に戻す
Local → Zoned:
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
LocalDateTime localTime = LocalDateTime.of(2025, 6, 1, 14, 0);
ZonedDateTime zonedTime = localTime.atZone(ZoneId.of("Europe/Minsk"));
System.out.println(zonedTime);
Zoned → Local:
LocalDateTime extracted = zonedTime.toLocalDateTime();
System.out.println(extracted);
5. 重要な注意点とニュアンス
なぜ LocalDateTime だけを保存してはいけないのか
LocalDateTime はタイムゾーンのない単なる日付と時刻です。多くの業務ロジックには不十分です。例えば "2025-06-01 12:00" を LocalDateTime として保存した場合、ミンスクのユーザーとニューヨークのユーザーにとって、それは現実世界の異なる瞬間を意味します。
絶対時刻(例: Instant)を保存するか、イベントが特定のゾーンに結びついているなら(ZonedDateTime)を保存してください。LocalDateTime が有効なのは、「浮動」する日付(例: タイムゾーンや時刻に依存しない誕生日)のような場合です。
夏時間/冬時間に伴う問題
タイムゾーンは UTC に対するオフセットだけでなく、夏時間/冬時間への切り替え規則も含みます。例えば、ある国では特定の日に時刻を1時間進めたり戻したりします。LocalDateTime だけを保存していると、その時刻がそもそも存在したかどうかを判別できません。
「時間の穴」の例:
- 米国では3月の午前2時に時計を午前3時に進めます。
- ニューヨークでは "2025-03-10 02:30" という時刻は存在しませんでした。
ZonedDateTime を使えば、このようなサプライズから身を守れます。ライブラリが時刻の妥当性を自動で検証してくれます。
関係図: LocalDateTime、ZonedDateTime、Instant の関係
graph TD
A["LocalDateTime
(日付 + 時刻、タイムゾーンなし)"] -->|+ ZoneId| B["ZonedDateTime
(日付 + 時刻 + ゾーン)"]
B -->|"toInstant()"| C["Instant
(絶対時刻、UTC)"]
C -->|"atZone(ZoneId)"| B
B -->|"toLocalDateTime()"| A
6. ZonedDateTime と Instant を扱う際の典型的なミス
誤り1: グローバルなイベントに LocalDateTime を使う。
異なる国のユーザー同士の会議時刻を LocalDateTime として保存すると、各自が自分の「12:00」を見ることになり、実際には異なる瞬間を指します。グローバルなイベントには ZonedDateTime か Instant を使用しましょう。
誤り2: 文字列のパース時にタイムゾーンを無視する。
文字列 "2025-06-01T12:00:00" をゾーンの指定なしにパースすると、得られるのは LocalDateTime で、ZonedDateTime ではありません。ZonedDateTime を得たい場合は、ゾーン付きの文字列を使うか、明示的にゾーンを追加してください。
誤り3: ゾーン間の変換方法を間違える。
withZoneSameInstant() の代わりに withZoneSameLocal() を使うと誤った時刻になります。同一の瞬間を別のゾーンで得たい場合は、常に withZoneSameInstant() を使いましょう。
誤り4: 夏時間/冬時間の切り替えを考慮しない。
切り替え境界でイベントを計画する場合は、必ず ZonedDateTime を使い、ライブラリに任せましょう。すべての切り替えや「時間の穴」を把握しています。
誤り5: ゾーンを無視して ZonedDateTime を比較する。
ローカル時刻が同じでも、ゾーンが異なる2つの ZonedDateTime は異なる瞬間を表し得ます。比較には toInstant() を使用してください。
GO TO FULL VERSION