こんにちは id:cero-t です。
前回話題にしたJava8 ですが、もう少し別の機能を試してみましょう。
今回は新しい日時API、Date 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になかった要素が取り込まれたり、使いづらかった点が改善されています。
そのせいでクラス数が多くなり、複雑になった面も否めませんが
日付が扱いやすくなったのは間違いないでしょうね。
それでは、またいずれ!