Hatena::ブログ(Diary)

m2

2012-03-20

Java の List の indexOf, contains, remove がなぜかタイプセーフじゃなくて軽く死ねる件 (Map#get(Object) も)

いやーそんなはずはないと思って今まで確認すらしてませんでした。

http://java.sun.com/javase/ja/6/docs/ja/api/java/util/List.html#contains(java.lang.Object)

どういうことかというと、これがコンパイルエラーにならないということです。

import static java.util.Arrays.*;

public class Hoge {
    public static boolean isHogehoge(Integer hogeType) {
        return asList("1", "3", "5").contains(hogeType);
    }
}

コンパイルエラーにするにはジェネリックなメソッドを作って、そちらを使用します。

import static java.util.Arrays.*;

public class Hoge {
    public static <E> boolean contains(E value, java.util.List<E> list) {
        return list.contains(value);
    }
    public static boolean isHogehoge(String hogeType) {
        return contains(hogeType, asList("1", "3", "5"));
    }
}

これで isHogehoge のパラメーターを「Integer hogeType」にすると、ちゃんとコンパイルエラーになります。

しかしまたつまらぬ static メソッドが増えてしまった。
Object#equals(Object) もそうだけど、基本的に Java プロジェクトでは一つ static メソッド集的なものを作って、オブジェクトメソッドでなくそちらを使うようにすると問題が減らせると思います。

こんなの絶対 OOP じゃないよ!

--

(追記)

Map#get(Object) も同じようです。

java List contains typesafe」なんかで検索してもそれっぽい情報が見つからなかったのですけど、なるほど Map の方が問題になるケースが多そうで、「java Map get typesafe」で検索するとそういった記事や議論が見つかりました。

Recently I have encountered a bug in a production code when dealing with a simple (really simple) usage of a Map.

...

import java.util.HashMap;
import java.util.Map;

public class EmployeeDataLookup2 {
    // A map storing relation between employee ID and name.
    private Map<Long, String> employeeIdToName;
...
    // Lookup method for finding employee name for given employee ID.
    public String findEmployeeName(int employeeId) {
        return employeeIdToName.get(employeeId);
    }

...

The problem is that after introducing generics, probably due to backward compatibility, some of the methods in Collection and Map interface have not been changed and still do not perform type checking. One of them is Map.get(Object) which have caused the bug in the code above.

JavaBlogging » Type safety in Java Set and Map

As mentioned by people above, the reason why get(), etc. is not generic because the key of the entry you are retrieving does not have to be the same type as the object that you pass in to get(); the specification of the method only requires that they be equal. This follows from how the equals() method takes in an Object as parameter, not just the same type as the object.

...

  1. Then why is V Get(K k) in C#?
    -> Differents specifications.
java - What are the reasons why Map.get%28Object key%29 is not %28fully%29 generic - Stack Overflow

an object of one type can be .equals() to an object of another type. get() only requires that the object that you give it be .equals() to the key you are getting

Java Generics: Why Does Map.get%28%29 Ignore Type? - Stack Overflow

A popular myth is that it is stupid and evil, but it was necessary because of backward compatibility. But the compatibility argument is irrelevant; the API is correct whether you consider compatibility or not. Here's the real reason why.

...

Let's say you have a method that wants to read from a Set of Foos:

    public void doSomeReading(Set<Foo> foos) { ... }

The problem with this signature is it won't allow a Set<SubFoo> to be passed in (where SubFoo is, of course, a subtype of Foo).

...

    public void doSomeReading(Set<? extends Foo> foos) { ... }

Perfect!

But here's the catch: if Set.contains() accepted type E instead of type Object, it would now be rendered completely unusable to you inside this method body!

...

Static analysis plays an extremely important role in the construction of bug-free software.

smallwig: Why does Set.contains%28%29 take an Object, not an E?

最後に引用した Kevin Bourrillion さんの post に全部書かれてますね。

僕が書いた contains の実装はワイルドカードに対応できません。そのまま使うとコンパイルエラーになります。

    static class Foo {
    }
    static class SubFoo extends Foo {
    }
    public void doSomeReading(List<? extends Foo> foos) {
        if (foos.contains(new SubFoo())) { // API仕様通り
            // 
        }
        if (contains(new Foo(), foos)) { // <- コンパイルエラー
            // 
        }
    }

えー、じゃあ contains をこんな感じにすればどうかなーと思うわけですが。

    public static <E> boolean contains2(E value, java.util.List<? extends E> list) {
        return list.contains(value);
    }

こんな結果に。元々何がやりたかったかわけわかんなくなります。

    public void doSomeReading(List<? extends Foo> foos) {
        if (contains2(new Object(), foos)) { // えっ
            // 
        }
    }

f:id:miya2000:20120321210131p:image


(あとで書く。結論としては FindBugs でOK。)

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。

トラックバック - http://d.hatena.ne.jp/miya2000/20120320/p0