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を経由して変換する必要がありそうです。
以下にサンプルコードを載せます。コメントの想定例を見ながら読んでください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
import java.util.*; import java.time.*; import java.time.temporal.*; import java.time.chrono.*; import java.time.format.*; public class Time { public static void main(String... args) { // 共有コンテキスト(毎回作る必要はないので、アプリグローバルでもよい) ZoneId jst = ZoneId.of("JST", ZoneId.SHORT_IDS); // ZoneId jst = ZoneId.systemDefault(); // 手元コードならこれでもOK ZoneId est = ZoneId.of("EST", ZoneId.SHORT_IDS); Locale jpLocale = new Locale("ja", "JP", "JP"); Chronology wareki = Chronology.ofLocale(jpLocale); // Chronology wareki = Chronology.of("Japanese"); // 上記2行の代替例 // 入力数値からZonedDateTime(内部時刻) // 想定例: 利用者は日本タイムゾーンで日時を入力 ZonedDateTime myTime = ZonedDateTime.of(2014, 1, 31, 10/*hour*/, 0/*min*/, 0/*sec*/, 0/*nanosec*/, jst); System.out.println(myTime); // ZonedDateTime(内部時刻)から表示 // 想定例: 利用者は日本タイムゾーンでの日時表示を期待 System.out.format("%d, %d, %d\n", myTime.getYear(), myTime.getMonthValue(), myTime.getDayOfMonth()); // ZonedDateTimeを使う日付計算 // 単純な計算はplusMonthsやminusMonthsなどのメソッドを使う // 少し複雑な計算はTemporalAdjustersを使う // 例: 2014.1.31の1ヶ月後の最初の土曜日は? => 2014.3.1 ZonedDateTime deadline = myTime.plusMonths(1).with(TemporalAdjusters.next(DayOfWeek.SATURDAY)); // 時刻の比較 // 例: 締切り前かの判定など if (myTime.compareTo(deadline) < 0) { System.out.println("before deadline"); } // 日数計算 // 例: 締切りまで残り何日か? // 注意: 締切り日の仕様を明確にするのはコード以前の話(たとえば、1月1日締切りの意味は、当日0時なのか夜11:59つまり事実上翌日の0時なのか) Duration duration = Duration.between(myTime, deadline); System.out.format("%d days until deadline\n", duration.toDays()); // JST時刻からEST時刻に変換(Instantとしては同時刻) // 想定例: 日本のカレンダーで作った予定を米国のカレンダーで表示 ZonedDateTime ustime = myTime.withZoneSameInstant(est); System.out.println("US: " + ustime); // ZonedDateTime(内部時刻)から和暦出力 // 想定例: 利用者が和暦での日時表示を期待 ChronoLocalDate heisei = wareki.date(myTime); // "Heisei"=>"平成"の文字列変換は自前実装が必要 System.out.format("%s, %d, %d, %d\n", heisei.getEra(), heisei.get(ChronoField.YEAR_OF_ERA), heisei.get(ChronoField.MONTH_OF_YEAR), heisei.get(ChronoField.DAY_OF_MONTH)); // 和暦年号(H26)から西暦年号(2014年)へ変換 // 想定例: 利用者が和暦で日時を入力 String WAREKI_HEISEI = "Heisei"; // "Heisei"はハードコード JapaneseEra jpEra = JapaneseEra.valueOf(WAREKI_HEISEI); ChronoLocalDate heisei2 = wareki.date(jpEra, 26, 3, 15); System.out.println("A.D. " + heisei2.get(ChronoField.YEAR)); //=> 2014 // ラベル日 // 想定例: クリスマスの終日予定は、どのタイムゾーンのカレンダーで見てもずれない LocalDate xmasDay = LocalDate.of(2014, 12, 25); // 旧API(java.util.Date)との変換 // java.util.Date => Date/Time API Date now = new Date(); ZonedDateTime ztime = ZonedDateTime.ofInstant(now.toInstant(), jst); // Date/Time API => java.util.Date ZonedDateTime znow = ZonedDateTime.now(); Date dateNow = Date.from(Instant.from(znow)); } } |
最近のコメント