Nested wildcards
It is important to understand the implication of the wildcard types.
You already understood that you can assign your Map<Integer, Map<Integer, String>>
to Map<?, ?>
as Map<?, ?>
implies arbitrary types, unknown to whoever might have a reference of the declared type Map<?, ?>
. So you can assign any map to Map<?, ?>
.
In contrast, if you have a Map<?, Map<?, ?>>
it has an unknown key type but the value type is not unknown. It’s Map<?,?>
the type, recall the information above, that can be assigned with any map.
So, the following code is legal:
Map<?, Map<?, ?>> map=new HashMap<>();
map.put(null, Collections.<String,String>singletonMap("foo", "bar"));
map.put(null, Collections.<Double,Integer>singletonMap(42.0, 1000));
map.put(null, Collections.<Object,Boolean>singletonMap(false, true));
Here, we are putting a null
key as we can’t put
anything else for keys but arbitrary typed maps as values as that’s what a value type of Map<?, ?>
implies: can be assigned from arbitrary maps. Note that by iterating over the entries we can also set other entries having non-null
keys to arbitrary maps then.
So I’m quite sure that you don’t want to assign your Map<Integer, Map<Integer, String>>
to a Map<?, Map<?, ?>>
and discover arbitrary maps not being Map<Integer, String>
as values afterwards and that you are quite happy that the compiler doesn’t allow this.
What you actually want to do is to assign your map to a type which has both, key and value type, unknown but still telling that your values are maps:
Map<Integer, Map<Integer, String>> someMap = new HashMap<>();
Map<?, ? extends Map<?, ?>> map=someMap;
In the generic type system Map<Integer, String>
is a sub-type of Map<?, ?>
so you can assign it to Map<?, ?>
as well as ? extends Map<?, ?>
. This sub-type relationship is not different than the relationship of String
to Object
. You can assign any String
to a variable of type Object
but if you have a Map<?,String>
you can’t assign it to Map<?,Object>
but only to Map<?, ? extends Object>
for the same reason: the map shall continue to contain String
s as values rather than receiving arbitrary objects.
Note that you can workaround this limitation. You can say:
Map<Integer, Map<Integer, String>> someMap = new HashMap<>();
Map<?, Map<?, ?>> map=Collections.unmodifiableMap(someMap);
Since the map returned by unmodifiableMap
does not allow any modifications, it allows widening the key and value types. The contained values are of the specified type (i.e. Map<?, ?>
) when you query the map, but attempts to put in arbitrary map values, while not rejected by the compiler, will be rejected at runtime.
The short answer is that generics are invariant, so this will not work.
The long answer takes a while to understand. It starts simple:
Dog woof = new Dog();
Animal animal = woof;
Works just fine, since a Dog
is an Animal
. On the other hand:
List< Animal > fauna = new ArrayList<>();
List< Dog > dogs = new ArrayList<>();
fauna = dogs;
will fail to compile, because generics are invariant; basically a List<Dog>
is not a List<Animal>
.
How come? Well if the assignment would have been possible, what is stopping you from doing:
fauna.add(new Cat());
dogs.get(0); // what is this now?
A compiler could be smarter here, actually. What if your Lists are immutable? After creation, you can't put anything into them. In such a case, fauna = dogs
, should be allowed, but java does not do this (scala does), even with the newly added Immutable collections in java-9.
When Lists are immutable, they are said to be Producers
, meaning they don't take the generic type as input. For example:
interface Sink<T> {
T nextElement();
}
Since Sink
never takes T
as input, it is a Producer
of T
s (not a Consumer
), thus it could be possible to say:
Sink<Object> objects ...
Sink<String> strings ...
objects = strings;
Since Sink
has no option to add elements, we can't break anything, but java does not care and prohibits this. kotlinc
(just like scalac
) allows it.
In java this shortcoming is solved with a "bounded type":
List<? extends Animal> animals = new ArrayList<>();
animals = dogs;
The good thing is that you still can't do: animals.add(new Cat())
. You know exactly what that list holds - some types of Animals, so when you read from it, you always, for a fact, know that you will get an Animal
. But because List<? extends Animal>
is assignable to List<Dog>
for example, addition is prohibited, otherwise:
animals.add(new Cat()); // if this worked
dogs.get(0); // what is this now?
This "addition is prohibited" is not exactly correct, since it's always possible to do:
private static <T> void topLevelCapture(List<T> list) {
T t = list.get(0);
list.add(t);
}
topLevelCapture(animals);
Why this works is explained here, what matters is that this does not break anything.
What if you wanted to say that you have a group of animals, like a List<List...>
? May be the first thing you want to do is List<List<Animal>>
:
List<List<Animal>> groups = new ArrayList<>();
List<List<Dog>> dogs = new ArrayList<>();
groups = dogs;
this would obviously not work. But what if we added bounded types?
List<List<? extends Animal>> groups = new ArrayList<>();
List<List<Dog>> dogs = new ArrayList<>();
groups = dogs;
even if List<Dog>
is a List<? extends Animal>
the generics of these are not (generics are invariant). Again, if this would have been allowed, you could do:
groups.add(<list of cats>);
dogs.get(0); // obvious problems
The only way to make it work would be via:
List<? extends List<? extends Animal>> groups = new ArrayList<>();
List<List<Dog>> dogs = new ArrayList<>();
groups = dogs;
that is, we found a super type of List<Dog>
in List<? extends Animal>
and we also need the bounded type ? extends List...
so that the outer lists themselves are assignable.
This huge intro was to show that:
Map<Integer, Map<Integer, String>> map = new HashMap<>();
Map<?, ?> broader = new HashMap<>();
broader = map;
would compile because there are no restrictions what-so-ever here, the broader
map basically is a map "of anything".
If you read what I had to say above, you probably know why this is not allowed:
Map<Integer, Map<Integer, String>> map = new HashMap<>();
Map<?, Map<?, ?>> lessBroader = new HashMap<>();
lessBroader = map;
if it would have been allowed, you could do:
Map<Double, Float> newMap = new HashMap<>(); // this is a Map<?, ?> after all
lessBroader.add(12, newMap);
map.get(12); // hmm...
If maps were immutable and the compiler would care, this could have been avoided and the assignment could have made to work just fine.