美文网首页
为什么Map.containsKey()方法的参数类型是Obje

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

作者: _wf | 来源:发表于2017-07-22 19:15 被阅读0次

    我在看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类型。至于这种设计好不好,或者说有没有更好、更优雅的方法解决前面提到的一些问题,这都不是我所要追究的了。我们现在能做的是了解它、接受它,以及使用它。

    相关文章

      网友评论

          本文标题:为什么Map.containsKey()方法的参数类型是Obje

          本文链接:https://www.haomeiwen.com/subject/ujmukxtx.html