Java8日付時刻APIの使いづらさと凄さ

いままでのJavaでは、日付時刻を扱おうとするとめんどくさい割に非常に低機能でした。
Java8では、新たに日付時刻APIが導入され、めんどくささが増しつつ非常に高機能になりました。
(以降、Java8で導入された日付時刻APIを単に「日付時刻API」と表します)

もちろん、慣れてきて、ちょっとしたサポートメソッドを用意してやれば、結構使いやすいのですが、それは「このAPIは使いやすい」という評価にはなりません。
つまり日付時刻APIは、慣れないとぜんぜんわからないし、サポートメソッドがないと面倒なコードが必要ということです。

いろいろあってよくわからない

日付時刻では、時点を扱うInstantや期間を扱うPeriod、時間量をあらわすDurationなど多くのクラス・インタフェースが導入されています。
これらは、IDEの補完でAPIを探りながら機能を推測すれば、それなりにドキュメントなしで使うことができます。
けど、基本の日付時刻をあらわすクラスが何通りかあって、この使い分けはカンではちょっと難しいです。

まとめるとこんな感じです。

クラス 用途 部分クラス
LocalDateTime 日付時刻をあらわす LocalDate/LocalTime
OffsetDateTime 時差つきの日付時刻をあらわす OffsetTime
ZonedDateTime タイムゾーンつきの日付時刻をあらわす ナシ
JapaneseDate 平成・昭和など日本の暦をあらわす JapaneseDateのみ

LocalDateTimeは、時差などの情報を持たない、単に何年何月何日何時何分何秒という情報を保持します。
OffsetDateTimeと、ZonedDateTimeの違いがわかりにくいです。
OffsetDateTimeは単純に標準時からの時差がついた日付時刻を扱います。そして、ZonedDateTimeは、タイムゾーンとして指定した地域での、夏時間などの補正も含めた日付時刻を扱います。
おそらく、アプリケーションではOffsetDateTimeを使うことはなく、ZonedDateTimeを使うことになると思います。
こういった使い分けは、なかなか浸透しないのではないかと思います。

ここで問題なのは、OffsetDateTimeかZonedDateTimeなど時差情報を持ったオブジェクトを渡すべきところに、LocalDateTimeのオブジェクトを渡してしまうと、時差情報がないという実行時エラーになるというところです。
部分的に動くからLocalDateTime使ってたけど統合しようとしたら例外が出て、あとからZonedDateTimeに置き換えるということが起きそうです。また、そういったことに気づかず地雷化する事例もあるかもしれません。

あと、日付時刻APIのオブジェクトは基本immutableで、オブジェクトの内容は変わらないのですが、このことで

public LocalDateTime add3Hours(LocalDateTime ldt){
  ldt.plusHours(3);//3時間すすめる
  return ldt;
}

のように、3時間すすめたように見えるけど、実際はなんもしてない、ってことでハマる人も多そうです。

旧来のDateとの相互変換が面倒

Javaはすでに10年以上も使われているので、旧来のjava.util.Dateを使う処理が多く書かれています。
そうすると、java.util.Dateと日付時刻APIでの相互変換を行う場面が多くなるはずです。
でも、日付時刻APIでは、

Date d = new Date();
LocalDateTime ldt = new LocalDateTime(d);

のように手軽に変換することはできません。

逆に

LocalDateTime ldt = LocalDateTime.now();
Date d = ldt.toDate();

のように一発変換のメソッドもありません。

とりあえず、そもそもDateと相互変換したいときには、LocalDateTimeではなく、OffsetDateTimeかZonedDateTimeを使うほうがいいです。OffsetDateTimeはあまりアプリケーションで使わないと思うので、Dateと相互変換する場合はZonedDateTimeを使うことになるでしょう。
Dateからの変換はこのようになります。

Date d = new Date();
ZonedDateTime zdt = ZonedDateTime.ofInstant(d.toInstant(), ZoneId.systemDefault());

めんどうですね。

