为什么List<Number>不是List<Object>的子类型?

9
public void wahey(List<Object> list) {}

wahey(new LinkedList<Number>());

调用该方法不会进行类型检查。我甚至无法将参数转换为以下类型:
wahey((List<Object>) new LinkedList<Number>());

根据我的研究,我发现不允许这样做的原因是为了类型安全。如果我们被允许这样做,那么我们可能会出现以下情况:

List<Double> ld;
wahey(ld);

在方法wahey中,我们可以向输入列表添加一些字符串(因为参数维护了一个List<Object>引用)。现在,在方法调用之后,ld引用了一个类型为List<Double>的列表,但实际上列表包含一些字符串对象!

这似乎与没有使用泛型的Java的正常工作方式不同。例如:

Object o;
Double d;
String s;

o = s;
d = (Double) o;

我们所做的事情本质上是相同的,但这将通过编译时检查,并且只会在运行时失败。带有列表的版本将无法编译。
这让我相信这纯粹是关于通用类型限制的设计决策。我希望能够就此决策得到一些评论?

1
与此相关的编程内容(虽然正确答案基本相同):https://dev59.com/HEXRa4cB1Zd3GeqPtaf3 - Kip
3
专业术语是“协方差”——我提到这个是为了让您更容易查找它,包括在 Stack Overflow 上(如果您想看看我在 C# 上的相关讨论,但同样适用于 Java,请搜索[covariance user:95810],例如)。 - Alex Martelli
一个像 wahey((List<Object>) (List<?>) new ArrayList<Number>()) 这样的双重转换虽然可以编译通过,但并不推荐使用。 - Robin479
6个回答

11

在“没有泛型”的示例中,您正在进行一个强制类型转换,这表明您正在执行一些不安全的类型操作。使用泛型进行等效操作如下:

Object o;
List<Double> d;
String s;

o = s;
d.add((Double) o);

这种行为与原问题的行为相同(可以编译,但在运行时失败)。不允许您询问的行为的原因是它会允许隐式类型不安全的操作,这在代码中很难注意到。例如:

public void Foo(List<Object> list, Object obj) {
  list.add(obj);
}

这个看起来完全没有问题,而且类型安全,直到你像这样调用它:

List<Double> list_d;
String s;

Foo(list_d, s);

这种方式看起来也是类型安全的,因为作为调用者,您不一定知道Foo将如何处理其参数。

因此,在这种情况下,您有两个看起来是类型安全的代码片段,但共同导致了类型不安全。这很糟糕,因为它是隐藏的,所以很难避免并且更难调试。


10

考虑如果它是...

List<Integer> nums = new ArrayList<Integer>();
List<Object> objs = nums
objs.add("Oh no!");
int x = nums.get(0); //throws ClassCastException

您可以将任何父类型的内容添加到列表中,但这可能与原先声明的内容不符。正如上面的示例所示,这会导致各种问题。因此,这是不允许的。


这是正确的,但问题表明他已经理解了这一点,并想知道为什么决定禁止这种行为,同时仍允许其他形式的类型不安全行为。 - Tyler McHenry
我已经理解了这个例子。我试图在问题中解释这种情况(就像Tyler所说的),但还是谢谢你的评论。 - ares
2
这种类型不安全的行为实际上是允许使用数组的。以下代码可以编译通过,但会生成运行时错误:Object[] arr = new Integer[2]; arr[0] = "oh no!"; - Kip

4
由于泛型的工作原理,它们不是彼此的子类型。你需要这样声明函数:
public void wahey(List<?> list) {}

然后它将接受任何扩展Object的List。你也可以这样做:

public void wahey(List<? extends Number> list) {}

这将使您能够接收数字子类的列表。
我建议您阅读Maurice Naftalin和Philip Wadler的《Java Generics and Collections》。

+1,这就是泛型的工作原理。务必拿一本Java Generics和Collections的书。太棒了! - WolfmanDragon
1
这不是我的问题。我知道这些是泛型子类型化的规则。 - ares

3
基本上有两个抽象维度,列表抽象和其内容的抽象。可以沿着列表抽象变化 - 例如说它是 LinkedList 还是 ArrayList,但进一步限制其内容 - 例如说:这个(保存对象的列表)是一个(只保存数字的链表)是不好的。因为通过其类型的合同,任何知道它为(保存对象的列表)的引用都可以理解它可以保存 任何 对象。
这与非泛型示例代码中所做的不同,在那里,您已经说:将此字符串视为 Double。相反,您正在尝试说:将此(仅保存数字的列表)视为(可以保存任何东西的列表)。而事实并非如此,并且编译器可以检测到它,因此不会让您轻易摆脱。

1
在Java中,当类型S是类型T的子类型时,List<S> 不是 List<T> 的子类型。这个规则提供了类型安全性。
假设我们允许List<String>成为List<Object>的子类型。考虑以下示例:
public void foo(List<Object> objects) {
    objects.add(new Integer(42));
}

List<String> strings = new ArrayList<String>();
strings.add("my string");
foo(strings); // this is not allow in java
// now strings has a string and an integer!
// what would happen if we do the following...??
String myString = strings.get(1);

因此,强制执行此操作可以提供类型安全性,但它也有一个缺点,即灵活性较差。考虑以下示例:

class MyCollection<T> {
    public void addAll(Collection<T> otherCollection) {
        ...
    }
}

这里有一个 T 的集合,你想要从另一个集合中添加所有的项。你不能使用 ST 的子类型的 Collection<S> 调用此方法。理想情况下,这是可以的,因为你只是将元素添加到你的集合中,而不是修改参数集合。

为了解决这个问题,Java 提供了所谓的“通配符”。通配符是提供协变性/逆变性的一种方式。现在考虑以下使用通配符的情况:

class MyCollection<T> {
     // Now we allow all types S that are a subtype of T
     public void addAll(Collection<? extends T> otherCollection) {
         ...

         otherCollection.add(new S()); // ERROR! not allowed (Here S is a subtype of T)
     }
} 

现在,使用通配符,我们允许类型T的协变,并阻止不安全的类型操作(例如将项目添加到集合中)。这样,我们既获得了灵活性又保证了类型安全。

1
“我们在这里做的基本上是相同的事情,只不过这将通过编译时检查,并且仅在运行时失败。带有列表的版本将无法编译。”
当您考虑到Java泛型的主要目的是使类型不兼容在编译时而不是运行时失败时,您所观察到的一切都是有道理的。
来自java.sun.com “泛型提供了一种方法,让您向编译器传达集合的类型,以便进行检查。一旦编译器知道集合的元素类型,它就可以检查您是否一致地使用了该集合,并且可以在从集合中取出值时插入正确的转换。”

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