Java、標準ライブラリにおけるオブジェクトの等価性は型で判断されないという話

Set<String> userNames=Sets.newHashSet();

int userId=...
String userName=...

// Collections<E>#add(E o)
// 型が違うのでエラーになってくれる
userNames.add(userId);

// Collections<E>#contains(Object o)
// 引数がObjectなのでコンパイルが通ってしまう!
if(userNames.contains(userId))
  doSomething();

Java Collection APIの仕様がどうしてこうなってるのか前から謎で。


調査したところ、つまりはこういうことだった: java - What are the reasons why Map.get(Object key) is not (fully) generic - Stack Overflow

boolean contains(Object o)

コレクションに指定された要素がある場合に true を返します。すなわち、このコレクションに (o==null ? e==null : o.equals(e)) である要素 e が 1 つ以上ある場合にだけ true を返します。

http://java.sun.com/j2se/1.5.0/ja/docs/ja/api/java/util/Collection.html#contains(java.lang.Object)

Javaにおいて、nullでない二つのオブジェクトが等しいということは、o1.equals(o2)が成り立つということだ。そして、Object#equalsは引数としてObjectを取る。すなわち、型と関係なく等価性が成り立つ!

だからCollection#containsやMap#getはObjectを引数に取らざるを得ず、様々なメリットや弊害をもたらしている(個人的感想としては弊害のほうが多い)

なぜこうなったのか

たとえばListの等価性は、リストの要素数と内容が等しいことだと定義されている。

boolean equals(Object o)
指定されたオブジェクトがリストと等しいかどうかを比較します。指定されたオブジェクトもリストであり、サイズが同じで、2 つのリストの対応する要素がすべて「等しい」場合にだけ true を返します。2 つの要素 e1 および e2 は、(e1==null ? e2==null : e1.equals(e2)) の場合に等しいと見なされます。つまり 2 つのリストは、同じ要素が同じ順序で含まれている場合に等しいものとして定義されます。この定義により、List インタフェースの実装が異なっても、equals メソッドが正しく動作することが保証されます。

http://java.sun.com/j2se/1.5.0/ja/docs/ja/api/java/util/List.html#equals(java.lang.Object)

つまり、ArrayList(1,2,3)とLinkedList(1,2,3)は、型が違うが、しかし等価だということだ。型によらない比較あってこそで、なるほど便利…… しかし本当にいいのかこれで……

どうしてほしかったのか

そもそも値の等価性(とハッシュ値の計算)はすべてのObjectが持つ性質ではない。定義できるのは参照等価性だけで、それはメソッドにするまでもなし(==で得られる)。

等価性が定義可能なクラスには、

interface Equalable<T> {
  public boolean equals(T other);
  public int hashCode();
}

のようなインタフェースを実装するようにすればよかったのではないか。
List implements Equalable にすれば先程の例は達成できる。

だがしかし、時はGenerics以前

Java1.0の頃だと

interface Equalable {
  public boolean equals(Object other);
  public int hashCode();
}

というものを定義することになり、規約によって指定された型以外のものを渡すなということはできるが、これではかなり嬉しさが減る。いっそのことObjectに生やしちまえみたいな気持ちになるのもうなずける。

まとめ

Java言語のことはそろそろ忘れたいです……