Posted by & filed under 開発.


Java8のリリースが近づいています。Java8と言うとラムダ式のほうが有名ですが、多くの人がブログに書きそうなので、地味なDate/Time API(JSR-310)のほうを説明します。
ひとつだけラムダ式について言及しておくと、「ラムダ式は(関数型インターフェースの)オブジェクトを生成する」と説明している文章があったら、その文章は怪しいので疑いの目で読んでください。実際にはラムダ式はオブジェクト生成のコードにはならないからです(InvokeDynamicの呼び出しコードになります)。
後日つっこみを受けました。詳しくは別記事を参照してください。

さて、Date/Time API(JSR-310)の話です。
Javaの新しい日時処理(日付処理および時刻処理)のAPIです。結構、複雑です。過剰設計という批判もあるようです。自分自身、まだそこまでの判断はできません。自分が言えるのは、日時処理はそもそも本質的に複雑だという経験です。ざっと思いつくだけでも日時処理が複雑になる要因として次のような思いつきます。

  • そもそも一貫性がない(月ごとに日数が異なる)
  • 様々な基数(12進数、60進数、7進数?(曜日))
  • 何の法則もない祝日
  • 何の法則もない年号
  • 複雑さに輪をかけるタイムゾーン
  • 複雑さに輪をかけるうるう年
  • 複雑さに輪をかける夏時間

うんざりします。

Date/Time APIというフレームワークに載ってラクができるならそれに越したことはありません。

Date/Time APIの具体例の前に用語定義をします。

基本概念の用語定義

時刻 時間軸上のある瞬間の値。タイムゾーンと関係あり。タイムゾーン非依存の時刻をエポック値と定義(後述)
時間 時刻の差の値。タイムゾーンと無関係
日時 時刻を人間のために年月日時分秒で表現したもの
日付 時刻のうち年月日

可能な演算

時刻と時刻 減算(結果は時間)
時間と時間 加算と減算(結果は時間)
時刻と時間 加算と減算(結果は時刻)

コンピュータのための概念の用語定義

エポック値 エポック時刻(UTC1970年1月1日0時)からの経過時刻。タイムゾーンと無関係

人間のための概念の用語定義

UTC日時 時刻をUTCで表記した日時。エポック値から一意に決まるので相互変換可能
ローカル日時 時刻を特定のタイムゾーンで表記した日時。エポック値とタイムゾーンから決まる
ラベル日 タイムゾーンと無関係な日(例: 12月25日はクリスマス)。厳密には時刻ではないので時刻(UTC日時、ローカル日時)と演算してはいけない
期間 年月日時分秒で表記した時間(例: 1年、3ヶ月)
年号 西暦や和暦などの年表記
タイムゾーン 時差

Date/Time APIに話を戻します。
Date/Time APIでローカル日時を表現しようとすると、LocalDateTime、OffsetDateTime、ZonedDateTimeの3つのクラスの候補があります。どれを使えばいいか迷います。可能か否かで言うとどれでも可能だからです。
まず、LocalDateTimeを使う選択をした場合、別途、タイムゾーンを管理する必要があります。とは言え、普通、国際化したプログラムであれば、利用ユーザのプリファレンスなどでそのユーザのタイムゾーン設定があるはずです。そのタイムゾーン情報をユーザコンテキストで持っていれば、LocalDateTimeで充足します。
一方、OffsetDateTime、ZonedDateTimeのふたつは、それ自体がタイムゾーンを持ちます。OffsetDateTimeは、元々、SQLやXMLなどの外部処理用らしいので、それ限定で良いかと思います。
そうなると、LocalDateTimeとZonedDateTimeのどちらを使うかに話が集約されます。
個人的にはZonedDateTimeを推します。別途ユーザコンテキストなどに持つタイムゾーン情報と冗長管理になりますが、どうせタイムゾーンが一緒でないと意味のないローカル日時であれば一緒のクラスにするほうが安全だと思うからです。

java.timeパッケージのクラスの説明と利用指針

Instant 時刻。UTC日時に使える
Duration 時間
Period 期間
ZoneId タイムゾーン(名前ベース)
ZoneOffset タイムゾーン(値ベース)。あまり使う機会はないかも
LocalDate ラベル日として使える(年号取得やうるう年判定に使える)
LocalTime タイムゾーンを気にしない単なる時刻処理クラスとして使える
LocalDateTime 通常の日時に使うのは危険(タイムゾーンなしの時刻はバグの元)。ラベル時刻(全世界同時刻)のためには使える
ZonedDateTime タイムゾーンありの日時。通常の日時に使う
OffsetTime 簡易版タイムゾーンありの時刻。外部処理用(SQLやXML)
OffsetDateTime 簡易版タイムゾーンありの日時。外部処理用(SQLやXML)

その他パッケージのクラスの説明

TemporalAdjusters 強力な日付演算(月末、直近の日曜日など)ユーティリティ
JapaneseChronology 和暦

現在時刻エポック値の表現と取得方法

数値 System.currentTimeMillis()やSystem.nanoTime()
Instant Instant.now()

実装方針

時刻の扱い

  • データベース上はエポック値を格納(JDBCではjava.sql.Dateを経由。ミリ秒以下が欲しければjava.sql.Timestamp経由)
  • コード上はjava.time.ZonedDataTimeで扱う。ただしUTC日時とローカル日時の区別は開発者の責任

時間の扱い

  • データベース上は数値として格納(JDBCではintを経由)
  • コード上はjava.time.Durationもしくは数値で扱う

ラベル日の扱い

  • データベース上は数値(20140101のような数値)として格納(JDBCではStringもしくはintを経由。タイムゾーン非依存にするため)
  • コード上はjava.time.LocalDateで扱う

JDBCまわりの対応は未整備です。java.sql.ResultSetもjava.sql.PreparedStatementもDate/Time APIにまだ対応していません。Object型でカラム値を読み書きするメソッドは追加されていますが、気持ち悪い上に動くのか不明(JDBCドライバ次第)なので、現状は旧来のjava.util.Dateを経由して変換する必要がありそうです。

以下にサンプルコードを載せます。コメントの想定例を見ながら読んでください。


関連文書:

  • 関連文書は見つからんがな

Comments are closed.