Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

続・今日から始めるJava8 - JSR-310 Date and Time API

こんにちは id:cero-t です。
前回話題にしたJava8 ですが、もう少し別の機能を試してみましょう。
今回は新しい日時APIDate and Time API(JSR-310) を紹介します。


JDK8 M6時点でのJavadocは、以下のURLで公開されています。
http://cr.openjdk.java.net/~rriggs/threeten/threeten-javadoc-b75/


注意:このエントリーの内容は、JDK8 M6 (b75) の時点のAPIに従っています。
M7以降では一部のAPIが変わっているので注意してください。GA時点でまた記事を見直します。


From Joda Time to Three Ten

Javaの日付クラス(java.util.Date)の扱いは、
皆さんもなかなか苦慮していることだと思います。
というのも、、、

・年月日などを指定したインスタンス生成ができない。
・計算をするために、Calendarクラスを使う必要がある。
・月が0オリジンだから、たとえば8月は「7」と表現する。
・SimpleDateFormatがスレッドセーフでない。
・日付だけ扱いたいのに、勝手に時間まで指定されて困る。あるいはその逆。
・たまにjava.sql.Dateと間違う。


このような問題に対して、もっと使いやすい日時APIとして
Joda TimeというOSSのライブラリが開発されました。
http://joda-time.sourceforge.net/

そしてJava8には、Joda Timeから発想を得た
新しいDate and Time API(JSR-310)が入ることになりました。


ちなみに、OpenJDKに入っているJSR-310の参照実装は
「ThreeTen」というコードネームになっています。

アクロバット系競技に興味のある皆様におかれましては
360はThreeSixty、540はFiveFortyなど聞きなれていることと思いますが、
それと同じということですね。

カッチョイイですね。

日付、時間、日時が別クラスに

JSR-310では 日付・時間・日時を扱うクラス がそれぞれ分かれています。
それが「LocalDate」「LocalTime」「LocalDateTime」の3つです。

具体的なコードで見てみましょう。

// 日付
LocalDate date = LocalDate.now();
System.out.println(date);

// 時間
LocalTime time = LocalTime.now();
System.out.println(time);

// 日時
LocalDateTime dateTime = LocalDateTime.now();
System.out.println(dateTime);

出力結果

2013-02-02
21:42:35.126
2013-02-02T21:42:35.126

ご覧の通り、日付のみ、時間のみ、日付と時間の両方の3つに分かれました。

また、toStringした結果がISO 8601形式で読みやすくなっています。
java.util.Dateの時は、toStringしても「Sat Feb 02 21:43:41 JST 2013」という
ちょっと読みにくい形式でした。


また、それぞれのクラスが持つメソッドも異なっており
LocalDateなら「getYear」「getMonth」「getDayOfMonth」
LocalTimeなら「getHour」「getMinute」「getSecond」「getNano」
LocalDateTimeなら、これら全てが使えるようになっています。

APIレベルでこのような制限が掛かっているので、使い間違いがありませんね。

インスタンスの作り方

これまでDateやCalendarのインスタンスを作る場合、
「現在の日時」か「1900/01/01 00:00:00」でのインスタンスを作るか、
DateFormat#parseを使ってインスタンスを作る必要がありました。

java.util.Dateには、年月日を指定してインスタンスを作るAPIもありましたが
現在ではdeprecated扱いになっています。


いっぽうLocalDateTimeでは、3つの作り方があります。
 1. 現在の日時
 2. 年月日時分秒を指定
 3. 日付文字列(ISO 8601形式)を指定

実際のコードで紹介しましょう。

// 現在の日時
System.out.println(LocalDateTime.now());

// 年月日などを指定。秒未満は省略可
System.out.println(LocalDateTime.of(2012, Month.FEBRUARY, 3, 21, 30, 15));
System.out.println(LocalDateTime.of(2012, 2, 3, 21, 30, 15, 123));

// 文字列を指定。秒未満は省略可
System.out.println(LocalDateTime.parse("2012-02-03T21:30:15.123"));

実行結果

2013-02-03T17:06:57.004
2012-02-03T21:30:15
2012-02-03T21:30:15.000000123
2012-02-03T21:30:15.123

