英文:
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<Object> lst)
, why can't we call it with an integer list List<Integer> 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<? extends Number> list = new ArrayList<Integer>(); // legal
- Contravariant:
List<? super Integer> list = new ArrayList<Number>(); // legal
- Invariant:
List<Integer> list = new ArrayList<Integer>(); // only integer will do here
With void m0(List<Object> list)
, you've picked invariant. For the generics part, only <Object>
will do (and for the List
part, that is covariant as 'normal' java is, so an ArrayList<Object>
can be passed here, but e.g. a List<String>
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<Integer> ints = new ArrayList<Integer>();
List<Number> 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<Number> 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<Integer>
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<? extends Number> numbers)
instead.
Here is an example of invariance:
public void addSumToEnd(List<Number> 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<? extends Number>
. After all, we're adding an int
and you can't do that to a List<Double>
. The only acceptable lists you can feed in here are List<Number>
and List<Integer>
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<?> lst) {...}
and <?>
is just short for <? extends Object>
. You've denied yourself the ability to call .add
, but you can still call .get
, and crucially you can pass List<String>
to such a method, whereas you cannot if it read List<Object>
(but, on the other hand, you can call .add()
on a List<Object>
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论