我读过一些关于 Java 中的协方差、逆变和不变性的文章,但我对它们感到困惑。
我使用的是Java 11,我有一个类层次结构A => B => C
(意味着C
是B
和A
的子类型,B
是A
的子类型)和一个类Container
:
class Container<T> {
public final T t;
public Container(T t) {
this.t = t;
}
}
例如,如果我定义一个函数:
public Container<B> method(Container<B> param){
...
}
这是我的困惑,为什么第三行要编译?
method(new Container<>(new A())); // ERROR
method(new Container<>(new B())); // OK
method(new Container<>(new C())); // OK Why ?, I make a correction, this compiles OK
如果在 Java 中泛型是不变的。
当我定义这样的东西时:
Container<B> conta = new Container<>(new A()); // ERROR, Its OK!
Container<B> contb = new Container<>(new B()); // OK, Its OK!
Container<B> contc = new Container<>(new C()); // Ok, why ? It's not valid, because they are invariant
协方差是在扩展超类型时传递或指定子类型的能力。如果您的 C 类扩展 B,则 C 是 B 的子类。C 和 B 之间的这种关系也称为is-a
关系,其中 C 的实例也是 B 的实例。因此,当您的变量contc
需要 B 实例并且您传递new C()
时,由于new C()
是 C 的实例和 C 实例is (also)-an
B 的实例,因此编译器允许以下编写:
Container<B> contc = new Container<>(new C());
相反,当你在写作时
Container<B> conta = new Container<>(new A());
您收到错误,因为 A 是 B 的超类型,从 A 到 B 没有is-a
关系,而是从 B 到 A 的关系。这是因为 B 的每个实例也是 A 的实例,但并非 A 的每个实例都是 B 的实例(举一个愚蠢的例子,每个拇指都是手指,但不是每个手指都是拇指)。A 是 B 的推广;因此,它不能出现在预期 B 实例的位置。
这里有一篇很好的文章扩展了Java中协方差的概念。
https://www.baeldung.com/java-covariant-return-type
问题的示例并未证明泛型的不变性。
证明这一点的一个例子是:
ArrayList<Object> ao = new ArrayList<String>(); // does not compile
(您可能会错误地期望编译上述内容,因为String
是Object
的子类。
这个问题向我们展示了构造Container<B>
对象的不同方法 - 其中一些可以编译,而另一些则不编译,因为A
、B
和C
的继承层次结构。
该钻石运算符<>
意味着创建的容器在每种情况下都是B
类型。
如果采用以下示例:
Container<B> contc = new Container<>(new C()); // compiles
并通过用C
填充菱形来重写它,您将看到以下内容无法编译:
Container<B> contc = new Container<C>(new C()); // does not compile
这将为您提供与我ArrayList
示例相同的"不兼容类型"编译错误。
Java7引入的好处之一是所谓的菱形运算符<>
。
它已经存在了很长时间,以至于很容易忘记,每次在实例化泛型类时使用 diamond 时,编译器都应该从上下文中推断泛型类型。
如果我们定义一个变量,该变量将保存对Person
对象列表的引用,如下所示:
List<Person> people = new ArrayList<>(); // effectively - ArrayList<Person>()
编译器将从左侧变量people
的类型推断ArrayList
实例的类型。
在Java 语言规范中,表达式new ArrayList<>()
被描述为类实例创建表达式,并且由于它未指定泛型类型参数并在上下文中使用,因此应将其归类为多边形表达式。规范中的引用:
类实例创建表达式是多边形表达式(§15.2),如果 它使用菱形形式作为类的类型参数,并且 出现在赋值上下文或调用上下文中(§5.2, §5.3)。
即,当菱形<>
与泛型类实例化一起使用时,实际类型将取决于它出现的上下文。
下面的三个语句表示所谓的赋值上下文的情况。并且Container
的所有三个实例都将被推断为类型B
。
Container<B> conta = new Container<>(new A()); // 1 - ERROR because `B t = new A()` is incorrect
Container<B> contb = new Container<>(new B()); // 2 - fine because `B t = new B()` is correct
Container<B> contc = new Container<>(new C()); // 3 - fine because `B t = new C()` is also correct
由于容器的所有实例都是B
类型,并且承包商期望的参数类型也将B
。 即可以提供B
或其任何子类型的实例。因此,如果我们1
遇到编译错误,同时2
和3
(B
的B
和子类型)将正确编译。
而且它不违反不变行为。可以这样想:我们可以在List<Number>
中存储Integer
、Byte
、Double
等实例,这不会导致任何问题,因为它们都可以表示它们的超类型Number
。但是编译器不允许将此列表分配给任何不属于List<Number>
类型的列表,否则将无法确保此赋值是安全的。这就是不变性的含义 - 我们只能将List<Number>
分配给List<Number>
类型的变量(但我们可以自由地在其中存储任何Number
子类型,这是安全的)。
例如,让我们考虑Container
类中有一个 setter 方法:
public class Container<T> {
public T t;
public Container(T t) {
this.t = t;
}
public void setT(T t) {
this.t = t;
}
}
现在让我们使用它:
Container<B> contb = new Container<>(null); // to avoid any confusion initialy `t` will be assigned to `null`
contb.setT(new A()); // compilation error - because expected type is `B` or it's subtype
contb.setT(new B()); // fine
contb.setT(new C()); // fine because C is a subtype of B
当我们使用 菱形<>
处理类实例创建表达式时,该表达式作为参数传递给方法,该类型将从调用上下文中推断出来,如上面提供的规范中的引用所述。
因为method()
期望Container<B>
,所以上述所有实例都将被推断为类型B
。
method(new Container<>(new A())); // Error
method(new Container<>(new B())); // OK - because `B t = new B()` is correct
method(new Container<>(new C())); // OK - because `B t = new C()` is also correct
注意
值得一提的是,在 Java 8 之前(即使用 Java 7,因为我们使用的是菱形),编译器将new Container<>(new C())
表达式解释为独立表达式(即上下文将被忽略)创建Container<C>
实例。这意味着你最初的猜测在某种程度上是正确的:在Java 7中,下面的语句将无法编译。
Container<B> contc = new Container<>(new C()); // Container<B> = Container<C> - is an illegal assignment
但是Java 8引入了一个名为目标类型和多边形表达式(即出现在上下文中的表达式)的特性,它确保类型推断机制始终考虑上下文。