java.util.Date―その悲劇と歴史

(この記事は2011年6月11日に投稿したものの再掲です)

今回はJavaプログラマにはおなじみ、java.util.Dateについて考察します。

java.util.Dateは、Javaの初版から含まれているAPIで、これまでに使い勝手の悪さを散々叩かれながらも、いまだリプレイスされずに使われ続けている、とても残念なクラスです。このクラスに対する苦情を挙げていくと、思いつくだけでも、

  • 日付・時刻フィールドを直接設定できない(一応可能ではあるが非推奨である)。
  • 月フィールドが 0 から始まる(ただしget/setするメソッドは非推奨である)。
  • CalendarやTimeZoneなしでは時差を表現できない。
  • DateFormatがなければ(あっても?)まともに文字列表現も生成できない。
  • toString() の出力形式が最近主流のISO 8601形式でない。
  • 日時の加算や減算ができない(Calendarの使い勝手の悪いメソッドを使えば可能)。
  • JDK1.1の段階で大多数のコンストラクタとメソッドが非推奨になっている。
  • オブジェクトのフィールドを直接書き換えできるのが気持ち悪い[JSR-310の主張]。

などなど、他にもまだありそうです。では、なぜこんな問題児が生み出されたのでしょう? 使い勝手の悪いAPIをはじめから提供するなんて不自然な話ですね。

java.util.Dateは、JDK1.1の時に改訂されて以来、実はまだ大きな改訂がありません。Java SE 5.0のときに@Deprecatedアノテーションが導入されていますが、それもほんの些細なことです。現時点ではJSR-310のJava SE 8入りが予定されており、それが行われればJSR-310との相互運用性のためのメソッドが追加されますが、JDK1.1の時に比べれば小規模な改修にとどまります。先ほどからJDK1.1の時の改訂ばかり強調していますが、それがjava.util.Dateをほとんど書き換えてしまうほどの大改訂だったことが推測されるからです。

オリジナルのjava.util.Dateの実装については、残念ながらJDK1.0を入手することができないので断言できませんが、現状の内部表現がほぼCalendar(JDK1.1から追加)ベースになっていることから、相当の規模であったことが予想されます。

ずいぶん昔のJava Programmers FAQにもこのときの改訂状況が掲載されています(英文です)。

http://www.experts123.com/q/what-happen-to-java.util.date-between-jdk-1.0-and-jdk-1.1.html

http://www.newsville.com/cgi-bin/getfaq?file=news.answers/computer-lang/java/programmers/faq

実際のところ、JDK1.1におけるjava.util.Dateの大改訂は、Javaの国際化対応の一部として行われたものです(正式にはそういうことになっています)。日付と時刻の国際化で避けては通れないのが時差・タイムゾーンですが、オリジナルのjava.util.Dateにはそれを扱うことができませんでした。そこで導入されたのがCalendarやTimeZoneといったAPIです。これらは国際化対応を前提に設計されているばかりでなく、java.util.Dateの機能をほぼ包含しており(現在のjava.util.Dateの内部実装がCalendarなので当然ですが...)、従ってこの時点でjava.util.Dateを完全に置き換えてしまうことも、あるいはできたはずです。しかし、下された判断はjava.util.Dateを時間軸上の特定の日時を表すオブジェクトに機能特化させ、日時フィールド操作は一律非推奨とする、というものでした。

さて、オリジナルのjava.util.DateがそんなにひどいAPIだったのかというと、必ずしもそうとは言い切れません。オリジナルのjava.util.Dateの仕様を大雑把に言うと (1)内部表現はUTCベース、(2)入出力はローカルの日付・時刻ベースまたはUTC、(3)UTCとローカルの時差を取得可、というものです。機能としてはC/C++のライブラリ関数(date.h/cdate)と概ね同じ水準であり、Javaが設計された1990年代初めとしてはごく標準的でした。

java.util.DateとCのdate.hの対応表を作ってみました。これを見ても、だいたい同じような機能が実装されていることがわかります。

コンストラクタ
java.util.Date date.h
Date() time()
Date(int, int, int) mktime()
Date(int, int, int, int, int) mktime()
Date(int, int, int, int, int) mktime()
Date(long) time_t の代入

 

メソッド
java.util.Date date.h
after() difftime() 戻り値の比較 または time_t の比較
before() difftime() 戻り値の比較 または time_t の比較
clone() time_t の代入
compareTo() difftime() 戻り値の比較 または time_t の比較
equals() difftime() 戻り値の比較 または time_t の比較
getDate() / setDate() (struct tm) tm.tm_mday
getDay() / setDay() (struct tm) tm.tm_wday
getHours() / setHours() (struct tm) tm.tm_hour
getMinutes() / setMinutes() (struct tm) tm.tm_min
getSeconds() / setSeconds() (struct tm) tm.tm_sec
getTime() time_t の値
getTimeZoneOffset() time(), gmtime(), localtime() から導出
getYear() / setYear() (struct tm) tm.tm_year
hashCode() 該当なし
parse() 該当なし
toGMTString() gmtime() の戻り値を strftime() で変換
toLocaleString() localtime() の戻り値を strftime() で変換
toString() asctime() または ctime()
UTC() 該当なし

 

java.util.Dateには月が 0 から始まるという変な癖があります。これは C から由来するものです。というのも、tm.tm_month フィールドの定義は 1 月を 0 とするもので、Javaもそれを踏襲しています。

最後に1つ、コンピュータ上での日付と時刻の表現が最近規格化されたもので、java.util.Dateもその影響を受けていることをお話ししましょう。Date.toString() の出力形式はUnixやCに似た文字列で、最近よく見かけるISO 8601形式ではありません。Javaが設計された1990年代初め、普及していたのはISO 8601ではなくUnix形式あるいはその類似でした。Unixの日付表現は1970年代までには固まっていたはずで、Cの普及も手伝って1990年代では広く用いられていました。一方でISO 8601は規格化が1988年、インターネット標準化(RFC-3339)が2002年と、かなり新しい規格です。つまり、java.util.Dateが生まれたときにはISO 8601自体が無名の新規格で、それを選択肢とするには早計だったわけです。