原文はこちら。
http://www.oracle.com/technetwork/articles/java/jf14-date-time-2125367.html
Why do we need a new date and time library?
Java開発者の長年の悩みの種に、通常の開発者のdateとtimeのユースケースに対するサポートが不十分な点があります。
例えば(
java.util.Date
のと
SimpleDateFormatter
などの)既存のクラスは、スレッドセーフではなく、ユーザーにとって同時実行の問題が発生し得ます。平均的な開発者が日付処理コードを記述する場合、この問題に対応できないでしょう。
日付と時間のクラスには、APIの設計が非常に貧弱なものがあります。例えば、
java.util.Date
の年(year)は、 1900年から始まりますが、月(month)は0から、そして日(days)は1から始まり、あまり直感的ではありません。こうした問題の結果、Joda-Timeのようなサードパーティの日付と時刻のライブラリの人気につながっています。
これらの問題に対処し、JDKのコアでよりよいサポートを提供するため、新しい日付と時間のAPIをJava SE 8で設計し、前述の問題を解消しています。
JSR 310の下、Joda-Timeの作者であるStephen ColebourneとOracleが共同でこのプロジェクトを主導しました。新しいJava SE 8のパッケージ
java.time
でご利用いただけます。
[訳注]
原文では月が1から、日が0からスタートとありますが、さすがにそれはどうよ、ということで修正して日本語にしています。
Core Ideas
この新しいAPIは3つの主要なアイデアから生まれています。
- Immutable-value classes(不変値クラス)
Javaの既存のフォーマッタの重大な弱点の一つとして、スレッドセーフでない点があります。これは、スレッドセーフな方法で利用したり、日付処理コードの日々の開発で並行性の問題を検討する必要があり、開発者にとって負担になります。新しいAPIでは、すべてのコアクラスは不変で、明確に定義された値を表すようにすることでこの問題を回避します。
- Domain-driven design(ドメイン駆動設計)
新しいAPIはDate
とTime
の様々なユースケースを詳細に表すクラスを使い、そのドメインを非常に正確にモデル化しており、その点で、以前の非常に劣っていたJavaライブラリとは異なっています。例えば、java.util.Date
はタイムラインのインスタンス(UNIXエポックからの経過時間(ミリ秒)のラッパー)を表しますが、toString()
メソッドを呼び出すと、タイムゾーンを含んだ結果を返すため、開発者を混乱させてしまいます。
ドメイン駆動設計を重視したことで、明快さと分かりやすさという長期的なメリットを提供していますが、以前のAPIからJava SE 8に移植する場合、アプリケーションの日付のドメインモデルの検討が必要になるかもしれません。
- Separation of chronologies(年代・年号の分離)
この新しいAPIを使うと、様々なカレンダーシステム(必ずしもISO-8601に従う必要はありません)を使い、世界のいろいろな地域、例えば、日本やタイの人々のニーズをサポートすることができます。標準的な年代だけ扱う必要のあるほとんどの開発者に追加の負担をかけずに、こうしたことが実現できます。
LocalDate and LocalTime
この新しいAPIを利用する際におそらく最初に遭遇するクラスは、
LocalDate
と
LocalTime
でしょう。これらは卓上カレンダーや壁掛け時計といったObserverのコンテキストから日付や時間を表すという意味で、ローカルです。また、
LocalDateTime
という複合クラスもあります。これは
LocalDate
と
LocalTime
の組み合わせです。
タイムゾーンは様々なObserverのコンテキストを明確にしますが、これはここでは除外しています。つまり、コンテキストを必要としない場合にはこれらのローカルクラスを使うべきです。デスクトップJavaFXアプリケーションもそうしたローカルクラスを使う対象の一つでしょう。これらのクラスを使って一貫性のあるタイムゾーンをもつ分散システムで時間を表現することが可能です。
Creating Objects
新しいAPIのコアクラスはすべてファクトリメソッドによって構築されています。その構成フィールドを使って値を構築する場合は、ファクトリは
of
と呼ばれます。別の型から変換する際には、ファクトリは
from
と呼ばれます。パラメータとして文字列を受け取る解析(parse)メソッドもあります(リスト1)。
リスト1
LocalDateTime timePoint = LocalDateTime.now(); // The current date and time
LocalDate.of(2012, Month.DECEMBER, 12); // from values
LocalDate.ofEpochDay(150); // middle of 1970
LocalTime.of(17, 18); // the train I took home today
LocalTime.parse("10:15:30"); // From a String
標準的なJavaのgetter規則を使ってJava SE 8クラスから値を取得しています(リスト2)。
リスト2
LocalDate theDate = timePoint.toLocalDate();
Month month = timePoint.getMonth();
int day = timePoint.getDayOfMonth();
timePoint.getSecond();
オブジェクトの値を変更して計算を実行することもできます。すべてのコアクラスは新しいAPIで不変ゆえ、これらのメソッドは
with
と呼ばれ新しいオブジェクトを返します。setterを使用する必要はありません(リスト3)。別のフィールドに基づいて計算するためのメソッドもあります。
リスト3
// Set the value, returning a new object
LocalDateTime thePast = timePoint.withDayOfMonth(10).withYear(2010);
/* You can use direct manipulation methods,
or pass a value and field pair */
LocalDateTime yetAnother = thePast.plusWeeks(3).plus(3, ChronoUnit.WEEKS);
新しいAPIには、アジャスター(adjuster)という概念も持ち合わせています。これは共通の処理ロジックをラップするためのコードブロックです。一つ以上のフィールドを設定するために利用する
WithAdjuster
を作成したり、フィールドを加算・減算するために利用する
PlusAdjuster
を作成したりすることができます。値クラスはadjusterとしても振る舞います。その場合、値クラスが表すフィールドの値を更新します。新しいAPIは組み込みadjusterを定義していますが、再利用したい特定のビジネスロジックを持っているならば、独自のadjusterを書くことができます(リスト4)。
リスト4
import static java.time.temporal.TemporalAdjusters.*;
LocalDateTime timePoint = ...
foo = timePoint.with(lastDayOfMonth());
bar = timePoint.with(previousOrSame(ChronoUnit.WEDNESDAY));
// Using value classes as adjusters
timePoint.with(LocalTime.now());
Truncation
新しいAPIは、日付、時刻、時刻と日付を表すための型を提供することで、さまざまな精度の時点をサポートしていますが、当然ながら、こうした型よりもきめ細かい、精度という概念があります。
truncatedTo
メソッドは、こうしたユースケースをサポートするために存在します。これを使うと、フィールドに合わせて値を切り捨てることができます(リスト5)。
リスト5
LocalTime truncatedTime = time.truncatedTo(ChronoUnit.SECONDS);
Time Zones
先ほど見てきたローカルクラスはタイムゾーンが導入した複雑性を取り除いています。タイムゾーンとは、標準時が同じ地域に対応する一連のルールセットであり、およそ40個あります。タイムゾーンは、協定世界時(UTC)からのオフセットによって定義されています。タイムゾーンはほぼ同期していますが、指定された差分だけ移動します。
タイムゾーンを2種類の識別子で参照することができます。例えば、省略した表記だと"PLT"、長い表記では"Asia/Karachi"といった感じです。アプリケーションを設計する際には、タイムゾーン利用においてどんなシナリオが適切か、どのオフセットが適切かを検討する必要があります。
ZoneId
は地域を表す識別子です(リスト6)。各ZoneId
が、その場所のタイムゾーンを定義するルールに対応しています。ソフトウェア設計時に"PLT"や “Asia/Karachi"の文字列の利用を検討している場合は、代わりにこのドメインクラスを使用する必要があります。ユースケースの例としては、ユーザーのタイムゾーンをユーザ・プリファレンスとして保存することになるでしょう。
リスト6
// You can specify the zone id when creating a zoned date time
ZoneId id = ZoneId.of("Europe/Paris");
ZonedDateTime zoned = ZonedDateTime.of(dateTime, id);
assertEquals(id, ZoneId.from(zoned));
ZoneOffset
はグリニッジ/UTCとタイムゾーンの差を表現する期間です。特定の時点での特定のZoneId
に対し、ZoneOffsetを解決することができます(リスト7)。
リスト7
ZoneOffset offset = ZoneOffset.of("+2:00");
Time Zone Classes
ZonedDateTime
は完全就職されたタイムゾーンを伴う日付と時間です(リスト8)。これは任意の時点でのオフセットを解決することができます。経験則では、特定のサーバーのコンテキストに依存せずに日付や時間を表現したい場合には、ZonedDateTime
を使うべきです。
リスト8
ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]");
OffsetDateTime
は解決されたオフセットを伴う日付と時間です。これはデータをシリアル化してデータベースに入れる際に有用です。様々なタイムゾーンにサーバーがある場合、タイムスタンプをログ出力するためにシリアル化形式として利用するべきでしょう。
OffsetTime
は解決されたオフセットを伴う時間です(リスト9)。
リスト9
OffsetTime time = OffsetTime.now();
// changes offset, while keeping the same point on the timeline
OffsetTime sameTimeDifferentOffset = time.withOffsetSameInstant(offset);
// changes the offset, and updates the point on the timeline
OffsetTime changeTimeWithNewOffset = time.withOffsetSameLocal(offset);
// Can also create new object with altered fields as before
changeTimeWithNewOffset.withHour(3).plusSeconds(2);
Javaには既にタイムゾーンのクラス(
java.util.TimeZone
)がありますが、Java SE 8では利用しません。それは、すべてのJSR 310クラスは不変であり、タイムゾーンは変更可能であるからです。
Periods
Period
は、タイムライン上の距離である、「3ヶ月と1日」のような値を表します。これは、これまで見てきたタイムライン上の点である他のクラスとは対照的です(リスト10)。
リスト10
// 3 years, 2 months, 1 day
Period period = Period.of(3, 2, 1);
// You can modify the values of dates using periods
LocalDate newDate = oldDate.plus(period);
ZonedDateTime newDateTime = oldDateTime.minus(period);
// Components of a Period are represented by ChronoUnit values
assertEquals(1, period.get(ChronoUnit.DAYS));
Durations
Duration
は、時間の観点で測定した、タイムライン上の距離であり、
Period
と同様の目的を果たしますが、異なる精度を持ちます(リスト11)。
リスト11
// A duration of 3 seconds and 5 nanoseconds
Duration duration = Duration.ofSeconds(3, 5);
Duration oneDay = Duration.between(today, yesterday);
Duration
インスタンス上で、通常のプラス、マイナスや、"with"オペレーションを実行することが可能ですし、また、
Duration
インスタンスを使い、日付や時間の値を変更することも可能です。
Chronologies
非ISOカレンダーシステムを使う開発者のニーズをサポートするため、Java SE 8では
Chronology
という概念を導入します。これはカレンダーシステムを表現し、カレンダーシステム内の時点のためのファクトリとして振る舞います。また、コアの時点クラスに対応するインタフェースもありますが、以下のクラスがパラメータ化します。
Chronology:
ChronoLocalDate
ChronoLocalDateTime
ChronoZonedDateTime
これらのクラスは、ローカルカレンダーシステムを考慮する必要のある、高度に国際化されたアプリケーションを開発している開発者のためだけに存在しているので、こうした要件を考慮する必要のない開発者が使うべきではありません。いくつかのカレンダーシステムには、月や週の概念がないものがあり、まさに汎用的なフィールドのAPIを使って計算をする必要があります。
The Rest of the API
Java SE 8には他のよく使われるユースケースに対応するクラスもあります。
MonthDay
クラスは
Month
と
Day
の組み合わせを保持するため、誕生日を表すのに有用です。The
YearMonth
クラスはクレジットカードの開始日と失効日といったユースケースや、日を気にしない日付がらみのシナリオに対応します。
Java SE 8のJDBCはこれらの新しい型をサポートしますが、公式なJDBC APIの変更はありません。既存の汎用setObjectとgetObjectメソッドで十分でしょう。
これらの型はベンダー固有のデータベース型またはANSI SQL型にマッピングできます。ANSI SQL型とのマッピングは表1のようになります。
ANSI SQL | Java SE 8 |
DATE | LocalDate |
TIME | LocalTime |
TIMESTAMP | LocalDateTime |
TIME WITH TIMEZONE | OffsetTime |
TIMESTAMP WITH TIMEZONE | OffsetDateTime |
表1
Conclusion
Java SE 8は、
java.time
パッケージで新しい日付と時間のAPIを提供します。このAPIは開発者にとっての安全性と機能性がすばらしく向上しています。この新しいAPIはドメインをよくモデリングしており、幅広い開発者のユースケースをモデリングするためのクラスが豊富に揃っています。
Learn More
JSR 310: Date and Time API
http://jcp.org/en/jsr/detail?id=310
著者について
Ben Evans (
@kittylyst) はjClarityのCEOであり、London Java Community (LJC)のまとめ役にして、Java SE/EE Executive Committeeのメンバーでもあります。
Richard Warburton は経験主義の技術者で、技術的課題を深掘りして解決していきます。直近では、jClarityで高性能コンピューティングのためのデータ分析に取り組んでいます。