Why List<String> is not acceptable as List<Object>?

34

考虑下面这个方法 doSomething(List<Object>),它接受 List<Object> 作为参数。

private void doSomething(List<Object> list) {
    // do something
}
现在考虑下面的代码片段,它试图调用 doSomething(),在其中我尝试将 List<String> 传递给 doSomething()
List<Object> objectList;
List<String> stringList;

doSomething(stringList); // compilation error incompatible types
doSomething(objectList); // works fine 

即使是以下代码,也会引发编译错误

objectList = stringList;  // compilation error incompatible types

我的问题是为什么无法将 List<String> 传递给一个接受 List<Object> 的方法?


3
请查看这个链接:http://docs.oracle.com/javase/tutorial/extra/generics/subtype.html。 - Konstantin Yovkov
7
List<? extends Object>应该允许它正常工作。 - AbstractChaos
5
没问题,只要“做某事”不涉及List#add(...)即可。 - Marco13
7个回答

53

因为虽然String扩展了Object,但List<String>并没有扩展List<Object>

更新:
通常情况下,如果FooBar的子类型(子类或子接口),并且G是一些泛型类型声明,则G<Foo>不是G<Bar>的子类型。

这是因为集合确实会发生变化。在您的情况下,如果List<String>List<Object>的子类型,则可以在使用其超类型引用列表时向其中添加除String以外的类型,如下所示:

List<String> stringList = new ArrayList<String>;
List<Object> objectList = stringList;// this does compile only if List<String> where subtypes of List<Object>
objectList.add(new Object());
String s = stringList.get(0);// attempt to assign an Object to a String :O

Java编译器必须防止这些情况。

有关此Java教程页面的更多详细说明。


4
+1;无论如何,你都可以调用doSomething(((List<Object>)(List<?>) stringList))函数。 - robermann

17

如果这样行得通,你可以将一个错误类型的对象放入列表中:

private void doSomething(List<Object> list) {
    list.add(new Integer(123)); // Should be fine, it's an object
}

List<String> stringList = new ArrayList<String>();
doSomething(stringList); // If this worked....
String s = stringList.get(0); // ... you'd receive a ClassCastException here

2
请注意:泛型的超类型/子类型关系在 http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ207 中有很好的解释。 - Marco13

12

对于不太熟悉泛型的人来说,这个Java中的通用问题可能看起来很困惑。一开始看起来好像String是一个对象,所以可以在需要List<Object>的地方使用List<String>,但这是错误的。这将导致编译错误。

如果您再进一步了解,它就有意义了,因为List<Object> 可以存储任何东西,包括StringInteger等,但List<String>只能存储Strings

还可以查看:为什么不能继承List<T>?


5
问题并不在于被调用的函数可以存储任何内容,而真正的问题是如果允许这样做,调用者将无法确定是否有一个字符串实例,因此必须像没有泛型的时代一样处理它(使用instanceof运算符)。 - robermann

5
这些限制是因为方差考虑。接下来看下面的代码:
public void doSomething(List<Object> objects)
{
  objects.add(new Object());
}

扩展你的示例,你可以尝试以下操作:

List<String> strings = new ArrayList<String>();
string.add("S1");

doSomething(strings);

for (String s : strings)
{
  System.out.println(s.length);
}

希望显而易见的是,如果编译器允许编译此代码(实际上不会),那么这将导致错误 - 当尝试将 Object 强制转换为 String 时,列表中的第二个项目将发生 ClassCastException。
要能够传递泛化的集合类型,您需要执行以下操作:
public void doSomething(List<?> objects)
{
  for (Object obj : objects)
  {
    System.out.println(obj.toString);
  }
}
再次强调,编译器会保护你的代码,如果你将System.out替换为objects.add(new Object()),编译器不会通过此操作,因为objects可能已被创建为List<String>
有关逆变性更多的背景知识,请参见维基百科文章Covariance and contravariance

3

来自Java泛型教程:

现在让我们测试一下你对泛型的理解。以下代码片段是否合法?

List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2 

第一行是合法的。问题更加棘手的部分在于第二行。这归结为一个问题:List是否是List。大多数人本能地回答:“当然可以!”

好吧,看看接下来的几行:

lo.add(new Object()); // 3
String s = ls.get(0); // 4: Attempts to assign an Object to a String!

这里我们给ls和lo起了别名。通过别名lo访问ls(一个字符串列表),我们可以将任意对象插入其中。因此,ls不再仅仅保存字符串,当我们尝试从中获取内容时,会得到一个不友好的惊喜。

当然,Java编译器会防止这种情况发生。第2行代码会导致编译时错误。

来源:泛型和子类型化


3
有时人们会期望 List<Object> 成为 List<String> 的超类型,因为ObjectString 的超类型。
这种期望源于数组存在这样的类型关系: Object[]String[] 的超类型,因为 ObjectString 的超类型(这种类型关系称为协变)。
组件类型的超子类型关系扩展到相应的数组类型中。
对于泛型类型的实例化,这种类型关系不存在(参数化类型不是协变的)。
请查看此处获取更多详细信息。

0
如果您不确定它将使用哪种数据类型,您可以按以下方式在Java中使用泛型。
public static void doSomething(List<?> data) {

}

public static void main(String [] args) {
    List<Object> objectList = new ArrayList<Object>();
    List<String> stringList = new ArrayList<String>();
    doSomething(objectList);
    doSomething(stringList);
}

但是在使用数据时,您需要指定正确的数据类型作为类型转换


1
没有关于这个问题的关系,我认为。 - Konstantin Yovkov
1
是的,我知道这与问题无关,但由于您在问题上添加了适当的注释,所以认为分享可能使用Java中的泛型的通用方式会很好 :-/ - rsakhale

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