連載
» 2014年12月17日 18時00分 公開

ImmutableでスレッドセーフになったJavaの新しい日時APIの基礎知識ここが大変だよJava 8 Date-Time API(1)(2/5 ページ)

[長谷川智之,株式会社ビーブレイクシステムズ]

Java 8 Date-Time APIの概要

 Java 8からの導入されたDate-Time APIは、java.timeパッケージとその下にあるサブパッケージにて定義されています。このDate-Time APIは次の2つのコンセプトの時間を扱っています。

・人が認識しやすい年、月、日、時、分、秒などの単位で日時を扱うクラス

・コンピューターが認識するエポック(1970年1月1日の0時0分0秒)からの経過時間(タイムライン)で扱うクラス

人が認識しやすい単位で日時を扱うクラス

 まず、この人が認識しやすい単位で日時を扱うクラスに対して、Date-Time APIでは「ISO 8601」という日時を表すための国際規格を基にしたクラスを基本に提供しています。このISO 8601とは簡単に説明すると、私たちが「西暦」と言っているグレゴリオ暦を基準にした日付や時刻を表す世界的な書式の規格で、「どのように年、月、日、時、分、秒、などを表すか」「どのようにタイムゾーンによる地域ごとでの時差などを表現するか」を定めた国際規格です。

 ISO 8601では、さまざまな種類の日時の表現が用意されていますが、一般的に用いられるのは年月日による日付の表現と時分秒を表す時刻の表現です。

 ISO 8601の基本表記と呼ばれるものは年月日や時分秒の区切りを入れずに表現しますが、Java 8では拡張表記と呼ばれる、年月日を用いた表現には「年-月-日」(「YYYY-MM-DD」)と「-」(ハイフン)を用いて区切り、時分秒を用いた表現には「時:分:秒」(hh:mm:ss)と「:」(コロン)を用いて区切る表記法をデフォルトで採用しています。

 また、日付と時刻の両方を表す際は、日付と時刻の間に「T」を記述します。例えば、2014年1月1日9時0分0秒を表現する場合は「2014-01-01T09:00:00」となります。

 そのため、Java 8のDate-Time APIのインスタンスが、toStringメソッドではISO 8601の拡張表記を用いた文字列が生成されます。デフォルトで用意されているparseメソッドで読み込む文字列は、日付に「-」を、時刻に「:」を用いた拡張表記での表現に対し解析が行えます。

 また、ISO 8601では国際化の対応も可能で、後述のUTCと呼ばれる世界標準時を持つ地点からの時差を「+」もしくは「-」と「時:分」(「:」はなくてもよい)をそれぞれ2桁の数字を使って表現します。例えばUTCから9時間進んだ時差がある地域での「2014年1月1日9時0分0秒」を表現する場合は「2014-01-01T09:00:00+09:00」となります。逆に同じ時点のUTCを持つ地域では「2014年1月1日0時0分0秒」になります。

LocalDateTime localDateTime = LocalDateTime.of(2014, Month.JANUARY, 1, 9, 0, 0);
OffsetDateTime offsetDateTime1 = OffsetDateTime.of(localDateTime, ZoneOffset.of("+09:00"));
System.out.println("offsetDateTime1=" + offsetDateTime1);
 
// 同じ時点のUTCの日時を取得
Instant instant = offsetDateTime1.toInstant();
OffsetDateTime offsetDateTime2 = instant.atOffset(ZoneOffset.UTC);
System.out.println("offsetDateTime2=" + offsetDateTime2);
サンプル
offsetDateTime1=2014-01-01T09:00+09:00
offsetDateTime2=2014-01-01T00:00Z
実行結果

 しかし、地域によってはISO 8601とは異なるカレンダーシステムも存在します。私たちが使っている和暦もISO 8601とは異なる年の概念を持っています。Date-Time APIではこれらのISO 8601とは違うカレンダーシステムにも対応しやすい設計がなされており、代表的なものは「java.time.chrono」パッケージ下に用意されています。私たちがよく使っている和暦で年を表した日付を扱えるクラスも「java.time.chrono.JapaneseDate」として用意されています。

