Java Magazinfe2014年3月号からの転載。講読申込み受付中。 |
Ben Evans、Richard Warburton著
新しい日付/時間ライブラリが必要になる理由
一般的な開発者が扱うような日付/時間のユースケースがJavaで十分にサポートされていないことは、Java開発者にとって長年の悩みの種でした。
たとえば、既存のクラス(java.util.DateやSimpleDateFormatterなど)はスレッド・セーフでないため、並行処理時に問題が発生する可能性があります。つまり、平均的な開発者が日付を処理するコードを記述する際に期待するようなものにはなっていません。
また、一部の日付/時間クラスのAPI設計には不備があります。たとえば、java.util.Dateの開始年は1900、開始月は1、開始日は0であり、あまり直感的ではありません。
他にもさまざまな問題があることから、Joda-Timeなどのサード・パーティの日付/時間ライブラリが普及してきました。
種々の問題を解決し、JDKコアでのサポートを強化するために、新しいDate and Time APIの設計がJava SE 8向けに行われてきました。
このプロジェクトはJSR 310の下で、Joda-Timeの作者(Stephen Colebourne氏)とオラクルが共同で主導してきており、新しいJava SE 8ではjava.timeパッケージ内に組み込まれる予定です。
この新APIのよりどころとなる中心的な考え方は次の3つです。
ドメイン駆動型設計:新APIでは、DateおよびTimeに関するさまざまなユースケースを厳密に表すクラスを使用することで、日付/時間ドメインを非常に正確にモデル化します。この点について不備のあった以前のJavaライブラリとは一線を画しています。たとえば、java.util.Dateはタイムライン上の特定の瞬間を表すもので、UNIXエポックを起点とする経過時間(ミリ秒)のラッパーですが、toString()を呼び出した結果にはタイムゾーンが含まれるため、開発者の混乱を招きます。
ドメイン駆動型設計を重視することで、長期的には明確さと理解しやすさという利点が得られます。ただし、以前のAPIからJava SE 8に移植する場合には、そのアプリケーションにおける日付のドメイン・モデルについて入念に検討する必要があるでしょう。
暦(Chronology)の区別:新APIでは、日本やタイなど、ISO-8601に必ずしも準拠していない地域のユーザー・ニーズに対応するため、通常とは異なる暦法(カレンダー・システム)を使用できます。大半の開発者はISO-8601準拠の暦以外を必要としないため、そのような開発者にとって新たな負担とならないように対処されます。
新APIを利用する際におそらく最初に扱うことになるクラスは、LocalDateとLocalTimeです。これらのクラスは、卓上カレンダーや壁掛け時計などのように、見る人の環境における日付と時間を表すという意味においてローカルです。また、LocalDateTimeという、LocalDateとLocalTimeを組み合わせた複合クラスもあります。
既存のクラスはスレッド・セーフでないため、並行処理時に問題―普通の開発者には予想もつかない問題―が発生する可能性があります。
見る人のそれぞれの環境を明確化する概念であるタイムゾーンは、これらのローカル・クラスでは無視されます。そのため、これらのローカル・クラスは、見る人の環境を区別する必要のない状況で利用してください。JavaFXデスクトップ・アプリケーションは、そのような状況の一例に挙げられるでしょう。また、分散システムの場合でもタイムゾーンが同一であれば、時間を表すために使用できます。
新APIのコア・クラスはすべて、流れるようなファクトリ・メソッドによって作成します。複数の構成フィールドによって1つの値を作成するファクトリはofと呼ばれます。一方、別の型から変換するファクトリはfromと呼ばれます。また、文字列をパラメータとして受け取る解析用のメソッドもあります(リスト1)。
LocalDateTime timePoint = LocalDateTime.now(
); // 現在の日付と時刻
LocalDate.of(2012, Month.DECEMBER, 12); // 値から作成
LocalDate.ofEpochDay(150); // 1970年の中頃
LocalTime.of(17, 18); // 今日の帰宅時に利用した電車
LocalTime.parse("10:15:30"); // 文字列から作成
Java SE 8のクラスから値を取得するためには、Javaの標準的なgetter表記規則を使用します(リスト2)。
LocalDate theDate = timePoint.toLocalDate(); Month month = timePoint.getMonth(); int day = timePoint.getDayOfMonth(); timePoint.getSecond();
また、計算を行う目的で、オブジェクトの値を変更することもできます。新APIではすべてのコア・クラスが不変であるため、値を変更するメソッドはwithと呼ばれ、新しいオブジェクトを返します。setterは使用しません(リスト3)。また、各種フィールドに基づいて計算を行うためのメソッドもあります。
// 値を設定すると新しいオブジェクトが返されます
LocalDateTime thePast = timePoint.withDayOfMonth(
10).withYear(2010);
/* メソッドを使用して直接操作するか、
値とフィールドのペアを渡すことができます */
LocalDateTime yetAnother = thePast.plusWeeks(
3).plus(3, ChronoUnit.WEEKS);
新APIにはアジャスタという概念もあります。アジャスタとは、共通の処理ロジックをまとめるために用いるコード・ブロックのことです。WithAdjuster(1つ以上のフィールドを設定するためのクラス)、PlusAdjuster(フィールド同士の加算や減算に使用するクラス)のいずれかを記述できます。値クラスもアジャスタの役割を果たすことができ、その場合は、その値クラス自体が表すフィールド値を更新します。新APIでは組込みのアジャスタが定義されますが、再利用したい固有のビジネス・ロジックがある場合には独自のアジャスタを記述できます。リスト4を参照してください。
import static java.time.temporal.TemporalAdjusters.*; LocalDateTime timePoint = ... foo = timePoint.with(lastDayOfMonth()); bar = timePoint.with(previousOrSame(ChronoUnit.WEDNESDAY)); // 値クラスをアジャスタとして使用 timePoint.with(LocalTime.now());
新APIでは、日付、時間、日付/時間を表す型をそれぞれ提供することで、特定の時点を異なる精度で扱うことができますが、これらの型では扱うことのできない、より精度が細かい時間のデータも当然存在します。
truncatedToメソッドは、そのようなユースケースに対応するために存在します。このメソッドを使用すると、指定したフィールドに合わせて値を切り捨てることができます(リスト5)。
LocalTime truncatedTime = time.truncatedTo(ChronoUnit.SECONDS);
これまでに確認したローカル・クラスでは、タイムゾーンによる複雑性が除去されています。タイムゾーンとは、標準時が同一の地域ごとに存在する一連のルールのことです。約40あるタイムゾーンは、協定世界時(UTC)からのオフセット(差)によって定義されます。すべてのタイムゾーンの時間はおおよそ同期して進みますが、一定の時差で区切られます。
タイムゾーンは、省略名(例:「PLT」)と正式名(例:「Asia/Karachi」)の2種類の識別子によって表すことができます。アプリケーションの設計時には、タイムゾーンの利用が適したシナリオや、オフセットの利用が適したケースについて検討する必要があります。
ZoneId:地域を表す識別子(リスト6)。各ZoneIdは、その地域のタイムゾーンを定義するなんらかのルールに対応しています。ソフトウェアの設計時に、「PLT」、「Asia/Karachi」などの文字列を頻繁に利用する予定の場合は、文字列の代わりにこのドメイン・クラスを使用してください(例:ユーザーのタイムゾーンの設定を保存する場合)。
// 特定地域の日時を作成する場合はZoneIdを指定できます
ZoneId id = ZoneId.of("Europe/Paris");
ZonedDateTime zoned = ZonedDateTime.of(dateTime, id);
assertEquals(id, ZoneId.from(zoned));
リスト6
ZoneOffset:グリニッジ/UTCとタイムゾーンの差を表す期間。特定のZoneIdの特定の時点に解決できます(リスト7)。
ZoneOffset offset = ZoneOffset.of("+2:00");
リスト7
ZonedDateTime:完全修飾されたタイムゾーンの情報を含む日付/時間(リスト8)。任意の時点でオフセットを解決できます。経験からすると、特定のサーバーの環境に依存せずに日付と時間を表す場合は、ZonedDateTimeを使用する必要があります。
ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]");
リスト8
OffsetDateTime:解決済みのオフセットを含む日付/時間。データをシリアライズしてデータベースに保存する場合に便利です。また、複数のサーバーが異なるタイムゾーン内に設置されている場合に、タイムスタンプをロギングするためのシリアライズ形式として使用してください。
OffsetTime:解決済みオフセットを含む時間(リスト9)。
OffsetTime time = OffsetTime.now();
// タイムライン上の時点を変えずにオフセットを変更します
OffsetTime sameTimeDifferentOffset = time.withOffsetSameInstant(
offset);
// オフセットを変更し、タイムライン上の時点を更新します
OffsetTime changeTimeWithNewOffset = time.withOffsetSameLocal(
offset);
// 以前とは異なるフィールドを使用して新しいオブジェクトを作成することもできます
changeTimeWithNewOffset
.withHour(3)
.plusSeconds(2);
リスト9
Javaには、タイムゾーンを表すjava.util.TimeZoneというクラスがすでにありますが、このクラスはJava SE 8では使用しません。JSR 310のすべてのクラスは不変である一方、java.util.TimeZoneは可変であるからです。
Periodは、タイムライン上の距離のことで、「3か月と1日」のような値を表します。これまでに確認した他のクラスがタイムライン上の点を表すのとは対照的です(リスト10)。
// 3年と2か月と1日 Period period = Period.of(3, 2, 1); // 期間を使用して日付の値を変更できます LocalDate newDate = oldDate.plus(period); ZonedDateTime newDateTime = oldDateTime.minus(period); // PeriodのコンポーネントはChronoUnitの値で表現されます assertEquals(1, period.get(ChronoUnit.DAYS));
リスト10
Java SE 8では、開発者にとって安全性と機能性が大幅に向上した新しいDate and Time APIがjava.timeパッケージ内で提供される予定です。この新APIはドメインを明確にモデル化し、幅広い開発者のユースケースをモデル化した豊富なクラスで構成されます。
Durationは、時間の観点から計測したタイムライン上の距離のことで、Periodと目的はよく似ていますが、精度が異なります(リスト11)。
// 3秒と5ナノ秒の経過時間 Duration duration = Duration.ofSeconds(3, 5); Duration oneDay = Duration.between(today, yesterday);
Durationインスタンスに対しては、通常の加算(+)、減算(-)や「with」操作を実行できます。また、Durationを使用して日付や時間の値を変更することもできます。
ISOに準拠しない暦法を利用する開発者のニーズに対応するために、Java SE 8ではChronologyという概念が導入されます。この概念は、ある特定の暦法を表し、その暦法での各時点を作成するためのファクトリの役割を担います。時点を表すコア・クラスに対応するインタフェースが存在しますが、以下のクラスによってパラメータ化されています。
Chronology: ChronoLocalDate ChronoLocalDateTime ChronoZonedDateTime
これらのクラスは各地域の暦法を考慮する必要のある高度な国際化アプリケーションを扱う開発者向けに用意されたものです。そのような要件に対応する必要のない開発者は使用しないでください。月や週といった概念すらない暦法も存在します。そのような場合には、汎用的なフィールドのAPIを利用して計算を行う必要があります。
Learn More |
Java SE 8には、共通的なユースケースに対応するクラスが他にもあります。たとえば、MonthDayクラスはMonthとDayのペアで構成され、誕生日を表すのに便利です。また、YearMonthクラスは、クレジット・カードの利用開始年月や有効期限といったユースケース、あるいはユーザーが特定の日を指定せずに年月のみを入力するようなシナリオに対応します。
Java SE 8のJDBCではこれらの新しい型をサポートする予定ですが、パブリックなJDBC APIの変更は特にありません。既存の汎用的なsetObjectメソッドとgetObjectメソッドにより十分対応できる予定です。
これらの型は、ベンダー固有のデータベース型やANSI SQLの型にマッピングすることが可能です。たとえば、ANSIとのマッピングは表1のようになります。
| ANSI SQL | Java SE 8 |
| DATE | LocalDate |
| TIME | LocalTime |
| TIMESTAMP | LocalDateTime |
| TIME WITH TIMEZONE | OffsetTime |
| TIMESTAMP WITH TIMEZONE | OffsetDateTime |
Java SE 8では、開発者にとって安全性と機能性が大幅に向上した新しいDate and Time APIがjava.timeパッケージ内で提供される予定です。この新APIはドメインを明確にモデル化し、幅広い開発者のユースケースをモデル化した豊富なクラスで構成されます。
|
Ben Evans(@kittylyst):jClarityのCEOでありLondon Java Community(LJC)主催者。Java SE/EE Executive Committeeのメンバーでもある。 |
|
|
Richard Warburton:根深い技術的な問題を解決する経験主義の技術者。最近は、jClarityで高パフォーマンス演算のためのデータ分析に取り組んでいる。 |
