Arrays.asList() 与不兼容的类型

37
在下面的例子中,如果列表中有多种类型,它会编译通过,但是如果只有一个元素,则会选择不再可分配的不同类型。
// compiles fine
List<Class<? extends Reference>> list = Arrays.asList(SoftReference.class, WeakReference.class);
// but take an element away and it no longer compiles.
List<Class<? extends Reference>> list2 = Arrays.asList(WeakReference.class);
// without giving the specific type desired.
List<Class<? extends Reference>> list3 = Arrays.<Class<? extends Reference>>asList(WeakReference.class);

我确信这件事情有一个逻辑上的解释,但我无法理解。

    Error:Error:line (30)error: incompatible types
required: List<Class<? extends Reference>>
found:    List<Class<WeakReference>>

为什么有两个元素可以编译,但只有一个元素不能编译?

顺便说一下:如果你尝试的话,很难找到一个简单的例子。

List<Class<? extends List>> list = Arrays.asList(ArrayList.class, LinkedList.class);

    Error:Error:line (28)error: incompatible types
required: List<Class<? extends List>>
found:    List<Class<? extends INT#1>>
where INT#1 is an intersection type:
INT#1 extends AbstractList,Cloneable,Serializable

这个也无法编译(甚至无法解析)

List<Class<? extends AbstractList & Cloneable & Serializable>> list = Arrays.asList(ArrayList.class, LinkedList.class);

Error:Error:line (30)error: > expected
Error:Error:line (30)error: ';' expected

但是这个可以成功编译

static abstract class MyList<T> implements List<T> { }
List<Class<? extends List>> list = 
        Arrays.asList(ArrayList.class, LinkedList.class, MyList.class);
List<Class<? extends List>> list = 
        Arrays.<Class<? extends List>>asList(ArrayList.class, LinkedList.class);

编辑:基于Marko的示例。在这四个示例中,其中一个无法编译,其余的产生了相同类型的相同列表。

List<Class<? extends Reference>> list = new ArrayList<>();
list.add(SoftReference.class);
list.add(WeakReference.class);
list.add(PhantomReference.class);

List<Class<? extends Reference>> list = new ArrayList<>(
     Arrays.asList(SoftReference.class));
list.add(WeakReference.class);
list.add(PhantomReference.class);

List<Class<? extends Reference>> list = new ArrayList<>(
     Arrays.asList(SoftReference.class, WeakReference.class));
list.add(PhantomReference.class);

List<Class<? extends Reference>> list = new ArrayList<>(
     Arrays.asList(SoftReference.class, WeakReference.class, PhantomReference.class));

我认为jdk < 7中的编译器要求语句的两个部分具有相同的声明。但我并不完全确定这是否是导致问题的原因!! - mamdouh alramadan
@TedHopp:不确定一个上限通配符是否可以扩展多个类,否则在我的理解中这将允许多重继承... - fge
呵呵。我们都知道这会引起麻烦。 :) - Ted Hopp
@fge - 也许您可以详细解释一下您的观点。通配符只是表示“与指定范围一致的某些类型”。无论这些界限是单个类名还是带有接口形式的其他界限的类名,在我看来似乎没有任何区别。 - Ted Hopp
1
所有的情况对我来说都是有意义的,但我仍然没有看到你想要在工作和非工作情况之间制造的紧张关系。Class<WeakReference> 不是 Class<? extends WeakReference>,因此自然而然地不能进行赋值。不过,这个例子确实指出了泛型类型系统的弱点。 - Marko Topolnik
显示剩余10条评论
4个回答

14

有趣的问题。我的看法是这样的。当你有像你展示的两个元素时,asList 的返回类型是所有参数中最具体的类型,在你的第一个例子中是 List<Reference>。这是可以赋值给 List<? extends Reference> 的。当你只有一个参数时,返回类型是参数的具体类型,这是不可赋值的,因为泛型不是协变的。


@PeterLawrey - 哎呀——打错字了。我是说“协变”的。已经更新了答案。 - Ted Hopp
我觉得隐式转换的规则在不同情况下是不同的。一定有一些逻辑可以解释为什么使用asList时可以进行某种类型的转换,而在赋值时却不能这样做。 - Peter Lawrey
@PeterLawrey - 计算交集(编译器在调用asList时所需的所有内容)从来不是问题 - 最坏的情况下,您最终会得到Object。然而,毫不奇怪的是,分配语句中解析泛型的规则与方法调用的规则不同。或者我误解了您的观点? - Ted Hopp
这让我感到惊讶,因为我一直认为参数被分配给方法参数。即使接受它们可能有不同的规则,这样做的好理由是什么?也许可以举一个完全不同的例子... - Peter Lawrey
3
@PeterLawrey - 我表述有误。规则是一样的,但情况不同,因此规则的不同方面适用。每个实际参数都被分配给一个参数,该参数是所有实际参数的交集类型。由于交集类型的定义,这几乎不会导致问题。另一方面,赋值语句将交集类型分配给您代码指定的通用类型。那时可能会出现问题(在此情况下,因为通用类型不是协变的)。 - Ted Hopp
1
这个解释有几个错误:首先,List<WeakReference> 可以赋值给 List<? extends Reference>。其次,问题不是关于这些类型的,而是关于 List<Class<? extends Reference>> -- 只有在这里才会出现问题,因为现在没有通配符捕获发生。还要注意,如果泛型类型是协变的,它们将被破坏,所以这并不是一个很好的解释。 - Marko Topolnik

