无法将List<List>转换为List<List<?>>。

28
一个原始列表可以很好地转换为List<?>。为什么一个原始列表的列表不能转换为List<?>的列表?
一个原始列表(raw list)是指没有指定类型的列表,而List<?>是指可接受任何类型参数的泛型列表。在Java中,一个原始列表可以被视为具有通配符类型List<?>的列表的子类,但一个包含原始列表的列表无法转换为包含List<?>的列表,因为通配符类型不是真正的类型,所以无法进行转换。
{   // works
    List raw = null;
    List<?> wild = raw;
}
{   // Type mismatch: cannot convert from List<List> to List<List<?>>
    List<List> raw = null;
    List<List<?>> wild = raw;
}

背景故事(以缓解xy问题):

我正在使用的API返回List<JAXBElement>。 我碰巧知道它总是List<JAXBElement<String>>。 我计划循环并构建自己的List<String>,但我试图在写List<JAXBElement> raw = api();时修复(但不是抑制)原始类型编译器警告。

我尝试过:

List<JAXBElement<?>> raw = api();
List<JAXBElement<?>> raw = (List<JAXBElement<?>>) api();

但是这些会导致类型不匹配错误。

有趣的是,这不会产生警告或错误:

for (JAXBElement<?> e : api()) {
    // ...
}

如果您知道它始终是 List<JAXBElement<String>>,请将其转换为该类型,而不是 List<?>List<JAXBElement<?>>。如果您知道所有类型,请避免使用问号。 - Mike 'Pomax' Kamermans
@djeikyb 我编辑了我的答案,请检查一下,你会发现你无法避免这个警告。 - Carlos Verdes
@Mike'Pomax'Kamermans,那个强制转换是个错误:无法将List<List>强制转换为List<List<String>>(当然不能通过中间转换为Object之类的方式)。 - djeikyb
@djeikyb FYI,for循环示例是一个不同的情况,因为它使用Iterable,将操作由api()返回的单个JAXBElement<String>实例,因此是允许的。 - Mark
3
因为“未知”并不等同于“任何事物”。 - user541686
显示剩余5条评论
3个回答

34
// #1 (does compile)
List raw = null;
List<?> wild = raw;

// #2 (doesn't compile)
List<List> raw = null;
List<List<?>> wild = raw;

首先,让我们搞清楚为什么它们实际上是不相关的任务。也就是说,它们受到不同的规则支配。

#1 被称为 未经检查的转换

从原始类或接口类型(§4.8) G 到任何形式为 G<T1,...,Tn> 的参数化类型存在一个未经检查的转换。

具体来说,它是一种特殊情况的 赋值上下文,仅适用于此场景:

如果在应用 [其他可能的转换] 后,结果类型是原始类型,则可以应用未经检查的转换。

#2需要进行引用类型转换,但问题在于它不是扩展转换(这是一种可以隐式允许无需强制转换的引用转换)。

为什么呢?好吧,这是由通用子类型规则特别控制的,更具体地说,是由以下要点控制:

给定一个通用类型声明 C<F1,...,Fn>n > 0),参数化类型 C<T1,...,Tn>直接超类型,其中 Ti(1 ≤ in)是一种类型,包括以下所有内容:
  • C<S1,...,Sn>,其中 Si 包含 Ti(1 ≤ in)。
这里涉及到JLS所称的包容性,即为了成为有效赋值,左侧参数必须包含右侧参数。包容性在很大程度上管理着通用子类型,因为"具体"泛型类型不变的
你可能熟悉以下思想:
- List<Dog>不是List<Animal> - 但是List<Dog>List<? extends Animal> 嗯,后者是正确的,因为? extends Animal 包含Dog
所以问题变成了“类型参数List<?>是否包含原始类型参数List”?答案是不:尽管List<?>List的子类型,但这种关系不适用于类型参数。
没有特殊规则使其成立:List<List<?>>基本上由于与List<Dog>不是List<Animal>的原因而不是List<List>的子类型。
因此,由于List<List>不是List<List<?>>的子类型,该赋值无效。同样地,您也不能执行直接的缩小转换强制转换,因为List<List>也不是List<List<?>>的超类型。

为了完成这个任务,你仍然可以应用强制类型转换。我认为有三种合理的方法来实现它。

// 1. raw type
@SuppressWarnings("unchecked")
List<List<?>> list0 = (List) api();

// 2. slightly safer
@SuppressWarnings({"unchecked", "rawtypes"})
List<List<?>> list1 = (List<List<?>>) (List<? extends List>) api();

// 3. avoids a raw type warning
@SuppressWarnings("unchecked")
List<List<?>> list2 = (List<List<?>>) (List<? super List<?>>) api();

你可以将内部的List替换为JAXBElement。使用这种类型转换应该是安全的,因为List<List<?>>List<List>更加限制严格。
  • The raw type statement is a widening cast then unchecked assignment. This works because, as shown above, any parameterized type can be converted to its raw type and vice-versa.

  • The slightly safer statement (named as such because it loses less type information) is a widening cast then narrowing cast. This works by casting to a common supertype:

        List<? extends List>
            ╱         ╲
    List<List<?>>     List<List>
    

    The bounded wildcard allows the type arguments to be considered for subtyping via containment.

    The fact that List<? extends List> is considered a supertype of List<List<?>> can be proven with transitivity:

    1. ? extends List contains ? extends List<?>, because List is a supertype of List<?>.

    2. ? extends List<?> contains List<?>.

    3. Therefore ? extends List contains List<?>.

    (That is, List<? extends List> :> List<? extends List<?>> :> List<List<?>>.)

  • The third example works in a way that's similar to the second example, by casting to a common supertype List<? super List<?>>. Since it doesn't use a raw type, we can suppress one less warning.


这里的非技术总结是,规范意味着List<List>List<List<?>>之间既没有子类型关系也没有超类型关系。
尽管从List<List>转换为List<List<?>>应该是安全的,但是不允许这样做。(因为两者都是可以存储任何类型的List,但List<List<?>>对其检索后如何使用其元素有更多限制。)
不幸的是,这种编译错误没有实际原因,只是因为原始类型很奇怪,使用它们会出现问题。

2
我非常感激你在叙述中引用JLS的努力。 - djeikyb

3
你不能直接分配或转换它,因为原始类型ListList<?>不同。

使用List时,类型检查被忽略,您可以使用任何泛型方法与任何类型。使用List<?>时,编译器不允许您使用带有泛型参数的方法


因此,您可以忽略警告:

@SuppressWarnings("rawtypes")

或者使用一个变通方法显式地转换它:

List<JAXBElement<String>> raw = (List<JAXBElement<String>>) ((Object)api());

0

如果你只想消除警告,可以使用@SuppressWarnings("rawtypes")。

基本上问题在于编译器将原始类型视为先前泛型的原始对象,因此..."旧对象"不是"通用对象",所以...你不能将它们强制转换。

从官方文档中阅读以下内容: http://docs.oracle.com/javase/tutorial/java/generics/rawTypes.html

但是,如果将原始类型分配给参数化类型,则会收到警告:

Box rawBox = new Box(); // rawBox 是 Box 的原始类型 Box intBox = rawBox; // 警告:未经检查的转换。您还会收到警告,如果使用原始类型调用在相应的泛型类型中定义的泛型方法:

Box stringBox = new Box<>(); Box rawBox = stringBox; rawBox.set(8); // 警告:未经检查的 set(T) 调用。警告显示原始类型绕过了泛型类型检查,将不安全代码的捕获推迟到运行时。因此,应避免使用原始类型。


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