为什么Map.containsKey()方法的参数类型是Object?

我在看Map源码的时候突然意识到Map.containsKey()方法的参数类型是Object类型,而且我发现Java集合框架中的List、Set、Map等接口中很多方法的参数类型都是Object类型而不是它们的范型参数类型。

package java.utils;

public interface Map {

    boolean containsKey(Object key);

    boolean containsValue(Object value);

    V get(Object key);

    V remove(Object key);

    ...
}
package java.utils;

public interface List extends Collection {

    boolean contains(Object o);

    boolean remove(Object o);

    int indexOf(Object o);

    ...
}
package java.utils;

public interface Set extends Collection {
    boolean contains(Object o);

    boolean remove(Object o);

    ...
}

但是感觉这些方法本身是应该用范型参数类型的,为什么要用Object类型呢?我为此感到疑惑,于是决定研究一下这样做的原因。

SO上很早就有人提过这个问题What are the reasons why Map.get(Object key) is not (fully) generic。看来很多人也都有这样的疑惑,那么现在就来看看别人是怎么解释这个问题的吧。

第一种解释

List、Set和Map中对key或value的相等(equivalence)的判断都是依赖Object.equals()方法,而Object.equals()方法接收的参数类型正是Object类型。Object子类如果重写了equals()方法的话,并没有要求参数类型和当前类类型一定要相同才返回true。

举个例子:

public class ItemA {

    private String id;

    public ItemA(String id) {
        this.id = Objects.requireNonNull(id, "id is null");
    }

    public String getId() {
        return id;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj instanceof ItemA) {
            return id.equals(((ItemA) obj).id);
        } else if (obj instanceof ItemB) {
            return id.equals(((ItemB) obj).getId());
        }

        return false;
    }

    @Override
    public int hashCode() {
        return id.hashCode();
    }
}
public class ItemB {

    private String id;

    public ItemB(String id) {
        this.id = Objects.requireNonNull(id, "id is null");
    }

    public String getId() {
        return id;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj instanceof ItemB) {
            return id.equals(((ItemB) obj).id);
        } else if (obj instanceof ItemA) {
            return id.equals(((ItemA) obj).getId());
        }

        return false;
    }

    @Override
    public int hashCode() {
        return id.hashCode();
    }
}
    public static void main(String[] args) {
        ItemA itemA = new ItemA("abc");
        ItemB itemB = new ItemB("abc");
        System.out.println(itemA.equals(itemB));
        System.out.println(itemB.equals(itemA));
    }
    /**
     * 输出:
     * true
     * true
     */

这里ItemA和ItemB是两个不同的类(代码基本相同,主要看equals()方法的实现),但是两个不同的类的对象调用equals()方法可能返回true,这在Java中是允许的,而且equals()方法的实现也完全符合规范(自反性、对称性、传递性、一致性)。既然equals()方法没有对参数类型进行限制,而List、Set、Map对集合内元素的检索又是依赖与这些元素的equals()方法的,那么这些集合类本身也不应该对检索方法的参数类型做额外的限制。

这样的话下面的代码就是允许的:

    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add(new ItemA("abc"));
        ItemB itemB = new ItemB("abc");
        System.out.println(list.contains(itemB));
        list.remove(itemB);
        System.out.println(list.contains(itemB));
    }
    /**
     * 输出:
     * true
     * false
     */

也就是说Java的集合类允许使用其它类的对象对集合中的元素进行检索。所以其实根本原因还是在于equals()方法的参数类型是Object。

第二种解释

还有一种解释在这篇博客里,这是Google的一位开发者写的。

他是这样解释的:如果contains()这样的方法接收的参数不是Object类型,而是指定的范型参数类型(假设是E),那么如果使用类似Set set这样的变量,在调用set.contains()方法时,无论传任何类型的对象,除非传null,否则编译器都会报错。因为? extends Foo不是一个确定的类型,任何确定的类型,即使是Foo的子类,该方法都不能接收,因为编译期不能确定这个Foo的子类就一定是创建set时指定的那个类型。

我这里写了一个例子来检验他的说法。首先我写了一个简易的List:

public class ExactTypeList {

    private Node head;

    public void add(E value) {
        value = Objects.requireNonNull(value, "value is null");

        Node node = new Node<>(value, null);
        if (head == null) {
            head = node;
        } else {
            // 头插
            node.next = head;
            head = node;
        }
    }

    public boolean contains(E value) {
        Node node = head;
        while (node != null) {
            if (node.value.equals(value)) {
                return true;
            }
            node = node.next;
        }
        return false;
    }

    private static final class Node {

        E value;

        Node next;

        Node(E value, Node next) {
            this.value = value;
            this.next = next;
        }
    }
}

其它的不用在意,只需要关注现在这个ExactTypeList的contains()方法的参数类型不再是Object类型了,而是E类型。然后我们需要再写一些测试代码,来进行验证。

public class Test {

    private static class Foo {
        // empty class
    }

    private static final class Bar extends Foo {
        // empty class
    }

    private static ExactTypeList sExactTypeList;
    private static List sNormalList;

    static {
        ExactTypeList list = new ExactTypeList<>();
        list.add(new Bar());
        sExactTypeList = list;

        List normalList = new ArrayList<>();
        normalList.add(new Bar());
        sNormalList = normalList;
    }

    public static void main(String[] args) {
//        sExactTypeList.add(new Bar()); // 编译不通过
//        System.out.println(sExactTypeList.contains(new Bar())); // 编译不通过

//        sNormalList.add(new Bar()); // 编译不通过
        System.out.println(sNormalList.contains(new Bar()));
    }
    /**
     * 输出:
     * false
     */
}

在这个测试代码中,变量sExactTypeList和sNormalList所引用对象的实际类型分别是ExactTypeList和ArrayList,在main()方法中,调用sExactTypeList.contains(new Bar())方法编译不通过,sNormalList.contains(new Bar())调用没有问题,而两个List的add()方法接收的参数类型也都是范型类型,所以调用也会导致编译不通过,说明前面说的问题的确存在。

这样看来contains()方法接收Object类型的参数是合理的,那为什么add()方法接收的却是E类型的参数呢?根据这位作者的解释,这是显而易见的,add()方法如果接收的不是E类型的参数的话,集合中保存的就不是E类型的元素了,这样集合就被破坏了,所以只有那些不会破坏集合的方法才可以接收Object类型的参数。

所以,根据上述的两种解释,以及可能还有其它的解释,集合中的contains()这样的方法接收的参数需要是Object类型。至于这种设计好不好,或者说有没有更好、更优雅的方法解决前面提到的一些问题,这都不是我所要追究的了。我们现在能做的是了解它、接受它,以及使用它。

你可能感兴趣的:(为什么Map.containsKey()方法的参数类型是Object?)