为什么我们有contains(Object o)而不是contains(E e)?

20

这是为了保持与旧版本的Collection(未泛型化)的向后兼容性吗?还是我遗漏了更微妙的细节?我在remove(Object o)中也看到了这种模式重复,但add则使用了泛型化的add(E e)


3
可能是 为什么 Java 集合的删除方法不是泛型的? 的重复问题。 - newacct
5个回答

14
contains()方法接收一个Object类型的参数,因为它所匹配的对象不必与传递给contains()的参数对象具有相同的类型;只需要它们相等即可。根据contains()方法的规范,如果存在一个对象e满足(o==null ? e==null : o.equals(e)),则contains(o)返回true。请注意,没有什么要求oe必须具有相同的类型。这是由于equals()方法接收一个Object类型的参数,而不仅仅是与该对象具有相同类型的参数。

虽然许多类通常在equals()中定义了其对象只能等于其自身类的对象,但这并不总是正确的。例如,List.equals()规范指出,如果两个List对象是不同的List实现,但内容相同,它们仍然相等。 因此回到本问题的示例,可以有一个Collection<ArrayList>并且我可以使用LinkedList作为参数调用contains(),如果有相同内容的列表,则可能返回true。如果contains()是泛型并限制其参数类型为E,则这将是不可能的。

实际上,contains()方法接受任何对象作为参数的事实允许一种有趣的用法,即您可以使用它来测试集合中是否存在满足某个属性的对象:

Collection<Integer> integers;
boolean oddNumberExists = integers.contains(new Object() {
    public boolean equals(Object e) {
        Integer i = (Integer)e;
        if (i % 2 != 0) return true;
        else return false;
    }
});

6
这个方法签名 contains( Object o ) 实际上是一个很好用的自我毁灭工具。这个方法展现了Java泛型的局限性。 - Alexander Pogrebnyak
如果contains()是通用的并将其参数类型限制为E,那么这是不可能的。但是,如果contains有一个重载,接受一个Comparator<E>,那么仍然可以实现相同的目标 - 这样就可以以您建议的方式(使用任意谓词和条件执行相等比较)进行操作,并以更安全的方式进行操作。 - Dai

5

4
这是因为contains函数利用了equals函数,而equals函数在基础Object类中定义,签名为equals(Object o)而不是equals(E e)(因为不是所有类都是泛型)。remove函数也是同样的情况 - 它使用equals函数遍历集合,该函数接受一个对象参数。

这并没有直接解释这个决定,因为他们仍然可以使用类型E并允许在调用equals时自动转换为类型Object;但我想他们希望允许该函数在其他对象类型上调用。有一个Collection<Foo> c;,然后调用c.contains(somethingOfTypeBar)是没有问题的 - 它总是返回false,因此它消除了对类型Foo的转换的需要(可能会抛出异常),或者为了保护异常,需要使用typeof调用。所以你可以想象如果你正在迭代混合类型的内容,并在每个元素上调用contains,你可以简单地在所有元素上使用contains函数,而不需要使用保护措施。
从某种意义上来说,这实际上类似于“新”的弱类型语言...

1
这与真正的原因无关。在containsremove上进行一定程度的类型检查会更好,这样就会减少错误的发生。 - Alexander Pogrebnyak
3
“它总是返回false” 不,它不会。一个类可以完全可能与另一个类的对象“相等”。 - newacct

0
否则,它只能与参数类型的精确匹配进行比较,特别是带通配符的集合将停止工作,例如。
class Base
{
}

class Derived
  extends Base
{
}

Collection< ? extends Base > c = ...;

Derived d = ...;

Base base_ref = d;

c.contains( d ); // Would have produced compile error

c.contains( base_ref ); // Would have produced compile error

编辑
对于那些认为这不是原因的怀疑者,这里有一个修改过的数组列表,带有可能泛型化的包含方法。

class MyCollection< E > extends ArrayList< E >
{
    public boolean myContains( E e )
    {
        return false;
    }
}

MyCollecttion< ? extends Base > c2 = ...;

c2.myContains( d ); // does not compile
c2.myContains( base_ref ); // does not compile

基本上contains( Object o )是一种hack,用于使这个非常常见的用例与Java Generics一起使用。


在正常情况下,在元素操作期间,没有人会想要将集合强制转换为“Collection<? extends Foo>”。如果是这种情况,“add(E e)”方法也会失败。 - Jai
@Jai。这里有一个场景void myMethod( Collection<? extends Foo> coll )。如果你需要在其中查看是否包含了某个版本的Foo,那么你将会遇到这个答案中描述的限制。 - Alexander Pogrebnyak

0

“那个苹果篮子里有这个橙吗?”

显然是无法给出正确答案的,但依然有两种可能:

  1. 答案是“否”。
  2. 问题不够完整,无法通过编译。

集合 API 选择了第一种方式。但第二种选择也是完全有道理的。像这样的问题在99.99%的情况下都是废话,所以干脆别问!


1
如果一个人有两个装着水果的篮子,并想知道其中一个篮子里是否有任何一种水果与另一个篮子里的水果相匹配,那么这个问题就不应该被认为是不合适的。如果一个篮子只装苹果,而另一个篮子只装橙子,那么可以很容易地缩短寻找匹配项的时间,但是假设一个篮子里只装苹果,而另一个篮子里装有各种水果,那么在询问苹果篮子中的每个水果是否与另一个篮子中的水果匹配之前,先验证其类型会更容易些。 - supercat
在我的例子中,参数已知为橙子,因此问题听起来很荒谬。在你的例子中,问题将是“这个苹果篮子里有这种水果吗?”,这是合理的。但这种用例较少见。我们可以重新设计API,允许您的问题,同时禁止我的问题,通过限制参数类型为Apple的超类型。 - irreputable
在 .Net 中没有机制可以限制一个方法的参数(泛型或其他类型)必须是另一种类型的超类型,我认为 Java 也没有这样的机制。这样做往往会违反 Liskov 替换原则,因为可用于Fruit的方法应该也可用于Orange。在 .Net 中有一些方法可以使用Obsolete标记来触发编译器警告,在某些特定的静态可识别的傻瓜情况下,但我认为这样做可能会更加混乱而不是有帮助。 - supercat
在Java中,您可以使用下限为E声明类型变量。这不会违反Liskov原则,因为泛型方法是重载方法。 - irreputable
给定一个抽象类 Bird 和一个接口 Flyable(由于并非所有的鸟都能飞,因此 Bird 没有实现它),以及一组 Bird 和对 Flyable 的引用,在集合中是否应该允许查找 Flyable?请注意,Flyable 既不是 Bird 的子类型也不是超类型;所引用的实例可能是 Sparrow(是鸟)或 Dragonfly(不是)。是否有任何方法可以在编译时警告试图在 Bird 集合中搜索 PersianCat 的尝试? - supercat
我会把它放在那0.01%的位置。 - irreputable

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接