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