10
考虑一下。
    // ok
    List<Object> list3 = Arrays.asList(new Object(), new String());
    // fail
    List<Object> list4 = Arrays.asList(new String());

第二个例子试图将一个List<String>分配给一个List<Object>,但是失败了。
如果javac查看周围的上下文,考虑目标类型,并推断出T=Object在这里会起作用,那么第二个例子可能会起作用。Java 8可能会这样做(我不确定)。
只有在一种情况下,javac(java 5的)才会使用上下文信息进行类型推断,请参见http://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.12.2.8 我们可以利用这一点来制定解决方案。
public static <R, T extends R> List<R> toList(T... elements)
{
    return Arrays.asList((R[])elements);
}

现在它们可以被编译:
    List<Object> list4 = toList(new String());

    List<Class<? extends Reference>> list = toList(SoftReference.class, WeakReference.class);

    List<Class<? extends Reference>> list2 = toList(WeakReference.class);

这是因为在参数类型无法推断出 R 的情况下,且该方法的结果处于一个赋值上下文中,所以 javac 会尝试通过目标类型推断 R。 这种方式适用于赋值或返回语句。
List<Class<? extends Reference>> foo()
{
    return toList(WeakReference.class);  // "subject to assignment conversion"
}

否则它无法正常工作。
void bar(List<Class<? extends Reference>> list){...}

bar( toList(WeakReference.class) ); // fail; R not inferred

2
+1,这是正确的答案。关键在于JLS §15.12.2.8的第一行:“如果任何方法的类型参数没有从实际参数的类型中推断出来,则现在将按以下方式推断它们...”由于在Arrays.asList()的情况下返回列表的类型从实际参数中推断出来的,因此上下文推断的规则不适用。 - Daniel Pryden
你仍然需要在 toList 方法中添加 @SuppressWarnings ("unchecked")。如果没有它,我会收到一个 Unchecked generics array creation for varargs parameter 警告。 - user711807
List<Object> list4 = toList(new String()); 无法编译(类型不兼容)。请修正您的答案。 - user711807
你的 toList 方法有问题。从 Arrays.asList 返回的 List 是由传入的数组支持的。你有一个 List<Object>,如果你把 Object 放进去会抛出 ArrayStoreException 异常。请参考 http://ideone.com/vcuGfS。 - Radiodef

4
这里需要解释两个方面的行为:
  1. 随着参数的变化,右侧类型如何变化?
  2. 为什么一些右侧类型与左侧类型不兼容?

1. 右侧类型

asList的签名是

<T> List<T> asList(T... a)

这意味着所有参数必须合并为单个类型T,该类型是所有参数类型的最具体公共类型。在这种情况下,我们有:

asList(WeakReference.class) -> List<Class<WeakReference>>

并且

asList(WeakReference.class, SoftReference.class) 
   -> List<Class<? extends Reference>>

这两个很显然。

2. 左侧表达式

现在,为什么我们不能将类型为List<Class<WeakReference>>的第一个表达式赋值给类型为List<Class<? extends Reference>>的变量呢?理解规则必须如此的最好方法是通过反证法来证明。考虑以下情况:

  • List<Class<? extends Reference>>add(Class<? extends Reference>)
  • List<Class<WeakReference>>add(Class<WeakReference>)

现在,如果Java允许您将一个赋值给另一个:

List<Class<WeakReference>> lw = new ArrayList<>();
List<Class<? extends Reference>> lq = lw;
lq.add(PhantomReference.class);

这将导致类型安全方面的明显违规。


2

这很有趣:

where INT#1 is an intersection type:
INT#1 extends AbstractList,Cloneable,Serializable

也许这是一些问题的原因?
元素的交集类型可能无法唯一确定。当您声明自己的列表 MyList<T> implements List<T> 时,数组的交集类型被确定为 List<T>
使用 Arrays.<Class<? extends List>>asList(ArrayList.class, LinkedList.class); 时,“交集”类型被明确说明(为 List),不需要由编译器推断。
除此之外,我相信 Ted Hopp 所说的另一种情况是正确的。
编辑:
List<Class<? extends Reference>> list2 = Arrays.asList(WeakReference.class);

并且

List<Class<? extends Reference>> list3 = Arrays.<Class<? extends Reference>>asList(WeakReference.class);

编译器确定新列表类型的时间可能是关键点:在考虑赋值之前,我认为它需要确定列表的泛型类型。为此,它利用已有信息推断出新列表的类型,而不考虑赋值。这可能会导致上述两个语句创建两种不同类型的列表,从而导致观察到的行为。


+1 我同意,这就是为什么我创建了虚拟的 MyList 来限制类型交集的方式。 - Peter Lawrey

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