153

Why isn't Collection.remove(Object o) generic?

Seems like Collection<E> could have boolean remove(E o);

Then, when you accidentally try to remove (for example) Set<String> instead of each individual String from a Collection<String>, it would be a compile time error instead of a debugging problem later.

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Chris Mazzola
  • 5,807
  • 5
  • 22
  • 15
  • 14
    This can really bite you when you combine it with autoboxing. If you try to remove something from a List and you pass it an Integer instead of an int it calls the remove(Object) method. – ScArcher2 Sep 19 '08 at 19:51
  • 3
    Similar question regarding Map: http://stackoverflow.com/questions/857420/what-are-the-reasons-why-map-getobject-key-is-not-fully-generic – AlikElzin-kilaka Sep 04 '12 at 12:37

10 Answers10

78

Josh Bloch and Bill Pugh refer to this issue in Java Puzzlers IV: The Phantom Reference Menace, Attack of the Clone, and Revenge of The Shift.

Josh Bloch says (6:41) that they attempted to generify the get method of Map, remove method and some other, but "it simply didn't work".

There are too many reasonable programs that could not be generified if you only allow the generic type of the collection as parameter type. The example given by him is an intersection of a List of Numbers and a List of Longs.

Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
dmeister
  • 34,704
  • 19
  • 73
  • 95
  • 6
    Why add() can take a typed parameter but remove() can't is still a bit beyond my comprehension. Josh Bloch would be the definitive reference for Collections questions. It might be all I get without trying to make a similar collection implementation and see for myself. :( Thanks. – Chris Mazzola Sep 22 '08 at 17:34
  • 2
    Chris - read the Java Generics Tutorial PDF, it will explain why. – JeeBee Jan 27 '09 at 13:01
  • 45
    Actually, it's very simple! If add() took a wrong object, it would break the collection. It would contain things it's not supposed to! That is not the case for remove(), or contains(). – Kevin Bourrillion Nov 07 '09 at 03:46
  • 13
    Incidentally, that basic rule -- using type parameters to prevent actual damage to the collection only -- is followed absolutely consistently in the whole library. – Kevin Bourrillion Nov 07 '09 at 03:49
  • 3
    @KevinBourrillion: I've been working with generics for years (in both Java and C#) without ever realising that "actual damage" rule even exists... but now that I've seen it stated directly it makes 100% perfect sense. Thanks for the fish!!! Except now I feel compelled to go back and look at my implementations, to see if some methods can and therefore should be degenerified. Sigh. – corlettk Aug 22 '13 at 23:41
  • 2
    Couldn't they have done `public interface List { boolean remove(E element); }` ? – Zymus Aug 18 '15 at 19:19
  • Yes, @Zymus, a very good point. There is no use in assuming there can be anything in a Collection because, there can be only objects of (sub)type T in Collection. – klaar Feb 28 '18 at 09:49
  • 1
    To spell out what Joshua Bloch likely meant by the intersection of a list of numbers and a list of longs: if Collection.retainAll would take a Collection extends E> instead of a Collection> then existing code like longs.retainAll(numbers) could NOT simply be generified but would have to be changed to numbers.retainAll(longs). – Stefan Feuerhahn Jul 24 '19 at 08:03
  • 2
    @Zymus a signature like `boolean remove(E element)` makes no sence, a method like `boolean remove(T element)` would already allow arbitrary subtypes of `T`. As you can always assign a subtype to a variable, e.g. `Object o = "foo";` – Holger Jul 24 '19 at 19:22
78

remove() (in Map as well as in Collection) is not generic because you should be able to pass in any type of object to remove(). The object removed does not have to be the same type as the object that you pass in to remove(); it only requires that they be equal. From the specification of remove(), remove(o) removes the object e such that (o==null ? e==null : o.equals(e)) is true. Note that there is nothing requiring o and e to be the same type. This follows from the fact that the equals() method takes in an Object as parameter, not just the same type as the object.

