为什么Java Collections不能通用删除方法?


144

为什么不Collection.remove(Object o)通用?

好像Collection<E>boolean remove(E o);

然后,当您不小心尝试从中删除(例如)Set<String>而不是每个String时Collection<String>,这将是编译时错误,而不是以后的调试问题。


13
当您将其与自动装箱功能结合使用时,这确实会伤到您。如果尝试从列表中删除某些内容,然后将其传递给Integer而不是Int,则它将调用remove(Object)方法。
ScArcher2

Answers:


73

乔什·布洛赫(Josh Bloch)和比尔·普(Bill Pugh)在《Java Puzzlers IV:幻影参考威胁,克隆人的攻击和复仇的复仇》中提到了这个问题。

乔什·布洛赫(Josh Bloch)说(6:41),他们试图泛化Map的get方法,remove方法和其他方法,但是“这根本没有用”。

如果仅允许将集合的通用类型作为参数类型,则无法生成太多合理的程序。由他给出的例子是一个交叉点ListNumberS和 ListLong


6
为什么add()可以接受类型化的参数,但是remove()不能接受,这仍然超出了我的理解范围。Josh Bloch将成为Collections问题的权威参考。这可能就是我不尝试进行类似的收集实现并亲自查看的全部内容。:(谢谢。
克里斯·马佐拉

2
克里斯-阅读Java Generics Tutorial PDF,它将解释原因。
JeeBee

42
其实,这很简单!如果add()使用了错误的对象,则会破坏集合。它包含了不应该包含的内容!对于remove()或contains()而言并非如此。
凯文·布瑞里恩

12
顺便说一句,在整个库中,绝对遵循该基本规则-使用类型参数仅防止对集合的实际损害。
凯文·布瑞里恩

3
@KevinBourrillion:多年来,我一直在使用泛型(在Java和C#中),而没有意识到“实际损坏”规则甚至存在……但是现在我已经看到它直接说明了它是100%完美的。谢谢你的鱼!!!除了现在,我感到不得不回过头来看看我的实现,以查看某些方法是否可以并且因此应该被简化。叹。
corlettk

74

remove()(in Map和in Collection)不是通用的,因为您应该能够将任何类型的对象传递给remove()。删除的对象不必与您传递给的对象具有相同的类型remove();它只要求它们相等。从本说明书中remove()remove(o)将删除对象e,使得(o==null ? e==null : o.equals(e))true。请注意,没有什么要求,o并且e必须是相同的类型。这是因为该equals()方法接受一个Objectas参数,而不仅仅是与该对象相同的类型。

尽管通常已经equals()定义了许多类,以便其对象只能等于其自己的类的对象,这确实是正确的,但事实并非总是如此。例如,的规范List.equals()说两个List对象都是相同的,并且都是相同的内容,即使它们都是的不同实现,也是如此List。所以回来的例子在这个问题上,可以有一个Map<ArrayList, Something>和我打电话给remove()一个LinkedList作为参数,并应删除这与相同内容的列表的关键。如果remove()是通用的并且限制了它的参数类型,那将是不可能的。


1
但是,如果将Map定义为Map <List,Something>(而不是ArrayList),则可以使用LinkedList进行删除。我认为这个答案是不正确的。
AlikElzin-kilaka 2012年

3
答案似乎正确,但不完整。它只适合问一个问题,为什么他们也没有将equals()方法通用化?我可以看到键入安全而不是这种“自由主义者”方法会带来更多好处。我认为当前实现的大多数情况是因为错误进入了我们的代码,而不是因为这种remove()方法带来的灵活性。
kellogs's

2
@kellogs:“泛化equals()方法” 是什么意思?
newacct

5
@MattBall:“其中T是声明类”,但是Java中没有这样的语法。T必须在类上声明为类型参数,并且Object没有任何类型参数。没有一种类型可以引用“声明类”。
newacct

3
我认为kellogs是说什么,如果平等是一个通用接口Equality<T>equals(T other)。然后,您可能拥有remove(Equality<T> o)并且o仅仅是可以与另一个对象进行比较的一个对象T
weston

11

因为如果类型参数是通配符,则不能使用通用的remove方法。

我似乎回想起了Map的get(Object)方法遇到的这个问题。在这种情况下,get方法不是通用的,尽管应该合理地期望将其传递给与第一个type参数相同类型的对象。我意识到,如果您要传递带有通配符作为第一个类型参数的Maps,那么如果该参数是通用的,则无法使用该方法从Map中获取元素。不能真正满足通配符参数,因为编译器无法保证类型正确。我推测add是泛型的原因是,您应该在将其添加到集合之前保证类型正确。但是,在删除对象时,如果类型不正确,则无论如何都不匹配。

我可能没有很好地解释它,但是对我来说似乎很合逻辑。


1
您能详细说明一下吗?
Thomas Owens

6

除了其他答案外,该方法还应接受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方法。这样,构建谓词变得非常简单。


投资者?(填料填料)
Matt R

3
该列表被实现为yourObject.equals(developer):作为集合API中记录java.sun.com/javase/6/docs/api/java/util/...
胡沙姆阿里

13
对我来说,这似乎是一种虐待
RAY 2010年

7
这是滥用,因为您的谓词对象破坏了equals方法的约束,即对称性。只要您的对象满足equals / hashCode规范,即可将remove方法绑定到其规范,因此任何实现都可以自由地进行比较。另外,您的谓词对象不会实现该.hashCode()方法(无法始终实现等于),因此remove调用将永远不适用于基于Hash的集合(如HashSet或HashMap.keys())。与ArrayList一起使用真是幸运。
圣保罗Ebermann

3
(我没有在讨论泛型类型问题-之前曾提到过,这里只对谓词使用equals。)当然,HashMap和HashSet正在检查哈希码,而TreeSet / Map正在使用元素的顺序。尽管如此,他们仍完全执行Collection.remove,而不会违反合同(如果顺序与等号一致)。而且,经过改写的带有equals调用的各种ArrayList(或我认为AbstractCollection)仍然可以正确实现合同-如果您违反equals合同,那么如果它不能按预期工作,那是您的错。
圣保罗Ebermann

5

假设一个人的集合Cat,和一些类型的对象引用AnimalCatSiameseCat,和Dog。询问集合是否包含CatSiameseCat引用所引用的对象似乎是合理的。询问它是否包含该Animal引用所引用的对象似乎有些狡猾,但这仍然是完全合理的。毕竟,所讨论的对象可能是Cat,并且可能会出现在集合中。

此外,即使对象碰巧不是a Cat,也可以毫无疑问地说它是否出现在集合中-只需回答“不,就不”。某种类型的“查找样式”集合应该能够有意义地接受任何超类型的引用,并确定对象是否存在于集合中。如果传入的对象引用是不相关的类型,则该集合不可能包含它,因此查询在某种意义上是没有意义的(它将始终回答“否”)。但是,由于没有任何方法可以将参数限制为子类型或超类型,因此最简单的方法是简单地接受任何类型,并对与集合的类型无关的任何对象回答“否”。


1
“如果传入的对象引用是不相关的类型,则该集合不可能包含它。”错误。它只需要包含与之相等的东西即可。并且不同类别的对象可以相等。
newacct 2012年

“看似狡猾,但仍然完全合理”是吗?考虑一个世界,在该世界中不能总是检查一种类型的对象与另一种类型的对象的相等性,因为它可以等于什么类型的对象是参数化的(类似于Comparable可以比较的类型的参数化方式)。那么,允许人们传递不相关类型的东西是不合理的。
newacct 2012年

@newacct:有大小比较和相等比较之间的根本区别:给定对象AB一种类型的,并XY另一个,使得A> B,和X> Y。任一A> YY< A,或X> BB< X。仅当量级比较了解两种类型时,这些关系才能存在。相比之下,对象的相等性比较方法可以简单地声明自己与任何其他类型的任何对象都不相等,而不必了解所讨论的其他类型的任何内容。类型的对象Cat可能不知道它是否是...
超级猫

...“大于”或“小于”类型的对象FordMustang,但说它是否等于这样的对象(答案显然是“否”)应该没有困难。
2012年

4

我一直认为这是因为remove()没有理由关心您提供的对象类型。无论如何,检查该对象是否是Collection包含的对象之一很容易,因为它可以对任何对象调用equals()。必须检查add()的类型,以确保它仅包含该类型的对象。


0

这是一个妥协。两种方法都有其优势:

  • remove(Object o)
    • 更灵活。例如,它允许遍历数字列表并将其从多头列表中删除。
    • 使用这种灵活性的代码可以更容易地生成
  • remove(E e) 通过在编译时检测细微的错误,使大多数程序想要执行的操作具有更多的类型安全性,例如错误地尝试从短裤列表中删除整数。

向后兼容性一直是发展Java API的主要目标,因此选择remove(Object o)是因为它使生成现有代码更加容易。如果向后兼容性不是问题,那么我猜设计师会选择remove(E e)。


-1

Remove不是通用方法,因此使用非通用集合的现有代码仍将编译并且仍具有相同的行为。

有关详细信息,请参见http://www.ibm.com/developerworks/java/library/j-jtp01255.html

编辑:评论者问为什么add方法是通用的。[...删除了我的解释...]第二位评论者回答了firebird84的问题,比我好得多。


2
那么为什么add方法是通用的?
Bob Gettys

@ firebird84 remove(Object)会忽略错误类型的对象,但是remove(E)会导致编译错误。那会改变行为。
诺亚

:shrug:-运行时行为更改;编译错误不是运行时行为。add方法的“行为”以这种方式更改。
詹森·S,2009年

-2

另一个原因是由于接口。这是显示它的示例:

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 */
}

您正在展示一些可以避免的东西,因为remove()它不是协变的。但是,问题是是否应该允许这样做。ArrayList#remove()通过值相等而不是引用相等来工作。您为什么期望a B等于a A?在您的示例可以,但是这是一个奇怪的期望。我希望您MyClass在这里提供一个论点。
2015年

-3

因为它将破坏现有的(Java5之前的)代码。例如,

Set stringSet = new HashSet();
// do some stuff...
Object o = "foobar";
stringSet.remove(o);

现在您可能会说上面的代码是错误的,但是假设o来自不同种类的对象(即,它包含字符串,数字,对象等)。您要删除所有匹配项,这是合法的,因为remove会忽略非字符串,因为它们不相等。但是,如果将其设置为remove(String o),它将不再起作用。


4
如果我实例化一个List <String>,我希望只能调用List.remove(someString);。如果需要支持向后兼容性,可以使用原始List-List <?>,然后可以调用list.remove(someObject),不是吗?
克里斯·马佐拉

5
如果用“ add”替换“ remove”,那么该代码将被Java 5中实际执行的操作
破坏。– DJClayworth
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.