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

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

package java.utils;

public interface Map<K, V> {

    boolean containsKey(Object key);

    boolean containsValue(Object value);

    V get(Object key);

    V remove(Object key);

    ...
}
package java.utils;

public interface List<E> extends Collection<E> {

    boolean contains(Object o);

    boolean remove(Object o);

    int indexOf(Object o);

    ...
}
package java.utils;

public interface Set<E> extends Collection<E> {
    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<ItemA> 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<? extends Foo> set这样的变量,在调用set.contains()方法时,无论传任何类型的对象,除非传null,否则编译器都会报错。因为? extends Foo不是一个确定的类型,任何确定的类型,即使是Foo的子类,该方法都不能接收,因为编译期不能确定这个Foo的子类就一定是创建set时指定的那个类型。

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

public class ExactTypeList<E> {

    private Node<E> head;

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

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

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

    private static final class Node<E> {

        E value;

        Node<E> next;

        Node(E value, Node<E> 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<? extends Foo> sExactTypeList;
    private static List<? extends Foo> sNormalList;

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

        List<Bar> 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<Bar>和ArrayList<Bar>,在main()方法中,调用sExactTypeList.contains(new Bar())方法编译不通过,sNormalList.contains(new Bar())调用没有问题,而两个List的add()方法接收的参数类型也都是范型类型,所以调用也会导致编译不通过,说明前面说的问题的确存在。

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容