当涉及类型参数时,List<List<?>> 无法赋值给 List<List<?>>。

12
在以下代码中,调用了get()并将其结果分配给一个变量,该变量的类型为List<List<?>>get()返回List<List<T>>,并且是在类型参数T设置为?的实例上调用的,因此应该适合。
import java.util.List;

class Test {
    void foo(NestedListProducer<?> test) {
        List<List<?>> a = test.get();
    }

    interface NestedListProducer<T> {
        List<List<T>> get();
    }
}

但是无论是IntelliJ IDEA还是Oracle的javac版本1.7.0_45都将我的代码拒绝为无效。这是'javac'的错误信息:

java: incompatible types
  required: java.util.List<java.util.List<?>>
  found:    java.util.List<java.util.List<capture#1 of ?>>

为什么这段代码是无效的,即如果允许它会发生什么问题?


例如,当你收到一个 List<Number> 时,代码可以接受一个 List<String>... 所以,是的,你不应该将变量声明为 List<?> - Luiggi Mendoza
@Luiggi 我的代码中没有使用 List<Number> 以外的任何东西。List<?> 无法转换为 List<Number>,所以我不认为有什么问题。 - Feuermurmel
1
@ValentinRuano 实际上,它也可以使用那个。 - Rohit Jain
2
这个可以工作:List<String> a = null; List<?> b = a; 但是这个不行:List<List<String>> a = null; List<List<?>> b = a; - ZhongYu
1
嵌套通配符与顶级通配符的含义不同 - 它们字面上代表“任何类型”,而不是“某些特定未知类型”。在我看来,它们应该有一个不同的符号,例如 List<List<*>> - Paul Bellora
显示剩余5条评论
4个回答

7

?是通配符,表示任何类型。一个?不能和另一个?相同,因为另一个?可以是任何其他类型,并且它们不匹配。您必须使用泛型来说明类型是相同的:

// Make this generic
<A> void foo(NestedListProducer<A> test) {
    List<List<A>> a = test.get();
}

3
为什么这段代码是可行的:void foo(List<List<?>> arg) { List<List<?>> a = arg; }?如果接受我问题中的代码会允许什么可能破坏类型系统的事情发生? - Feuermurmel
1
@Feuermurmel 这是可能的,因为在这段代码中没有发生捕获转换,而在你的问题示例中,T正在被捕获。 - Marko Topolnik
我认为这个答案给出了一个相当好的解释。 - Marko Topolnik
@MarkoTopolnik 我认为这是一个相当糟糕的解释。 - ZhongYu

4
List<List<T>>代表一个列表,你可以从中读取List<T>或写入新的List<T>,同样地,List<List<?>>代表一个列表,你可以从中读取List<?>或写入新的List<T>。关于?奇怪的事情是你可以将任何类型S的列表转换为List<?>。例如,你可以写成:

void foo(List<String> a, List<Integer> b, List<List<?>> out) {
  List<?> unknownA = a;
  List<?> unknownB = b;
  out.add(a);
  out.add(b);
}

如果你想将一个 List<List<T>> 转换成一个 List<List<?>>,那么你可以使用一个 List<List<PeanutButter>> 来调用 foo 方法,之后再向其中添加一些字符串或整数的列表。
通常人们会遇到这个问题是因为他们想表达的是希望有一个类型不确定的子集合集合。如果这就是你想要的,你可以将类型从 List<List<?>> 改为 List<? extends List<?>>,这样就表达了一个我可以读取但不能写入的子列表集合。将一个 List<List<T>> 转换成一个 List<? extends List<?>> 是合法的。

这个答案解释了在不同的List<List<?>>类型实例之间进行强制转换的问题所在,而Rohit的答案则解释了类型系统如何防止代码编译。 - Feuermurmel

4
您好像对编译器如何处理 List<?>List<List<?>> 有些困惑。 List<?> 是一个包含 未知 类型的某种类型的 List,而 List<List<?>> 是一个包含不同类型的 ListList (这些类型也是未知的)。
因此,在 List<?> 中,通配符 ? 代表单个未知类型,所以编译器会将其捕获为该单个未知类型的占位符。而在 List<List<?>> 中,通配符 ? 表示不同的未知类型。 编译器不会捕获这些类型,因为不同未知类型不能用单个占位符来表示。
现在考虑您的原始示例:
void foo(NestedListProducer<?> test) {
    List<List<?>> a = test.get();
}

在这种情况下,编译器将捕获NestedListProducer?以在编译时创建匿名类型参数,并将创建一个类似于辅助方法的东西:
<CAP#1 of ?> void foo_2(NestedListProducer<CAP#1 of ?> test) {
    List<List<?>> a = test.get();
}

(注意:它不会捕获List<List<?>>中的?,因此它将保留原样)。
现在,在这种情况下test.get()的返回类型将为List<List<CAP#1 of ?>>。它不能从List<List<?>>转换为分配捕获,因此无法将其分配给它。因此它无法编译通过。
因此解决方法是自己添加类型参数,如已建议的那样:
<T> void foo(NestedListProducer<T> test) {
    List<List<T>> a = test.get();
}

评论中的查询:

现在如你在评论中所问,为什么以下代码可以工作?

void foo(List<List<?>> arg) { 
    List<List<?>> a = arg; 
}

从上述解释中,您可以猜测在形式参数中的 List<List<?>> 中的通配符 ? 将不会被捕获。因此,这个赋值实际上是从一个 List<List<?>> 到另一个 List<List<?>>,这是有效的。这里没有 CAP#1 of ?


0
你可以使用这个技巧(通配符捕获)
void foo(NestedListProducer<?> test) {
    foo2(test);
}

<T> void foo2(NestedListProducer<T> test) {
    List<List<T>> a = test.get();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接