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実装を見てみるとどうやら元凶はこやつだと分かる。
ここから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の標準ライブラリはクソ(再確認)