ZonedDateTimeの場合は、次のようにも書けます。

Date d = new Date();
ZonedDateTime zdt = d.toInstant().atZone(ZoneId.systemDefault());

ちょっと楽です。
OffsetDateTimeの場合は、atZoneメソッドではなくてatOffsetメソッドですがZoneOffsetの取得が面倒です。

APIを見てると次のように書ける気がするかもしれませんが、爆死します。

Date d = new Date();
ZonedDateTime zdt = ZonedDateTime.from(d.toInstant());

たぶん、補完みながら初めて使う人は、一度は経験することでしょう。

逆の変換は次のようになります。

Date rd = Date.from(zdt.toInstant());

LocalDateTimeからDateへの変換は、なんか血をみます。

LocalDateTime ldt = LocalDateTime.now();
Date d = Date.from(ldt.toInstant(ZoneId.systemDefault().getRules().getOffset(ldt)));

ldtが2回でてくるあたり。

一度ZonedDateTimeに変換したほうがいいでしょう。

LocalDateTime ldt = LocalDateTime.now();
ZonedDateTime zdt = ldt.atZone(ZoneId.systemDefault());
Date rd = Date.from(zdt.toInstant());

こんな感じで、Dateとの相互変換には慣れが必要です。引数省略したらZoneId.systemDefault()を使うようになってればいいのに。

※ 2015/7/21 追記
DateからLocalDateTimeへの変換は次のように書けます。

Date d;
LocalDateTime ldt = LocalDateTime.ofInstant(d.toInstant(), ZoneId.systemDefault());

※ 2021/9/18 追記
LocalDateTimeからDateへの変換は、java.sql.Timestampを使って

Date d = java.sql.Timestamp.valueOf(ldt);

がいいかも。実際のオブジェクトの型はTimestampだけど。

逆もこんな感じにできる。

LocalDateTime ldt = new java.sql.Timestamp(d.getTime()).toLocalDateTime();

Java8 日付時刻APIのちょっとえらいところ

タイムゾーンと時差が別れていることから推測できるように、日付時刻APIでは各地域の夏時間などの情報をもっています。
で、タイムゾーンを扱うZoneIdには、固定時差かどうかを示すフラグがあるのですが、次のようにするとfalseが表示されました。

System.out.println(ZoneId.of("Asia/Tokyo").getRules().isFixedOffset());

日本には夏時間は導入されていないので、当然trueになるかと思ったんですが、falseがでてきて、「ふぁっ?」ってなりました。
で、そういえば戦後に夏時間がどうのという話があった気がすると思って、実際にどうなってるか表示してみました。

ZoneId.of("Asia/Tokyo").getRules().getTransitions().forEach(System.out::println);

そうすると次のようになりました。

Transition[Overlap at 1888-01-01T00:18:59+09:18:59 to +09:00]
Transition[Gap at 1948-05-02T02:00+09:00 to +10:00]
Transition[Overlap at 1948-09-11T02:00+10:00 to +09:00]
Transition[Gap at 1949-04-03T02:00+09:00 to +10:00]
Transition[Overlap at 1949-09-10T02:00+10:00 to +09:00]
Transition[Gap at 1950-05-07T02:00+09:00 to +10:00]
Transition[Overlap at 1950-09-09T02:00+10:00 to +09:00]
Transition[Gap at 1951-05-06T02:00+09:00 to +10:00]
Transition[Overlap at 1951-09-08T02:00+10:00 to +09:00]

ぐぐってみると、たしかに1948年〜1951年の間、日本で夏時間が実施されていたようです。
夏時間:日本におけるサマータイム - Wikipedia

なんかえらいなーと思いました。

tz databaseを使ってるんじゃないか、という指摘がありました。タイムゾーンを管理するデータベースです。
tz database - Wikipedia
こういうのがあるんですね。
ちゃんとこのような仕組みを標準APIとして持つのは(そしてメンテナンスしていくというのは)すごいなと思いました。