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

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.


You are not going to have any problems in runtime because generic types are erased from binaries -- see: https://docs.oracle.com/javase/tutorial/java/generics/erasure.html

You are also not going to have any problems on compile time for Collection<?> and Collection are equivalent. -- see: https://docs.oracle.com/javase/tutorial/extra/generics/legacy.html

Tags:

Java

Generics