月の指定で「2」を使えるところがいい ですね。
これまでは0オリジンなので、2月なら「1」を指定する必要がありました。

また、SimpleDateFormat的なクラスを使わなくとも
文字列から作れるようになったのも便利ですね。

演算も簡単に

これまで面倒だった日時の演算も、JSR-310では簡単になりました。
Date型は演算ができず、Calendarの時は引数にフィールドを指定する必要がありましたが
LocalDateTimeでは、plusDays、minusHoursといったAPIを利用して演算ができるようになりました。

具体的なコードは、このようになります。

// 2012/02/03 21:30:15
LocalDateTime dateTime = LocalDateTime.of(2012, 2, 3, 21, 30, 15);

// 3日後
System.out.println(dateTime.plusDays(3));

// 100日前
System.out.println(dateTime.minusDays(100));

// 30分前
System.out.println(dateTime.minusSeconds(30));

// 元のインスタンスの値は・・・?
System.out.println(dateTime);

実行結果

2012-02-06T21:30:15
2011-10-26T21:30:15
2012-02-03T21:29:45
2012-02-03T21:30:15

LocalDateTimeクラスは imutable なので演算を行った後でも
最初に作ったdateTimeインスタンスの日時が変化していないところにも注目です。
(ちなみに スレッドセーフ性 も保証されています)

また、LocalDateTimeは「日付」「時間」の演算のいずれも可能ですが、
LocalDateなら「日付」のみ、LocalTimeなら「時間」のみ演算できるように
メソッドが用意されているため、これまた使い間違いがありません。

文字列変換はDateTimeFormatter

Dateクラスは、DateFormat(SimpleDateFormat)クラスで変換を行いましたが
JSR-310の日付クラスは、DateTimeFormatterクラスで変換を行います。

DateTimeFormatterはJavadocにも "This class is immutable and thread-safe" と明記されており
スレッドセーフが保証されているところがいいですね。
SimpleDateFormatがスレッドセーフでないと知らず、事故った 方もいることでしょう。


さて、このDateTimeFormatterクラスは、3つの作り方があります。
 1. DateTimeFormattersから選んで作る
 2. DateTimeFormattersから文字列で作る
 3. DateTimeFormatterBuilderできちんと作る


まずは1つめ、数は多くありませんが
ISO形式のフォーマットをいくつか選択することができます。

LocalDateTime dateTime = LocalDateTime.of(2012, 2, 3, 21, 30, 15, 123);

System.out.println(dateTime.toString(DateTimeFormatters.isoDateTime()));
System.out.println(dateTime.toString(DateTimeFormatters.isoDate()));
System.out.println(dateTime.toString(DateTimeFormatters.isoTime()));
System.out.println(dateTime.toString(DateTimeFormatters.isoWeekDate()));

実行結果

2012-02-03T21:30:15.000000123
2012-02-03
21:30:15.000000123
2012-W05-5

システムによってはこれで事足りることもあるでしょう。


しかし日本では、日付の区切りに - ではなく / を使うことのほうが多いですよね。
そういう場合、フォーマット指定を行うことができます。

LocalDateTime dateTime = LocalDateTime.of(2012, 2, 3, 21, 30, 15, 123);
System.out.println(dateTime.toString(DateTimeFormatters.pattern("yyyy/MM/dd HH:mm:ss")));

実行結果

2012/02/03 21:30:15

この方法が一番よく使われそうですね。


最後に、DateTimeFormatterBuilderの使い方も見ておきましょう。

LocalDateTime dateTime = LocalDateTime.of(2012, 2, 3, 21, 30, 15, 123);
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
        .appendValue(ChronoField.YEAR)
        .appendLiteral("/")
        .appendValue(ChronoField.MONTH_OF_YEAR)
        .appendLiteral("/")
        .appendValue(ChronoField.DAY_OF_MONTH)
        .appendLiteral(" ")
        .appendValue(ChronoField.HOUR_OF_DAY)
        .appendLiteral(":")
        .appendValue(ChronoField.MINUTE_OF_HOUR)
        .appendLiteral(":")
        .appendValue(ChronoField.SECOND_OF_MINUTE)
        .toFormatter();

System.out.println(dateTime.toString(formatter));

実行結果

2012/02/03 21:30:15

