嵌套通配符

Nested wildcards

发现关于无限通配符的事实让我很恼火。例如:

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
      }
}

它失败了,尽管适用于 Map<?, ?>Map<?, Map<Integer, String>> return 类型。

谁能告诉我具体原因?提前致谢。


更新

看来我明白了,这个问题最简单的解释(省略所有这些复杂的规则),在我看来,是捕获转换中的最后一个注释(link):Capture conversion is not applied recursively.

了解通配符类型的含义很重要。

您已经了解可以将 Map<Integer, Map<Integer, String>> 分配给 Map<?, ?>,因为 Map<?, ?> 表示任意类型,可能引用声明类型 Map<?, ?> 的任何人都不知道.因此,您可以将 any 映射分配给 Map<?, ?>.

相反,如果您有一个 Map<?, Map<?, ?>>,它的键类型未知,但值类型 不是 未知。是 Map<?,?> 类型,回想上面的信息,可以用 any 映射分配。

所以,下面的代码是合法的:

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));

在这里,我们放置了一个 null 键,因为我们不能 put 任何其他键,但任意类型的映射作为值,因为这就是 Map<?, ?> 的值类型所暗示的: 可以从任意地图分配。请注意,通过 iterating over the entries we can also set other entries 具有指向任意映射的非 null 键。

所以我很确定你不想将你的 Map<Integer, Map<Integer, String>> 分配给 Map<?, Map<?, ?>> 然后发现任意地图不是 Map<Integer, String> 作为值然后你是很高兴编译器不允许这样做。

您真正想要做的是将您的地图分配给一个同时具有键和值类型的类型,未知但仍然表明您的值是地图:

Map<Integer, Map<Integer, String>> someMap = new HashMap<>();
Map<?, ? extends Map<?, ?>> map=someMap;

在通用类型系统中,Map<Integer, String>Map<?, ?> 的子类型,因此您可以将其分配给 Map<?, ?> 以及 ? extends Map<?, ?>。这种子类型关系与 StringObject 的关系没有区别。您可以将任何 String 分配给类型为 Object 的变量,但是如果您有一个 Map<?,String>,则不能将其分配给 Map<?,Object>,而只能分配给 Map<?, ? extends Object>同样的原因:地图应继续包含 Strings 作为值,而不是接收任意对象。

请注意,您可以解决此限制。你可以说:

Map<Integer, Map<Integer, String>> someMap = new HashMap<>();
Map<?, Map<?, ?>> map=Collections.unmodifiableMap(someMap);

由于 unmodifiableMap 返回的映射不允许任何修改,因此它允许扩展键和值类型。当您查询地图时,包含的值属于指定类型(即 Map<?, ?>),但尝试放入任意地图值,虽然不会被编译器拒绝,但会在运行时被拒绝。

简短的回答是泛型是不变的,所以这行不通。

较长的答案需要一段时间才能理解。开始很简单:

Dog    woof   = new Dog();
Animal animal = woof; 

工作正常,因为 DogAnimal。另一方面:

List< Animal > fauna   = new ArrayList<>();
List<  Dog   > dogs    = new ArrayList<>();
fauna = dogs;

将无法编译,因为泛型是不变的;基本上 List<Dog> 不是 List<Animal>.

怎么会?好吧,如果任务是可能的,是什么阻止了你做:

fauna.add(new Cat());
dogs.get(0); // what is this now?

实际上,编译器在这里可能更聪明。如果你的列表是不可变的怎么办?创建后,您不能将任何内容放入其中。在这种情况下,应该允许 fauna = dogs,但 java 不会这样做(scala 会这样做),即使 java-9 中新添加的不可变集合也是如此。

当列表不可变时,它们被称为 Producers,这意味着它们不将通用类型作为输入。例如:

interface Sink<T> {
    T nextElement();
}

由于 Sink 从不将 T 作为输入,它是 TProducer(不是 Consumer),因此它可能是可以说:

Sink<Object> objects ... 
Sink<String> strings ...
objects = strings;

由于Sink没有选择添加元素,我们不能破坏任何东西,但java不关心并禁止这样做。 kotlinc(就像 scalac)允许它。

在java中这个缺点用"bounded type"解决了:

List<? extends Animal> animals = new ArrayList<>();
animals = dogs;

好在你还是做不到:animals.add(new Cat())。您确切地知道该列表包含什么 - 某些类型的动物,因此当您阅读它时,事实上,您总是知道您将得到一个 Animal。但是因为List<? extends Animal>例如可以赋值给List<Dog>,所以禁止添加,否则:

animals.add(new Cat()); // if this worked
dogs.get(0); // what is this now?

这个 "addition is prohibited" 并不完全正确,因为总是可以这样做:

private static <T> void topLevelCapture(List<T> list) {
    T t = list.get(0);
    list.add(t);
}

topLevelCapture(animals);

解释了为什么这样做 here,重要的是这不会破坏任何东西。


如果你想说你有一个 group of animals,比如 List<List...> 怎么办?可能你想做的第一件事是 List<List<Animal>>:

List<List<Animal>> groups = new ArrayList<>();
List<List<Dog>> dogs = new ArrayList<>();
groups = dogs;

这显然行不通。但是如果我们添加有界类型呢?

List<List<? extends Animal>> groups = new ArrayList<>();
List<List<Dog>> dogs = new ArrayList<>();
groups = dogs;

even if List<Dog> is a List<? extends Animal> these generics are not (泛型是不变的) .同样,如果允许这样做,您可以这样做:

groups.add(<list of cats>);
dogs.get(0); // obvious problems

让它工作的唯一方法是通过:

 List<? extends List<? extends Animal>> groups = new ArrayList<>();
 List<List<Dog>> dogs = new ArrayList<>();
 groups = dogs;

也就是说,我们在 List<? extends Animal> 中找到了 List<Dog> 的超类型,我们还需要有界类型 ? extends List... 以便外部列表本身是可分配的。


这个巨大的介绍是为了表明:

Map<Integer, Map<Integer, String>> map = new HashMap<>();
Map<?, ?> broader = new HashMap<>();
broader = map;

会编译,因为这里没有任何限制,broader 地图基本上是一个地图 "of anything".

如果你读了我上面所说的,你可能知道为什么不允许这样做:

Map<Integer, Map<Integer, String>> map = new HashMap<>();
Map<?, Map<?, ?>> lessBroader = new HashMap<>();
lessBroader = map;

如果允许的话,你可以这样做:

Map<Double, Float> newMap = new HashMap<>(); // this is a Map<?, ?> after all
lessBroader.add(12, newMap);
map.get(12); // hmm...

如果映射是不可变的并且编译器会关心,这本可以避免并且分配可以正常工作。