joda-timeでの安全な時間処理(1) -導入編-

joda-timeとは?

joda-timeというJavaで時間を扱うためのライブラリがあり、ここ数年注目されています。また、Java8からはjoda-timeのAPIの影響を受けたJavaには新しいDate and Time API(JSR-310)が提供されます。これまでJavaの日付処理の定番といえば、java.util.Dateやjava.util.Calendarでしたがこれらにはどのような問題があったのでしょうか。

java.util.Date, java.util.Calendarの問題点

Javaでの時間処理といえば、java.util.Dateやjava.util.Calendarでの実装が思い浮かぶと思いますが、これらのAPIは次の大きな問題を抱えています。

インスタンスが変更可能(mutable)である

次のようなコードがあるとしましょう。

DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
Calendar now = Calendar.getInstance();
System.out.println("now = " + df.format(now.getTime()));

現在時間を取得するコードですね。まあここまでは良いでしょう。
さて、nowをベースとして1時間後の時間が取得したくなりました。そうなると次のようなコードになりますね。

DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
Calendar now = Calendar.getInstance();
System.out.println("now = " + df.format(now.getTime()));
		
// 1時間後
now.add(Calendar.HOUR_OF_DAY, 1);
System.out.println("now(add 1hour) = " + df.format(now.getTime()));

では、これを実行してみましょう。

now = 2013-07-14 23:16:39:670
now(add 1hour) = 2013-07-15 00:16:39:670

上がgetInstanceして取得したタイミングの時間、下がその時間に1時間が足されたものです。純粋に目的が達成されて良かった!とはなりません。このコードは潜在的に以下の問題点を抱えています。

  • Calendar#getInstanceして取得されたインスタンスは、addのような変更メソッドによって保持する時間を変えられてしまう。
  • 1時間後を取得という目的を果たすことができるが、getInstanceした時間としての役目を果たすことはできなくなる。

この問題点を解決するために、たまに用いられてしまうアンチパターンが以下の手法です。

DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
Calendar now = Calendar.getInstance();
System.out.println("now = " + df.format(now.getTime()));
		
// 1時間後
Calendar now2 = ((Calendar) now.clone());
now2.add(Calendar.HOUR_OF_DAY, 1);
System.out.println("now = " + df.format(now.getTime()));
System.out.println("now2 = " + df.format(now2.getTime()));

CalendarがClonenableである特性を利用したコードですね。結果は以下のとおり。

now = 2013-07-14 23:19:31:960
now = 2013-07-14 23:19:31:960
now2 = 2013-07-15 00:19:31:960

たしかにnow自身はそのままの時間を保ちながら、1時間後を取得するという目的は果たしています。しかし、java.util.Calendarのaddのような変更メソッドにより中身を変えられてしまう脆さがあるには変わらないので、今後の修正によってこのコードが今後も最初の目的を果たしうるコードであり続けられるか?という保証はできません。性善説の上だけで成立するものです。

mutableであるがゆえの限界

mutableな構造であるjava.util.Date、java.util.Calendarは以下のような問題を持っています。

  • スレッドセーフではない
  • mutableであるため、時間のインスタンスの再利用によるスパゲッティコードになりやすい。(cloneすれば良いという問題でもない)
  • メソッドの引数としてとる場合、使用者側にとって意図しない副作用が発生しないような実装にしなければならない。(※これについては、例えばjava.util.Calendarをfinalにすれば良いという問題ではない。finalにすればインスタンスの差し替えは防御できるが、Calendarの保持する値を防御できるわけではない。)

きれいに安全な時間処理を行うためには、やはりimmutableな時間処理APIが必要です。そこで登場したのがjoda-timeです。さっそく使ってみましょう。

導入する

mavenのセントラルリポジトリに登録されているjoda-timeの現状のバージョンは2.2です。mavenであればpom.xmlに以下のように依存関係を追加します。

  <dependencies>
  (中略)
  	<dependency>
  	  <groupId>joda-time</groupId>
  	  <artifactId>joda-time</artifactId>
  	  <version>2.2</version>
  	</dependency>
  </dependencies>

ちなみにjoda-timeは依存関係を持たないライブラリなので、他のライブラリとの干渉の心配も無いです。

joda-timeで簡単な時間処理をする

ではjoda-timeを使って先ほどのコードを書き換えてみましょう。joda-timeではorg.joda.time.DateTimeで時間を表現します。

DateTimeFormatter df = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss:SSS");
DateTime now = DateTime.now();
System.out.println("now = " + df.print(now));
		
// 1時間後
DateTime nowAfterOneHour = now.plusHours(1);
System.out.println("now = " + df.print(now));
System.out.println("nowAfterOneHour = " + df.print(nowAfterOneHour));

実行結果は以下のとおり。

now = 2013-07-15 00:20:18:158
now = 2013-07-15 00:20:18:158
nowAfterOneHour = 2013-07-15 01:20:18:158

DateTime#nowで現在時間を取得、DateTime#plusHourで対象のDateTimeから任意の時間を追加した新たなDateTImeのインスタンスが作られます。
DateTimeはimmutableであるため、plusHourのあともnowした時間を保持し続けます。nowに対してplusだろうがminusだろうがしてもnowの内容は保証されるわけです。
java.util.Dateやjava.util.Calendarの問題点をシンプルなコードで解決できます。
また、org.joda.time.DateTimeは例のplusHourはもちろん、plusMinutesやminusDays等の直感的に利用しやすいメソッドがひと通り揃っています。この点もCalendarより優れていますね。

まとめ

さわりですがjoda-timeの良さと、Calendarのヤバさがわかって頂けたかと思いますw。
Calendarでの時間処理がデフォルトとなってるプロジェクトはjoda-timeへの移行を検討してみましょう!
次回はjoda-timeの基本的な時間処理にもう少し突っ込んだエントリを書きます。(まだまだつづく)