9

Found fact about unbounded wildcards that is annoying me. For example:

public class Test {

      private static final Map<Integer, Map<Integer, String>> someMap = new HashMap<>();

      public static void main(String[] args) {
         getSomeMap();
      }

      static Map<?, Map<?, ?>> getSomeMap() {
         return someMap;  //compilation fails
      }
}

It fails, although works with Map<?, ?> or Map<?, Map<Integer, String>> return type.

Could someone tell me the exact reason? Thanks in advance.


Update

Seems that i understood and the simplest explanation for this question(omitting all these sophisticated rules), in my opinion, is the last note in Capture Conversion(link): Capture conversion is not applied recursively.

samabcde
  • 6,988
  • 2
  • 25
  • 41
Dmytro
  • 1,850
  • 3
  • 14
  • 20
  • What's the question here? – Zoltán Jan 12 '15 at 12:38
  • `return someMap; //compilation fails` – Dmytro Jan 12 '15 at 12:39
  • 2
    possible duplicate of [Multiple wildcards on a generic methods makes Java compiler (and me!) very confused](http://stackoverflow.com/questions/3546745/multiple-wildcards-on-a-generic-methods-makes-java-compiler-and-me-very-confu) – Sascha Wolf Jan 12 '15 at 12:43
  • `Map, ? extends Map, ?>>` – Sascha Wolf Jan 12 '15 at 12:45
  • 1
    Also relevant: http://stackoverflow.com/questions/2745265/is-listdog-a-subclass-of-listanimal-why-arent-javas-generics-implicitly-p – Sascha Wolf Jan 12 '15 at 12:48
  • @Zeeker, yea, it works, but what's wrong with my example? – Dmytro Jan 12 '15 at 12:50
  • @Zeeker comments are very useful - I'd recommend putting into an answer. My own opinion, the question isn't exactly a duplicate, but this answer (http://stackoverflow.com/a/3547372/838992) to the first one you highlight is excellent and should answer the OP – J Richard Snape Jan 12 '15 at 12:50
  • My question is much shorter and it's about `Map`. Also i didn't find the answer in that 'List posts' – Dmytro Jan 12 '15 at 12:54
  • 2
    What @Zeeker is highlighting is that your question is not unique to Map - it is any nested generic wildcard. As per the answer I linked in my comment above (with Map substituted for List) : 1. A Map> is NOT (captureable by) a Map,Map,?>> 2. A Map,Map> IS (captureable by) a List,? extends Map, ?>>. As also referenced in that answer, this is due to the rules of capture conversion http://docs.oracle.com/javase/specs/jls/se5.0/html/conversions.html#5.1.10, particularly Capture conversion is not applied recursively. Non-intuitive, but clearly defined. – J Richard Snape Jan 12 '15 at 13:34
  • Now clear, thank you @J Richard Snape. – Dmytro Jan 12 '15 at 14:51
  • I agree with the Java decision not to apply the capture conversion recursively. Stuff like this is often allowed in C++ and makes my head hurt. – Michael Shopsin Jan 12 '15 at 15:24
  • 1
    @J Richard Snape: it might be non-intuitive if you try to understand “Capture Conversion” but it will be intuitive if you [consider the implications](http://stackoverflow.com/a/27908749/2711488). – Holger Jan 12 '15 at 18:51

2 Answers2

7

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 Strings 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.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Thanks, but i have one question about this fragment and about similar above(about nested `Maps`): _"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 Strings as values rather than receiving arbitrary objects"._ But in case of `Map, ? extends Object>` we still receiving arbitrary objects. `get` still give us `Object` but not `String`. – Dmytro Jan 12 '15 at 21:50
  • 2
    Well, if you want to have a `Map` whose values are `String`s you need a `Map<…,String>`. But you can assign that map to `Map<…,? extends Object>` as the same map guarantees that the values are `Object`s because `String`s are `Object`s. But you can’t *put* arbitrary `Object`s into the map. That’s what `? extends X` means, you are guaranteed that the instances of that type are of type `X` upon retrieving because it might be `X` or a subtype of `X`, but you can’t pass instances to it because you don’t know whether it is `X` or a subclass of `X` (and which). – Holger Jan 13 '15 at 08:42
  • Maybe [this document](http://www.oracle.com/technetwork/server-storage/ts-6623-159104.pdf) help a bit. – Holger Jan 13 '15 at 08:43
  • @Holger _... and discover arbitrary maps not being..._. how is that different to `Map map = new HashMap<>(); map.put(1, "1"); Map, ?> second = map; Double d = (Double) second.get(1);` you will still "discover" this being wrong at runtime. What I am missing? – Eugene Oct 25 '19 at 17:41
  • @Eugene a type cast is an operation that in case of a narrowing reference conversion is known to potentially throw a `ClassCastException`. You spell it and you get it. Generics, on the other hand, are supposed to guaranty that for code which is free of manually written type casts and has no warnings ignored or suppressed by you, will never throw a `ClassCastException`. To achieve this, you have to maintain the generic type signature consistently throughout your code. Of course, you can drop the type information and perform potentially breaking casts, but why would you ever want that? – Holger Oct 28 '19 at 10:27
4

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 Ts (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.

Eugene
  • 117,005
  • 15
  • 201
  • 306