Should remove(Object) be remove(? super E)

180 views Asked by At

In this answer, I tried to explain why the Collection method add has the signature add(E) while remove is remove(Object). I came up with the idea that the correct signature should be

public boolean remove(? super E element)

And since this is invalid syntax in Java, they had to stick to Object, which just happens to be super E (supertype of E) for any E. The following code explains why this makes sense:

List<String> strings = new ArrayList();
strings.add("abc");

Object o = "abc"; // runtime type is String
strings.remove(o);

Since the runtime type is String, this succeeds. If the signature were remove(E), this would cause an error at compile-time but not at runtime, which makes no sense. However, the following should raise an error at compile time, because the operation is bound to fail because of its types, which are known at compile-time:

strings.remove(1);

The remove takes an Integer as an argument, which is not super String, which means it could never actually remove anything from the collection.

If the remove method was defined with the parameter type ? super E, situations like the above could be detected by the compiler.

Question:

Am I correct with my theory that remove should have a contravariant ? super E parameter instead of Object, so that type mismatches as shown in the above example can be filtered out by the compiler? And is it correct that the creators of the Java Collections Framework chose to use Object instead of ? super E because ? super E would cause a syntax error, and instead of complicating the generic system they simply agreed to use Object instead of super?

Also, should the signature for removeAll be

public boolean removeAll(Collection<? super E> collection)

Note that I do not want to know why the signature is not remove(E), which is asked and explained in this question. I want to know if remove should be contravariant (remove(? super E)), while remove(E) represents covariance.


One example where this does not work would be the following:

List<Number> nums = new ArrayList();
nums.add(1);
nums.remove(1); // fails here - Integer is not super Number

Rethinking my signature, it should actually allow sub- and supertypes of E.

4

There are 4 answers

2
blgt On BEST ANSWER

This is a faulty assumption:

because the operation is bound to fail because of its types, which are known at compile-time

It's the same reasoning that .equals accepts an object: objects don't necessarily need to have the same class in order to be equal. Consider this example with different subtypes of List, as pointed out in the question @Joe linked:

List<ArrayList<?>> arrayLists = new ArrayList<>();
arrayLists.add(new ArrayList<>());

LinkedList<?> emptyLinkedList = new LinkedList<>();
arrayLists.remove(emptyLinkedList); // removes the empty ArrayList and returns true

This would not be possible with the signature you proposed.

3
Sergey Kalinichenko On

I think that designers of the collections framework made a decision to keep remove untyped, because it is a valid solution that lets you keep a post-condition without introducing a pre-condition or compromising type safety.

The post-condition of a c.remove(x) is that after the call x is not present in c. Method signature remove(Object) lets you pass any object or null, with no further checks. Method signature ? super E, on the other hand, introduces a pre-condition on the type of x, requiring it to be related to E.

Each pre-condition that you introduce in an API makes your API harder to use. If removing a pre-condition lets you keep all your post-conditions, it is a good idea to remove the pre-condition.

Note that removing an object of a wrong type is not necessarily an error. Here is a small example:

class Segregator {
    private final Set<Integer> ints = ...
    private final Set<String> strings = ...
    public void addAll(List<Object> data) {
        for (Object o : data) {
            if (o instanceof Integer) {
                ints.add((Integer)o);
            }
            if (o instanceof String) {
                strings.add((String)o);
            }
        }
    }
    // Here is the method that becomes easier to write:
    public void removeAll(List<Object> data) {
        for (Object o : data) {
            ints.remove(o);
            strings.remove(o);
        }
    }
}

Note how removeAll method's code is simpler than the code of addAll, because remove does not care about the type of the object that you pass to it.

3
André Stannek On

In your question you already explained why it can't (or shouldn't) be remove(E).

But there is also a reason why it shouldn't be remove(? super E). Imagine some piece of code where you have an object of unknown type. You still might want to try to remove that object from that list. Consider this code:

public void removeFromList(Object o, Collection<String> col) {
    col.remove(o);
}

Now your argument was, that remove(? super E) is more typesafe way. But I say it doesn't have to be. Look at the Javadoc of remove(). It says:

More formally, removes an element e such that (o==null ? e==null : o.equals(e)), if this collection contains one or more such elements.

So all the preconditions the parameter has to match is that you can use == and equals() on it, which is the case with Object. This still enables you to try to remove an Integer from a Collection<String>. It just wouldn't do anything.

10
Louis Wasserman On

remove(? super E) is entirely equivalent to remove(Object), because Object is itself a supertype of E, and all objects extend Object.