Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Irregularities with the (?) wildcard generic type

I believe that the type ? in generics is a specific unknown type. Which means, declaring let's say a list of that type would prevent us from adding any type of object into it.

List<?> unknownList;
unknownList.add(new Object()); // This is an error.

The compiler gives an error as expected.

But when the unknown type is a second level generics, the compiler doesn't seem to care.

class First<T> {}

List<First<?>> firstUnknownList;

// All these three work fine for some reason.
firstUnknownList.add(new First<>());
firstUnknownList.add(new First<Integer>());
firstUnknownList.add(new First<String>());

I thought probably the compiler doesn't care about generic parameter in the second level at all, but it's not the case,

List<First<Integer>> firstIntegerList;
firstIntegerList.add(new First<String>()); // This gives a compiler error as expected.

So, why does the compiler allow us adding any kind of element when only an unknown element (and hence nothing) is acceptable in the second example?

Note: Compiler Java 1.8

like image 553
Codebender Avatar asked Aug 18 '16 09:08

Codebender


2 Answers

You can add anything to a List<T> that you can store in a reference of type T:

T item = ...
List<T> list = new ArrayList<>();
list.add(item);

First<?> is a supertype of First<T>; so you can store a reference to a First<T> in a variable of type First<?>:

First<?> first = new First<String>();

So, substituting T for First<?> above:

First<?> item = new First<String>();
List<First<?>> list = new ArrayList<>();
list.add(item);

All that is happening in OP's example is that the temporary variable item is omitted:

firstUnknownList.add(new First<String>());

However, if you do this with the firstIntegerList example:

First<Integer> item = new First<String>(); // Compiler error.
List<First<Integer>> list = new ArrayList<>();
list.add(item);

it is clear why that's not allowed: you can't make the assignment of item.


It's also possible to see that you can't do anything unsafe with the contents of that list.

If you add a couple of methods to the interface:

interface First<T> {
  T producer();
  void consumer(T in);
}

Now, consider what you can do with the elements that you added to the list:

for (First<?> first : firstUnknownList) {
  // OK at compile time; OK at runtime unless the method throws an exception.
  Object obj = first.producer();

  // OK at compile time; may fail at runtime if null is not an acceptable parameter.
  first.consumer(null);

  // Compiler error - you can't have a reference to a ?.
  first.consumer(/* some maybe non-null value */);
}

so there isn't actually anything that you can really do with elements of that list that would violate type safety (provided you don't do anything willful to violate it, like using raw types). You can demonstrate that generic producer/consumer methods are similarly safe or forbidden by the compiler.

So there is no reason not to allow you to do this.

like image 73
Andy Turner Avatar answered Nov 07 '22 11:11

Andy Turner


It's all about subtype/supertype-relationships.

List<?> is a list that contains elements of an unknown (but specific) type. You never know which type exactly is contained in this list. So you may not add objects to it, because they may be of the wrong type:

List<Integer> ints = new ArrayList<Integer>();
List<?> unknowns = ints;

// It this worked, the list would contain a String....
unknowns.add("String"); 

// ... and this would crash with some ClassCastException
Integer i = ints.get(0);

It may also be clear that you can do

List<Number> numbers = null;
Integer integer = null;
numbers.add(integer); 

This works because Number is a true supertype of Integer. It does not violate the type safety to add an object of a more specific type to a list.


The key point regarding the second example is:

First<?> is a supertype of every First<T>

You can always to something like

First<Integer> fInt = null;
First<Integer> fString = null;

First<?> f = null;
f = fInt; // works
f = fString; // works

So the reason of why you can add a First<String> to a List<First<?>> is the same as why you can add an Integer to a List<Number>: The element that you want to add is of a true subtype of the elements that are expected in the list.

like image 40
Marco13 Avatar answered Nov 07 '22 10:11

Marco13