Java generics impossible assignment?

Viewed 122

Every time I think I understand generics better (and can answer without compiling), I get to an example where this theory breaks. Here is a very simple example:

static void consumer(List<? super List<String>> param) {
    System.out.println(param);
}

And two invocations:

public static void main(String[] args) {
    List<String> list = List.of("123");
    consumer(list);
    consumer(List.of("123"));
}

To me, none of the invocations should compile. A String is not a supertype of List. Still, the second one compiles. But let's suppose that this happens because the compiler could infer some type here. Of course such a type does not exist and it will fail at runtime, right? Right? Nope. It just works. As such, can someone bring some sanity to my life please?

2 Answers

Ah darn!

javac  --debug=verboseResolution=all Sandbox.java

shows that consumer(List.of("123")) is compiled to:

instantiated signature: (Object)List<Object>
target-type: List<? super List<String>>

If you want a more “functional” explanation, you have to consider what param is.

By applying PECS, notice that it is thus a List that is a consumer of List<String>.

  • Compatible consumers are those which can consume List<String> or any of its parent types;
  • When applied to List, it means a list on which you can call add() with any parent type of List<String>. A List<Object> is thus compatible.

This is why consumer() can be called with a List<Object> but not a List<String> (String is not a supertype of List<String>).

Since List.of(…) can always match List<Object> at declaration time, it accepts the second call.

Note that from inside the consumer() method, you will never be able to retrieve a List<String> from param (i.e. use it as a producer). You can only add new ones into it (i.e. use it as a consumer) – and indeed, you can add a List<String> into a List<Object> (although in this particular case, List.of() produces an immutable list, so it will fail at runtime).

Related