为什么Collection.remove(Object o)不是通用的?
好像Collection<E>
可以有boolean remove(E o);
然后,当您不小心尝试从 a 中删除(例如)Set<String>
而不是每个单独的 String 时Collection<String>
,这将是编译时错误,而不是稍后的调试问题。
为什么Collection.remove(Object o)不是通用的?
好像Collection<E>
可以有boolean remove(E o);
然后,当您不小心尝试从 a 中删除(例如)Set<String>
而不是每个单独的 String 时Collection<String>
,这将是编译时错误,而不是稍后的调试问题。
Josh Bloch 和 Bill Pugh 在Java Puzzlers IV: The Phantom Reference Menace, Attack of the Clone 和 Revenge of The Shift中提到了这个问题。
Josh Bloch 说(6:41)他们试图泛化 Map 的 get 方法、remove 方法和其他一些方法,但“它根本不起作用”。
如果只允许集合的泛型作为参数类型,有太多合理的程序无法泛化。他给出的例子是s的a和s
的aList
的交集。Number
List
Long
remove()
(inMap
和 in Collection
) 不是通用的,因为您应该能够将任何类型的对象传递给remove()
. 移除的对象不必与传入的对象类型相同remove()
;它只要求它们是平等的。从 的规范中remove()
,remove(o)
删除对象e
是。请注意,没有什么要求和必须是同一类型。这是因为该方法接受一个as 参数,而不仅仅是与对象相同的类型。(o==null ? e==null : o.equals(e))
true
o
e
equals()
Object
equals()
虽然,许多类已经定义了它的对象只能等于它自己的类的对象,这可能是普遍正确的,但情况肯定并非总是如此。例如,规范List.equals()
说如果两个 List 对象都是 List 并且具有相同的内容,则它们是相等的,即使它们是不同的实现List
。所以回到这个问题中的例子,Map<ArrayList, Something>
我可以使用 a 和remove()
aLinkedList
作为参数调用,它应该删除作为具有相同内容的列表的键。remove()
如果是通用的并且限制了它的参数类型,这是不可能的。
因为如果你的类型参数是通配符,你就不能使用通用的 remove 方法。
我似乎记得使用 Map 的 get(Object) 方法遇到了这个问题。在这种情况下,get 方法不是通用的,尽管它应该合理地期望传递一个与第一个类型参数相同类型的对象。我意识到,如果您使用通配符作为第一个类型参数传递 Maps,那么如果该参数是通用的,则无法使用该方法从 Map 中获取元素。通配符参数不能真正得到满足,因为编译器不能保证类型是正确的。我推测 add 是通用的原因是您应该在将其添加到集合之前保证类型是正确的。但是,当删除一个对象时,如果类型不正确,那么它无论如何都不会匹配任何东西。
我可能没有很好地解释它,但对我来说似乎足够合乎逻辑。
除了其他答案之外,该方法应该接受一个Object
谓词还有另一个原因。考虑以下示例:
class Person {
public String name;
// override equals()
}
class Employee extends Person {
public String company;
// override equals()
}
class Developer extends Employee {
public int yearsOfExperience;
// override equals()
}
class Test {
public static void main(String[] args) {
Collection<? extends Person> people = new ArrayList<Employee>();
// ...
// to remove the first employee with a specific name:
people.remove(new Person(someName1));
// to remove the first developer that matches some criteria:
people.remove(new Developer(someName2, someCompany, 10));
// to remove the first employee who is either
// a developer or an employee of someCompany:
people.remove(new Object() {
public boolean equals(Object employee) {
return employee instanceof Developer
|| ((Employee) employee).company.equals(someCompany);
}});
}
}
关键是传递给remove
方法的对象负责定义equals
方法。通过这种方式构建谓词变得非常简单。
假设一个人有一个集合Cat
,以及一些类型为Animal
、Cat
、SiameseCat
和的对象引用Dog
。询问集合是否包含Cat
orSiameseCat
引用所指的对象似乎是合理的。询问它是否包含Animal
引用所指的对象可能看起来很狡猾,但它仍然是完全合理的。毕竟,有问题的对象可能是Cat
, 并且可能出现在集合中。
此外,即使对象碰巧不是 a Cat
,也没有问题说它是否出现在集合中——只需回答“不,它没有”。某种类型的“查找样式”集合应该能够有意义地接受任何超类型的引用并确定该对象是否存在于集合中。如果传入的对象引用是不相关的类型,则集合不可能包含它,因此查询在某种意义上是没有意义的(它总是回答“否”)。尽管如此,由于没有任何方法可以将参数限制为子类型或超类型,因此最实际的做法是简单地接受任何类型并对类型与集合无关的任何对象回答“否”。
我一直认为这是因为 remove() 没有理由关心你给它什么类型的对象。无论如何,检查该对象是否是 Collection 包含的对象之一是很容易的,因为它可以在任何东西上调用 equals()。有必要检查 add() 上的类型以确保它只包含该类型的对象。
这是一种妥协。这两种方法都有其优势:
remove(Object o)
remove(E e)
通过在编译时检测细微的错误(例如错误地尝试从短裤列表中删除整数)来为大多数程序想要做的事情带来更多的类型安全性。向后兼容一直是 Java API 发展的主要目标,因此选择 remove(Object o) 是因为它使生成现有代码更容易。如果向后兼容性不是问题,我猜设计师会选择 remove(E e)。
Remove 不是泛型方法,因此使用非泛型集合的现有代码仍然可以编译并且仍然具有相同的行为。
有关详细信息,请参阅http://www.ibm.com/developerworks/java/library/j-jtp01255.html。
编辑:评论者问为什么 add 方法是通用的。[...删除了我的解释...] 第二个评论者回答了 firebird84 的问题,比我好得多。
另一个原因是接口。这是一个显示它的示例:
public interface A {}
public interface B {}
public class MyClass implements A, B {}
public static void main(String[] args) {
Collection<A> collection = new ArrayList<>();
MyClass item = new MyClass();
collection.add(item); // works fine
B b = item; // valid
collection.remove(b); /* It works because the remove method accepts an Object. If it was generic, this would not work */
}
因为它会破坏现有的(Java5 之前的)代码。例如,
Set stringSet = new HashSet();
// do some stuff...
Object o = "foobar";
stringSet.remove(o);
现在您可能会说上面的代码是错误的,但假设 o 来自一组异构对象(即,它包含字符串、数字、对象等)。您想删除所有匹配项,这是合法的,因为 remove 只会忽略非字符串,因为它们不相等。但是,如果您将其设为 remove(String o),则不再有效。