Nested wildcards
Asked Answered
B

2

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.

Bucksaw answered 12/1, 2015 at 12:37 Comment(12)
What's the question here?Copyedit
return someMap; //compilation failsBucksaw
possible duplicate of Multiple wildcards on a generic methods makes Java compiler (and me!) very confusedRein
Map<?, ? extends Map<?, ?>>Rein
Also relevant: #2745765Rein
@Zeeker, yea, it works, but what's wrong with my example?Bucksaw
@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 (https://mcmap.net/q/295959/-multiple-wildcards-on-a-generic-methods-makes-java-compiler-and-me-very-confused) to the first one you highlight is excellent and should answer the OPRayshell
My question is much shorter and it's about Map. Also i didn't find the answer in that 'List posts'Bucksaw
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<Map<Integer, String>> is NOT (captureable by) a Map<?,Map<?,?>> 2. A Map<?,Map<Integer,String>> IS (captureable by) a List<?,? extends Map<?, ?>>. As also referenced in that answer, this is due to the rules of capture conversion docs.oracle.com/javase/specs/jls/se5.0/html/…, particularly Capture conversion is not applied recursively. Non-intuitive, but clearly defined.Rayshell
Now clear, thank you @J Richard Snape.Bucksaw
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.Vivien
@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.Censor
C
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.

Censor answered 12/1, 2015 at 18:48 Comment(5)
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.Bucksaw
Well, if you want to have a Map whose values are Strings you need a Map<…,String>. But you can assign that map to Map<…,? extends Object> as the same map guarantees that the values are Objects because Strings are Objects. But you can’t put arbitrary Objects 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).Censor
Maybe this document help a bit.Censor
@Censor ... and discover arbitrary maps not being.... how is that different to Map<Integer, String> 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?Discant
@Discant 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?Censor
D
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.

Discant answered 30/10, 2019 at 18:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.