エポックからの経過時間で時間を表す「Instant」クラス

 また、Date-Time APIでは、「エポック」と呼ばれる特別な時点(1970年1月1日の0時0分0秒)からの経過時間で時間を表す「Instant」というクラスがあります。これは先ほどのサンプルでは同じ時点を表すInstantを、違う時差を扱うクラスに受け渡すために使われています。このInstantクラスは旧日時APIのjava.utilDateに一番近いクラスかもしれません。

 そして、Java 8ではDate-Time APIと旧日時APIに変換する方法が用意されています。同様に旧日時APIにもDate-Time APIに変換するためのメソッドも追加されています。ただし、もともとDate-Time APIは旧日時APIを拡張して使うようなことを意図してデザインされたものではないため、Date Time APIのクラスと旧日時APIとが、Date Time APIのどのクラスに対しても簡単でシンプルな変換ができるわけではない点を注意してください。

Localとタイムゾーンを扱う3つのクラス

 それでは、人が認識しやすい年や月などの単位で扱うクラスについて見ていきましょう。この新たに導入された年や月などの単位で扱うクラスは、国際化にも対応できるよう、大きく分けて次の3つに分類できます

  • タイムゾーンを考慮しない「Local」で始まるクラス
  • 特定の場所で同じ時間を共有する地域(タイムゾーン)が設定された「Zoned」で始まるクラス
  • UTC(協定世界時)からの時差が設定された「Offset」で始まるクラス

 このUTC(協定世界時)とは、簡単に説明すると、世界的に標準と見なされている時間のことです。各地域ではUTCを基準にどのくらいの時差があるかで各地の標準時を決めています。基本的にはイギリスにあるグリニッジと同じ緯度にある地域を“時差なし”としています。日本の場合は+09:00で9時間のプラスの時差があることになるので、UTCより9時間先に進んでいることになります。逆に言うと、日本の時刻から9時間引いたものがUTCとなります。

 この時差はプラスだけではなくマイナスもあり、イギリスより西にあるロサンゼルスでは、後述するサマータイムを考慮しないと、-08:00で8時間のマイナスの時差があり、UTCより8時間遅れていることになります。

 また、地域によってはサマータイム制といい、夏になると時刻を進めて、冬になると時刻を戻すことによって、時計が示す時間と日照時間を調整する仕組みを導入しているところもあります。例えばアメリカのシカゴでは通常時はUTCから-6時間の時差を持っていますが、サマータイム時は-5時間の時差になります。

コラム「サマータイムについて」

 サマータイム制は別名「Daylight Saving Time」とも言い、時間を早めることにより日照時間を多くしようという仕組みです。例えば、農業や酪農など外での仕事をしているとして、朝7時に起きて夜19時に外での作業を終えるような生活をしている場合、1時間時刻が早まることにより、朝起きてから外での作業が終了するまでの活動時間の日照時間が長くなることになります。

※上記の図では黄色の箇所が日の当たる時間を表し、暗くなるにつれて日の光が弱くなっていくことを表現しています。

 このようにサマータイム制を導入することにで、活動時間により多くの日が当たるというメリットがあり、世界ではこのサマータイム制を導入している地域が多数あります。また、多くの地域では1時間ずらすようになっていますが、いくつかの地域では30分ずらすなどの地域もあります。

 余談ですが、日本でも戦後間もなくの頃は一時的にサマータイム制を導入した時期もあったそうです。


「Local」で始まるクラス

 「Local」で始まるクラスは単純に年や月などの日付や時刻の各単位で構成されているクラスです。このクラスではタイムゾーンや地域ごとの時差についての情報を持っていません。