記述が冗長になってしまうので、私はあまり好きではないのですが
間違いにくさという観点では、良いのでしょうかね?

誤ったFormatterを使うとどうなるの?

上の例では、LocalDateTimeに対するフォーマットを行ったので特に問題ないのですが、
たとえば「日付」に対して「時間」のフォーマットを行ったり、
「時間」に対して「日時」のフォーマットを行うと、情報が足りないわけですが、
その場合はどうなるのでしょうか。

早速試してみましょう。

DateTimeFormatter formatter = DateTimeFormatters.pattern("yyyy/MM/dd HH:mm:ss");
LocalTime.now().toString(formatter);

実行結果

Exception in thread "main" java.time.DateTimeException: Unsupported field: Year
at java.time.LocalTime.get0(LocalTime.java:670)
at java.time.LocalTime.getLong(LocalTime.java:647)
at java.time.format.DateTimePrintContext.getValue(DateTimePrintContext.java:261)
at java.time.format.DateTimeFormatterBuilder$NumberPrinterParser.print(DateTimeFormatterBuilder.java:1904)
at java.time.format.DateTimeFormatterBuilder$CompositePrinterParser.print(DateTimeFormatterBuilder.java:1567)
at java.time.format.DateTimeFormatter.printTo(DateTimeFormatter.java:335)
at java.time.format.DateTimeFormatter.print(DateTimeFormatter.java:307)
at java.time.LocalTime.toString(LocalTime.java:1540)
at DateTimeTest.main(DateTimeTest.java:17)
...

フォーマットに失敗して、DateTimeException が発生しました。
勝手に0000などで埋められるよりは、きっぱりと例外が発生したほうが
問題を検出しやすいので良いですね。

ちなみにフォーマットではなく、パースに失敗した時は、どうなるんでしょうか?

LocalTime.parse("2013-02-12");

実行結果

Exception in thread "main" java.time.format.DateTimeParseException: Text '2013-02-12' could not be parsed at index 2
at java.time.format.DateTimeFormatter.parseToBuilder(DateTimeFormatter.java:469)
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:372)
at java.time.LocalTime.parse(LocalTime.java:475)
at java.time.LocalTime.parse(LocalTime.java:460)
at Main.main(Main.java:15)
...

DateTimeParseException が発生しました。
実行時例外になっているのも、イマドキな感じで好感触ですね。

DBとの連携は?

最後に、気になるDBとの連携について。

Java8でアップデートされるJDBC 4.2では、
JSR-310の日付型などを扱えるようにするためのAPIが追加されました。

Javadocを見ると、setObjectメソッドが追加されていますね。
http://download.java.net/jdk8/docs/api/java/sql/PreparedStatement.html

setObjectだけでなく、setDate/setTime/setDateTimeにも
オーバーロードメソッドを追加してくれれば良かったのですが、
なぜ追加されなかったのでしょうかね? 余計に混乱を生むため?


ちなみにsetObjectメソッドはdefaultで実装されているため、
過去に独自PreparedStatementクラスを作っていた場合でも、
特にメソッド追加することなく流用することができますね。

Java5からJava6に上がった時は、
ConnectionやPreparedStatementインタフェースにメソッドが追加されたため、
過去に作った独自Connectionをうまく流用できないなど問題になった記憶がありますが、
Java8からはdefaultのおかげで、そういう問題を抑えることができそうです。

最後に

まずは今回のまとめです。

・Java8では日付、時間、日時が分かれたぞ!
・日付、時間、日時でそれぞれ習得、演算メソッドが分かれているので安全だぞ!
・日付系クラスも、DateTimeFormatterも、immutableでスレッドセーフだぞ!
・フォーマットは、DateTimeFormatters#patternメソッドから始めよう!
JDBC周りは、まぁまぁ。


また、今回のエントリーでは触れませんでしたが、
JSR-310ではタイムゾーンももちろん扱えますし、
期間を示す「Period」クラスや、和暦などの暦を扱う「Chronology」など
java.util.Dateになかった要素が取り込まれたり、使いづらかった点が改善されています。

そのせいでクラス数が多くなり、複雑になった面も否めませんが
日付が扱いやすくなったのは間違いないでしょうね。


それでは、またいずれ!