Is it backwards-compatible to replace raw type like Collection with wildcard like Collection<?>?

lexicore :

I'm the author of a certain open-source library. One of the public interfaces has methods which use raw types like Collection, for instance:

public StringBuilder append(..., Collection value);

I get Collection is a raw type. References to generic type Collection<E> should be parameterized warnings.

I'm thinking about fixing these warnings. Implementations actually don't care about the types of the elements in collections. So I was thinking about replacing Collection<?>.

However, these methods are parts of public interface of my library. Client code may call these methods or provide own implementations of these public interfaces thus implementing these methods. I am afraid that changing Collection to Collection<?> will break client code. So here is my question.

If I change Collection -> Collection<?> in my public interfaces, may this lead to:

  • compilation errors in the client code?
  • runtime errors in the already compiled existing client code?
Andy Turner :

It is not safe at runtime to make this replacement.

I should perhaps say more precisely that this change is safe by itself; but that subsequent changes that it encourages could lead to failures.

The difference between a Collection and a Collection<?> is that you can add anything to the former, whereas you cannot add anything except literal null to the latter.

So, somebody currently overriding your method might do something like:

@Override
public StringBuilder append(Collection value) {
  value.add(Integer.valueOf(1));
  return new StringBuilder();
}

(I don't know what the method is meant to be for; this is a pathological example. It certainly looks like something they shouldn't do, but that's not the same as them not doing so).

Now, let's say this method is called like so:

ArrayList c = new ArrayList();
thing.append(c);
c.get(0).toString();

(Again, I don't know how it is used for real. Bear with me)

If you changed the method signature to append Collection<?> in the superclass, perhaps surprisingly (*), you would not need to update the subclass to be generic too: the append method above would continue to compile.

Seeing the new generic type of the parameter in the base class, you could then think that you could now make this calling code non-raw:

ArrayList<Double> c = new ArrayList<>();
thing.append(c);
c.get(0).toString();

Now, the gotcha here is how the last line is evaluated: there is an implicit cast in there. It would actually be evaluated something like:

Double d = (Double) c.get(0);
d.toString();

This is despite the fact you can invoke toString() on an Object: there is still a checkcast inserted by the compiler, to the erasure of the list element type. This would fail at runtime, because the last item in the list is an Integer, not a Double.

And the key point is that no cast is inserted for the raw-typed version. That would be evaluated like:

Object d = (Object) c.get(0);
d.toString();

This would not fail at runtime, because anything can be cast to object (in fact, there would be no cast at all; I am merely inserting it for symmetry).

This is not to say that such calling code could not exist before making the parameter Collection<?>: it certainly could, and it would already fail at runtime. But the point I am trying to highlight is that making this method parameter generic could give the mistaken impression that it is safe to convert existing raw calling code to use generics, and doing so would cause it to fail.

So... Unless you can guarantee that there is no such insertion in subclasses, or you have explicitly documented that the collection should not be modified in third method, this change would not be safe.


(*) This arises as a consequence of the definition of override-equivalence, in JLS Sec 8.4.2, where erasure is explicitly considered.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=36021&siteId=1