「Zoned」で始まるクラス

 「Zoned」で始まるクラスは年や月などの日付や時刻の情報に加え、同じ時間を共有する地域のタイムゾーンのIDの情報を持っているクラスです。タイムゾーンのIDより地域の情報とUTCからの時差を持つことになります。また地域によってはサマータイム制を導入しているところもあるので、「Zoned」で始まるクラスはそのような地域での規約について情報も持っています。

 「Zoned」で始まるクラスは「ZoneId」というタイムゾーンを表すIDのクラスを持っています。このZoneIdは基本的には「Asia/Tokyo」のような地域を表すIDの文字列を解析して生成されます。そしてこの地域を表すZoneIdの場合、その地域でサマータイム制のようなルールを適用したZoneRulesクラスが設定されています。

 また、ZoneIdは後述するUTCからの時差を表すZoneOffsetの親クラスであり、「+」や「-」をつけたUTCからの時差、例えば「+09:00」のように、時差を表す文字列をZoneIdとして解析することが可能です。ただし、その場合は同じ時差を持っていたとしてもサマータイム制を適用したZoneRulesは設定されていません。

 例えば、アメリカのロサンゼルスでは2014年3月9日の午前2時からサマータイム制が導入され午前2時が午前3時となるのですが、これを「America/Los_Angeles」で定義したZoneIdと「-08:00」で定義したZoneIdの場合とで違いを見てみましょう。

// 地域のZoneIdを持ったZonedDateTime
ZonedDateTime zonedDateTimeOfRegion = ZonedDateTime.of(2014, 3, 9, 1, 59, 0, 0, ZoneId.of("America/Los_Angeles"));
// 時差のZoneIdを持ったZonedDateTime
ZonedDateTime zonedDateTimeOfOffset = ZonedDateTime.of(2014, 3, 9, 1, 59, 0, 0, ZoneId.of("-08:00"));
 
System.out.println("zonedDateTimeOfRegion=" + zonedDateTimeOfRegion);
System.out.println("zonedDateTimeOfOffset=" + zonedDateTimeOfOffset);
 
// 1分経過
ZonedDateTime oneMinuteLaterOfRegion = zonedDateTimeOfRegion.plusMinutes(1L);
ZonedDateTime oneMinuteLaterOfOffset = zonedDateTimeOfOffset.plusMinutes(1L);
 
System.out.println("oneMinuteLaterOfRegion=" + oneMinuteLaterOfRegion);
System.out.println("oneMinuteLaterOfOffset=" + oneMinuteLaterOfOffset);
zonedDateTimeOfRegion=2014-03-09T01:59-08:00[America/Los_Angeles]
zonedDateTimeOfOffset=2014-03-09T01:59-08:00
oneMinuteLaterOfRegion=2014-03-09T03:00-07:00[America/Los_Angeles]
oneMinuteLaterOfOffset=2014-03-09T02:00-08:00
実行結果

「Offset」で始まるクラス

 「Offset」で始まるクラスは年や月などの日付や時刻の情報に加え、UTC(協定世界時)からの時差の情報を持っているクラスです。単純にUTCから時差の時間をプラスもしくはマイナスで設定するだけなので、サマータイム制のような地域による独自のルールには対応していません。この「Offset」で始まるクラスはUTCからの時差の情報を表すZoneOffsetというクラスを持っています。

 ZoneOffsetが解析できる主な書式は次のものになります。UTCの場合は0(ゼロ)を表す「Z」、UTCからの時差がある地域の場合は「+」(プラス)もしくは「-」(マイナス)をつけた時(「h」)と分(「m」)を設定します。また、時の単位しか表さない場合は1桁でも解析されますが、基本的には頭に0を付けた形式の表現をします。

  • Z
  • +h
  • -h
  • +hh
  • -hh
  • +hh:mm
  • -hh:mm
  • +hhmm
  • -hhmm

 ただし、ZoneOffsetの親クラスはZoneIdであるため、systemDefaultメソッドをZoneOffsetから呼び出してもZoneIdが返ってくるので注意が必要です。

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。