Although, it may be commonly true that many classes have equals() defined so that its objects can only be equal to objects of its own class, that is certainly not always the case. For example, the specification for List.equals() says that two List objects are equal if they are both Lists and have the same contents, even if they are different implementations of List. So coming back to the example in this question, it is possible to have a Map<ArrayList, Something> and for me to call remove() with a LinkedList as argument, and it should remove the key which is a list with the same contents. This would not be possible if remove() were generic and restricted its argument type.

Sai Kishore
  • 326
  • 1
  • 7
  • 16
newacct
  • 119,665
  • 29
  • 163
  • 224
  • 1
    But if you were to define the Map as Map (instead of ArrayList), it would have been possible to remove using a LinkedList. I think this answer is incorrect. – AlikElzin-kilaka Sep 04 '12 at 12:21
  • 1
    @kilaka but if you do that, then you can add not `ArrayList` in the map which may be undesirable. – PhoneixS Aug 07 '13 at 15:47
  • 3
    The answer appears to be correct, but incomplete. It only lends itself to asking why the heck did they not genericize the `equals()` method as well ? I could see more benefits to type safety instead of this "libertarian" approach. I think most cases with current implementation are for bugs getting into our code rather than about joyfulness this flexibility that the `remove()` method brings. – kellogs Feb 05 '15 at 18:17
  • 2
    @kellogs: What would you mean by "genericize the `equals()` method"? – newacct Feb 05 '15 at 20:12
  • 1
    @newacct I assume that @kellogs is referring to a signature like `public boolean equals(T t)` where `T` is the declaring class. – Matt Ball Mar 27 '15 at 21:37
  • 5
    @MattBall: "where T is the declaring class" But there is no such syntax in Java. `T` must be declared as a type parameter on the class, and `Object` doesn't have any type parameters. There is no way to have a type that refers to "the declaring class". – newacct Mar 27 '15 at 21:50
  • 3
    I think kellogs is saying what if equality were a generic interface `Equality` with `equals(T other)`. Then you could have `remove(Equality o)` and `o` is just some object that can be compared to another `T`. – weston Feb 11 '17 at 16:48
11

Because if your type parameter is a wildcard, you can't use a generic remove method.

I seem to recall running into this question with Map's get(Object) method. The get method in this case isn't generic, though it should reasonably expect to be passed an object of the same type as the first type parameter. I realized that if you're passing around Maps with a wildcard as the first type parameter, then there's no way to get an element out of the Map with that method, if that argument was generic. Wildcard arguments can't really be satisfied, because the compiler can't guarantee that the type is correct. I speculate that the reason add is generic is that you're expected to guarantee that the type is correct before adding it to the collection. However, when removing an object, if the type is incorrect then it won't match anything anyway. If the argument were a wildcard the method would simply be unusable, even though you may have an object which you can GUARANTEE belongs to that collection, because you just got a reference to it in the previous line....

I probably didn't explain it very well, but it seems logical enough to me.

Bob Gettys
  • 1,194
  • 1
  • 8
  • 12
6

In addition to the other answers, there is another reason why the method should accept an Object, which is predicates. Consider the following sample:

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);
        }});
    }
}

The point is that the object being passed to the remove method is responsible for defining the equals method. Building predicates becomes very simple this way.

