java.sql.Timestampのequalsが規約を満たしてない

事の発端

とある社内ライブラリを修正していたら、java.util.Date同士の比較で、getTimeを呼び出さないとtrueにならなくてエラー、というコードを発見した。こんな感じ

date1.getTime == date2.getTime // => true
date1 == date2 // => false

Scalaの==はJavaのequalsメソッドを呼び出す仕様なので、参照を比較しているということはない。

ところがjava.util.Dateのドキュメントを読むと、「getTimeでミリ秒を比較して同じならtrueを返す」って書いてある。

http://docs.oracle.com/javase/8/docs/api/java/util/Date.html#equals-java.lang.Object-

この辺りでTwitterに色々書いて、色んな意見を聞くうちに、筋が悪いjava.util.Dateを継承した何かが関わってるのでは、という仮説を立てる。

java.sql.Timestampだった

デバッガで調べてみると、date1が実体はjava.sql.Timestampだったということが分かる。標準ライブラリだけどひょっとして…と思ってequals実装を見てみるとどうやら元凶はこやつだと分かる。

http://grepcode.com/file_/repository.grepcode.com/java/root/jdk/openjdk/8-b132/java/sql/Timestamp.java/?v=source

ここからequals実装を抜き出したのが以下

    public boolean equals(Timestamp ts) {
        if (super.equals(ts)) {
            if  (nanos == ts.nanos) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    public boolean equals(java.lang.Object ts) {
      if (ts instanceof Timestamp) {
        return this.equals((Timestamp)ts);
      } else {
        return false;
      }
    }

あっこれequalsの規約にある「対称性」満たしてないやつや!

equalsの規約

equalsとか==の実装は、色々な所で使われるために、直感的におかしな挙動をしないようにいくつか規約があり(詳細はEffective Javaを読むかググるのが良いと思う)、その1つに対称性を満たしてないといけない、ってのがある。つまりこういう

s1 == s2がtrueのとき、s2 == s1がtrueを返す

何を当たり前なことを、って言われそうだが、Timestampの実装は満たしていない。以下に反例を挙げる

new java.util.Date(0L) == new java.sql.Timestamp(0L) // => true
new java.sql.Timestamp(0L) == new java.util.Date(0L) // => false

私見では、そもそも継承すること自体が間違いだったと思われる(実際ドキュメントで実装継承のつもりだったとか言ってる。アホか)

結論

Javaの標準ライブラリはクソ(再確認)