简体   繁体   中英

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

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.

I'll change First interface to Box interface

Box<?> uknownBox gray box with something in it

Box<Apple> appleBox box with apple

List<Box<Apple>> appleBoxList many boxes with apples

List<Box<?>> uknownBoxList many unknown gray boxes

appleBoxList.add(new Box<Orange>()) - can't add box with oranges to list of apple boxes

unknownBoxList.add(new Box<?>()) - we don't know what's in that gray boxes, adding one more unknown gray box changes nothing

unknownBoxList.add(new Box<Orange>()) - same rules when you add specific boxes
unknownBoxList.add(new Box<Apple>()) - since you are not allowed to 'open' them

unknownBoxList = appleBoxList this does not compile to prevent adding gray (possibly not apple) boxes to apple box list. because of that previous operation is legal.

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.

I believe that the type ? in generics is a specific unknown type.

This is slightly inaccurate. Yes, a wildcard type stands for an unknown type, but may stand for different types at different times:

List<?> list = new ArrayList<String>();
list = new ArrayList<Integer>();

The only invariant is that an expression whose type contains a wildcard will always yield a value whose type conforms to that wildcard. Since every value has a type that is not just a wildcard, one could say that wildcard stands for a (more) "specific" type at any time.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM