Why method with argument List<Object> can not accept a list of any type like List<A> as argument in java?

huangapple go评论61阅读模式
英文:

Why method with argument List<Object> can not accept a list of any type like List<A> as argument in java?

问题

假设我们有一个方法 void m0(List<Object> lst),为什么我们不能用整数列表 List<Integer> iLst = new ArrayList(); m0(iLst); 调用它,而使用方法 void m0(Object a)m0(1); 却是可以的。从逻辑上讲,整数列表是对象的列表,为什么 m0(iLst); 是不正确的呢?

英文:

Suppose we have a method void m0(List&lt;Object&gt; lst), why can't we call it with an integer list List&lt;Integer&gt; iLst = new ArrayList(); m0(iLst); while with method void m0(Object a), m0(1); is OK. Logically a list of Integer is a list of object, why m0(iLst); is not correct?

答案1

得分: 3

有一个被称为方差的概念。

让我们使用一些我们都熟悉的类型:

java.lang.Integer 继承自 java.lang.Number 继承自 java.lang.Object *

协变性(Covariance)

在一个协变系统中,你可以这样写:

Number x = new Integer();

但你不能这样写:

Integer y = new Number();

正如你可能会猜测的那样,在 Java 中的基本赋值和类似操作都是协变的。

有道理,对于一个指向 Number 实例的引用,我可以对一个 Integer 实例做的任何操作,也同样适用,比如调用其中的 .intValue()。但反过来却不成立;Integer 可能有一些 Number 没有的方法。

因此,就像你熟悉的那样,基本的 Java 赋值、参数传递等都是 协变 的。

逆变性(Contravariance)

在一个逆变系统中,你 不能写

Number x = new Integer();

但从另一方面来说,这实际上是有效的:

Integer y = new Number();

不变性(Invariance)

这是一种不灵活的情况;在这种情况下,上述两种方式都不起作用。唯一能做的事情是:

Integer y = new Integer();

好的,那么,泛型呢?

尽管 Java 在基本操作方面是协变的,但泛型不是。泛型可能是逆变的、协变的或不变的,这取决于你如何编写泛型。

  • 协变:List<? extends Number> list = new ArrayList<Integer>(); // 合法
  • 逆变:List<? super Integer> list = new ArrayList<Number>(); // 合法
  • 不变:List<Integer> list = new ArrayList<Integer>(); // 只能使用 Integer

对于 void m0(List<Object> list),你已经选择了不变性。对于泛型部分,只有 <Object> 是合适的(至于 List 部分,它是协变的,就像普通的 Java 一样,因此可以传递一个 ArrayList<Object>,但无法传递 List<String>)。

嗯,什么鬼?为什么?

因为... 生活。这就是现实生活的运作方式。

试想如果不是这样的。那么我可以做这个,然后打破一切:

List<Integer> ints = new ArrayList<Integer>();
List<Number> numbers = ints; // 标记此行!
numbers.add(new Double(5.0));
Integer x = ints.get(0); // 错误!

在上面的代码中,如果它编译并运行,最后一行将会出错,因为 .get(0) 调用会检索一个不是整数的 double 值。幸运的是,上面的代码无法编译;错误会在标记的那一行出现。那是因为编译器应该不允许这样做。泛型本质上是 不变 的。

协变性是可能存在的。例如,如果你有一个方法,它将对其中的每个数字调用 .intValue() 并对结果求和,那么你可以这样写:

public int sumAll(List<Number> list) {
   int result = 0;
   for (Number n : list) result += n.intValue();
   return result;
}

但这是一个不好的写法;你已经确定了参数是不变的,因此不能将 List<Integer> 传递给此方法。但代码本身是协变的。如果传递一个整数列表,它也能正常工作。因此,你应该将其写成 public int sumAll(List<? extends Number> numbers)

以下是不变性的一个示例:

public void addSumToEnd(List<Number> list) {
    int sum = 0;
    for (Number n : list) sum += n.intValue();
    list.add(sum);
}

因为我们在这里添加了一个数字,所以你不能写成 List<? extends Number>。毕竟,我们添加的是一个 int,你不能将其添加到一个 List<Double> 中。你唯一能传递的列表是 List<Number>List<Integer>,而在 Java 中无法表达这一点。

对于列表来说,很简单:“逆变性 = 添加”(.add().addAll() 等),“协变性 = 读取”,“不变性 = 两者皆可”。对于其他泛型类型来说可能就没那么简单了。

假设你的 m0 方法类只会 ‘读取’,那么你可以将它定义为协变的,写成:

public m0(List<?> lst) {...}

其中 <?> 等价于 <? extends Object>。你放弃了调用 .add 的能力,但仍然可以调用 .get,关键是你可以将 List<String> 传递给这样的方法,而如果它读取 List<Object> 的话是不行的(但另一方面,你可以在 List<Object> 参数上调用 .add(),添加任何你喜欢的元素!)

*) 这些是 Java 中的实际类型,但 Number 是抽象的。对于这个示例,假设它不是抽象的,并且它们都有无参构造函数。重点是类型关系,与这些类型的特定内容无关。

英文:

There's this thing called variance.

Let's use some types we are all familiar with:

java.lang.Integer extends java.lang.Number extends java.lang.Object *

Covariance

In a covariant system, you can write:

Number x = new Integer();

but you cannot write:

Integer y = new Number();

As you might surmise, basic assignment and such in java is all covariant.

Makes sense, right? Whatever I can do to a reference to a Number instance, I can do to an Integer instance, such as invoking .intValue() on it. But in reverse it does not hold; Integer may have methods that Number doesn't.

Therefore, as you're familiar with, basic java assignment, parameter passing etc is covariant.

Contravariance

In a contravariant system, you cannot write:

Number x = new Integer();

but on the flipside, this actually works:

Integer y = new Number();

Invariance

This is the inflexible one; in this one, neither works. The only thing you can do is:

Integer y = new Integer();

Okay, so, what about generics?

Whereas java is covariant for basic stuff, generics isn't. Generics is contravariant, or covariant, or invariant, depending on how you write the generics.

  • Covariant: List&lt;? extends Number&gt; list = new ArrayList&lt;Integer&gt;(); // legal
  • Contravariant: List&lt;? super Integer&gt; list = new ArrayList&lt;Number&gt;(); // legal
  • Invariant: List&lt;Integer&gt; list = new ArrayList&lt;Integer&gt;(); // only integer will do here

With void m0(List&lt;Object&gt; list), you've picked invariant. For the generics part, only &lt;Object&gt; will do (and for the List part, that is covariant as 'normal' java is, so an ArrayList&lt;Object&gt; can be passed here, but e.g. a List&lt;String&gt; cannot).

Um, wtf? Why???

Because... life. That is how real life works.

Imagine it did not. I can do this, then, and break everything:

List&lt;Integer&gt; ints = new ArrayList&lt;Integer&gt;();
List&lt;Number&gt; numbers = ints; // MARK THIS LINE!
numbers.add(new Double(5.0));
Integer x = ints.get(0); // ERROR!

In the above, if it had compiled and run, the last line would be an error, as the .get(0) call would retrieve a double value which isn't an integer. Fortunately, the above does not compile; the error occurs on the marked line. That's.. because the compiler should disallow this. Generics by its very nature are invariant.

Now, covariance can exist. For example, if you have a method that will sum up the result of invoking .intValue() on each of the Numbers inside, then you could write:

public int sumAll(List&lt;Number&gt; list) {
   int result = 0;
   for (Number n : list) result += n.intValue();
   return result;
}

but that's a bad way to write it; you've decreed that the parameter is invariant, thus, you cannot pass a List&lt;Integer&gt; to this thing. But the code is covariant. It would work just as well if you pass a list of integers. So, you should write that as public int sumAll(List&lt;? extends Number&gt; numbers) instead.

Here is an example of invariance:

public void addSumToEnd(List&lt;Number&gt; list) {
    int sum = 0;
    for (Number n : list) sum += n.intValue();
    list.add(sum);
}

Because we're adding a number here, you couldn't write List&lt;? extends Number&gt;. After all, we're adding an int and you can't do that to a List&lt;Double&gt;. The only acceptable lists you can feed in here are List&lt;Number&gt; and List&lt;Integer&gt; and there's no way to express that in java.

For lists, it's easy: "contravariance = adds" (.add(), .addAll(), etc), "covariance = reads", "invariance = does both". For other generified types it may not be that simple.

Presumably if your m0 method class will only ever 'read', then you can make it covariant, and write:

public m0(List&lt;?&gt; lst) {...}

and &lt;?&gt; is just short for &lt;? extends Object&gt;. You've denied yourself the ability to call .add, but you can still call .get, and crucially you can pass List&lt;String&gt; to such a method, whereas you cannot if it read List&lt;Object&gt; (but, on the other hand, you can call .add() on a List&lt;Object&gt; parameter, and add whatever you like!

*) These are real types in java, but Number is abstract. For the purposes of this example, assume it is not, and that they all have no-args constructors. The point is the type relation, not anything particular about these types.

huangapple
  • 本文由 发表于 2020年9月5日 18:14:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/63752786.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定