Hosam Aly
  • 41,555
  • 36
  • 141
  • 182
  • Investor? (Filler filler filler) – Matt R Sep 08 '09 at 19:53
  • The last is not a great idea: how is list.remove() implemented -- developer.equals(yourAnonymousObject) or yourAnonymousObject.equals(developer)? – Matt R Sep 08 '09 at 19:59
  • 3
    The list is implemented as `yourObject.equals(developer)`, as documented in the Collections API: http://java.sun.com/javase/6/docs/api/java/util/Collection.html#remove(java.lang.Object)] – Hosam Aly Sep 09 '09 at 04:36
  • Thanks @Software Monkey. I fixed it. The `||` is intended as per the comment above the operation. – Hosam Aly Nov 24 '10 at 21:23
  • 13
    This seems like a abuse to me – RAY Dec 24 '10 at 05:18
  • @RAY: Why? I think it's the main reason the collection framework designers used the idiom of using the parameter's `equals` method. – Hosam Aly Dec 24 '10 at 09:06
  • 7
    It is abuse since your predicate object breaks the contract of the `equals` method, namely symmetry. The remove method is only bound to its specification as long as your objects are fulfilling the equals/hashCode spec, so any implementation would be free to do the comparison the other way around. Also, your predicate object does not implement the `.hashCode()` method (can't implement consistently to equals), thus the remove call will never work on a Hash-based collection (like HashSet or HashMap.keys()). That it works with ArrayList is pure luck. – Paŭlo Ebermann Feb 28 '11 at 23:43
  • `ArrayList list = new ArrayList();` would not compile. Do you mean `List extends Person>`, `List`, or `ArrayList()` as the variable type? – Paŭlo Ebermann Feb 28 '11 at 23:46
  • Thanks @Paŭlo for the correction. I have updated the code using your suggestion of ` extends Person>`. Sorry for that mistake. – Hosam Aly Mar 01 '11 at 16:45
  • @Paŭlo: It is not abuse, and luck has nothing to do with it. The documentation of `java.util.Collection.remove(Object)` explicitly requires this behavior. Even more, the `remove` method does not require an object of the same generic type as the collection, because what it is essentially receiving is a single function (`delegate` in C# terms, of function pointer in C++). The object being passed does not need to adhere to the contract of the `equals` method. – Hosam Aly Mar 01 '11 at 17:02
  • However, the case with hash-based collections (i.e. `HashMap` and `HashSet`) is different, because these classes explicitly check for the hash code of the object being compared, even before checking whether the objects are equal. – Hosam Aly Mar 01 '11 at 17:02
  • 3
    (I'm not discussing the generic type question - this was anwered before -, only your use of equals for predicates here.) Of course HashMap and HashSet are checking for the hash code, and TreeSet/Map are using the ordering of the elements. Still, they fully implement `Collection.remove`, without breaking its contract (if the ordering is consistent to equals). And a varied ArrayList (or AbstractCollection, I think) with the equals call turned around would still correctly implement the contract - it is your fault if it does not work as intended, since you are breaking the `equals` contract. – Paŭlo Ebermann Mar 01 '11 at 17:33
  • <:SHOCKED:> You overrided equals only to get one object removed. +1 for that – Marcos Vasconcelos Oct 01 '15 at 20:40
  • @MarcosVasconcelos It doesn't have to be `remove`. It could be `indexOf` for example. – Hosam Aly Oct 02 '15 at 20:07
  • @HosamAly really impressive use of override, thanks pointing that. – Marcos Vasconcelos Oct 02 '15 at 20:26
  • There is no guaranty that this hack works. While the documentation of `Collection.remove` mentions `o.equals(e)`, [the documentation of `Object.equals`](https://docs.oracle.com/javase/6/docs/api/java/lang/Object.html#equals(java.lang.Object)) clearly says that implementations *must be symmetric*, there would be nothing wrong with a collection calling `e.equals(o)` instead. It’s this asymmetric `equals` implementation that is broken… – Holger Jul 24 '19 at 20:06
5

Assume one has a collection of Cat, and some object references of types Animal, Cat, SiameseCat, and Dog. Asking the collection whether it contains the object referred to by the Cat or SiameseCat reference seems reasonable. Asking whether it contains the object referred to by the Animal reference may seem dodgy, but it's still perfectly reasonable. The object in question might, after all, be a Cat, and might appear in the collection.

Further, even if the object happens to be something other than a Cat, there's no problem saying whether it appears in the collection--simply answer "no, it doesn't". A "lookup-style" collection of some type should be able to meaningfully accept reference of any supertype and determine whether the object exists within the collection. If the passed-in object reference is of an unrelated type, there's no way the collection could possibly contain it, so the query is in some sense not meaningful (it will always answer "no"). Nonetheless, since there isn't any way to restrict parameters to being subtypes or supertypes, it's most practical to simply accept any type and answer "no" for any objects whose type is unrelated to that of the collection.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • 1
    "If the passed-in object reference is of an unrelated type, there's no way the collection could possibly contain it" Wrong. It only has to contain something equal to it; and objects of different classes can be equal. – newacct May 29 '12 at 08:24
  • "may seem dodgy, but it's still perfectly reasonable" Is it? Consider a world where an object of one type cannot always be checked for equality with an object of another type, because what type it can be equal to is parameterized (similar to how `Comparable` is parameterized for the types you can compare with). Then it would not be reasonable to allow people to pass something of an unrelated type. – newacct May 29 '12 at 08:31
  • @newacct: There is a fundamental difference between magnitude comparison and equality comparison: given objects `A` and `B` of one type, and `X` and `Y` of another, such that `A`>`B`, and `X`>`Y`. Either `A`>`Y` and `Y`<`A`, or `X`>`B` and `B`<`X`. Those relationships can only exist if the magnitude comparisons know about both types. By contrast, an object's equality comparison method can simply declare itself unequal to anything of any other type, without having to know anything whatsoever about the other type in question. An object of type `Cat` may have no idea whether it's... – supercat May 29 '12 at 15:15
  • ..."greater than" or "less than" an object of type `FordMustang`, but it should have no difficulty saying whether it's equal to such an object (the answer, obviously, being "no"). – supercat May 29 '12 at 15:16
4

I always figured this was because remove() has no reason to care what type of object you give it. It's easy enough, regardless, to check if that object is one of the ones the Collection contains, since it can call equals() on anything. It's necessary to check type on add() to ensure that it only contains objects of that type.

ColinD
  • 108,630
  • 30
  • 201
  • 202
2

It was a compromise. Both approaches have their advantage:

  • remove(Object o)
    • is more flexible. For example it allows to iterate through a list of numbers and remove them from a list of longs.
    • code that uses this flexibility can be more easily generified
  • remove(E e) brings more type safety to what most programs want to do by detecting subtle bugs at compile time, like mistakenly trying to remove an integer from a list of shorts.

Backwards compatibility was always a major goal when evolving the Java API, therefore remove(Object o) was chosen because it made generifying existing code easier. If backwards compatibility had NOT been an issue, I'm guessing the designers would have chosen remove(E e).

Stefan Feuerhahn
  • 1,564
  • 1
  • 14
  • 22
-1

Remove is not a generic method so that existing code using a non-generic collection will still compile and still have the same behavior.

See http://www.ibm.com/developerworks/java/library/j-jtp01255.html for details.

Edit: A commenter asks why the add method is generic. [...removed my explanation...] Second commenter answered the question from firebird84 much better than me.

Jeff C
  • 445
  • 4
  • 10
  • 2
    Then why is the add method generic? – Bob Gettys Sep 19 '08 at 19:35
  • @firebird84 remove(Object) ignores objects of the wrong type, but remove(E) would cause a compile error. That would change the behavior. – noah Sep 19 '08 at 19:49
  • :shrug: -- runtime behavior is *not* changed; compile error is not *runtime* behavior. The add method's "behavior" changes in this way. – Jason S Oct 15 '09 at 13:12
-2

Another reason is because of interfaces. Here is an example to show it :

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 */
}
Patrick
  • 831
  • 11
  • 25
  • You're showing something you can get away with because `remove()` is not covariant. The question, though, is whether this *should* be allowed. `ArrayList#remove()` works by way of value equality, not reference equality. Why would you expect that a `B` would be equal to an `A`? In your example, it *can* be, but it's an odd expectation. I'd rather see you supply a `MyClass` argument here. – seh Nov 26 '15 at 13:50
-3

Because it would break existing (pre-Java5) code. e.g.,

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

Now you might say the above code is wrong, but suppose that o came from a heterogeneous set of objects (i.e., it contained strings, number, objects, etc.). You want to remove all the matches, which was legal because remove would just ignore the non-strings because they were non-equal. But if you make it remove(String o), that no longer works.

noah
  • 21,289
  • 17
  • 64
  • 88
  • 4
    If I instantiate a List I would expect to only be able to call List.remove(someString); If I need to support backward compatibility, I would use a raw List -- List> then I can call list.remove(someObject), no? – Chris Mazzola Sep 22 '08 at 17:25
  • 5
    If you replace "remove" with "add" then that code is just as broken by what was actually done in Java 5. – DJClayworth Apr 20 '09 at 20:13