14

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?
lexicore
  • 42,748
  • 17
  • 132
  • 221
  • Possible duplicate of [Difference between an unbound wildcard and a raw type](https://stackoverflow.com/questions/14242174/difference-between-an-unbound-wildcard-and-a-raw-type) – Nikolas Charalambidis Jun 02 '18 at 15:07
  • 3
    @Nikolas I don't think it's a duplicate. – lexicore Jun 02 '18 at 15:16
  • Also generics were added back in JDK1.5, ~14 years ago. Microsoft doesn’t support a 12 year old OS filled with bugs and security holes, likewise you shouldn’t support a 14 year old programming language with the same problems. – vandench Jun 02 '18 at 16:24
  • 2
    @vandench It's not about supporting a 14 year old programming language. It's about estimating how much of a breaking change would switching from `Collection` to `Collection>` be. The age of generics is largely irrelevant. It is my fault that I did not update the public API earlier. Frankly, it just never was a priority. – lexicore Jun 03 '18 at 18:09

3 Answers3

11

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.

Andy Turner
  • 137,514
  • 11
  • 162
  • 243
  • I can't seem to produce code that adds `Integer.valueOf(1)` to a `Collection>` without producing a *compile-time* error. And that would seem to make sense to me. Do you have a working example of a snippet of code that does so, compiles, and then fails at runtime as you say, because I can't seem to produce such behavior? – Silvio Mayolo Jun 02 '18 at 18:15
  • For reference, [here](https://gist.github.com/Mercerenies/bf7632b6882a188136125f2975b2d4ad) is my attempt at reproducing the changes and behavior you described. The gist I've just linked does not compile on my machine, as well I would think it shouldn't. – Silvio Mayolo Jun 02 '18 at 18:17
  • 1
    @SilvioMayolo you can't. You can add it to a raw `Collection` though, and changing the signature of the parent method to be `Collection>` wouldn't require the subclasses to add the `>` too. – Andy Turner Jun 02 '18 at 18:20
  • 1
    Ahhhhh okay. Thank you for the clarification! That's a truly terrible quirk of the Java language then, but that makes sense as an explanation at least. :( – Silvio Mayolo Jun 02 '18 at 18:21
  • Interfaces in question are basically strategies for basic things like "convert to string", "check equals", "calculate hash code". They are not supposed to change the passed values, so I have no problems if it causes problems in case they (clients) do it. – lexicore Jun 02 '18 at 19:59
  • @lexicore I agree with your sentiment, but I wonder if you are being unfair to your users: such code was allowed by your API, and to change it quietly would be pernicious. I don't know what your release notes are like, but I would strongly emphasize the potential for a breaking change, how to check if you are affected, perhaps provide tooling to check etc. – Andy Turner Jun 03 '18 at 09:26
  • 1
    @AndyTurner I do have *Backwards compatibility* sections in release notes. This is actually the reason for the question. – lexicore Jun 03 '18 at 18:05
5

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

Kalecser
  • 1,143
  • 12
  • 15
1
  1. Collection is a collection of any type (ie it can contains any type of element: Integer, String, Object...)
  2. Collection<?> is a collection of some specific type.

The client code will not have any compilation error or runtime errors. since when you pass a collection to Collection<?> it is treated as Collection<Object>

Mạnh Quyết Nguyễn
  • 17,677
  • 1
  • 23
  • 51