不变泛型似乎无法正常工作
Invariant Generics don't seem working correctly
我在 Java,但我对它们感到困惑。
我正在使用 Java 11,并且我有一个 class 层次结构 A => B => C
(意味着 C
是 B
的子类型并且 A
,并且 B
是 A
) 的子类型和 class 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
if in 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 class extends B,那么 C 是 B 的子 class。C 和 B 之间的这种关系也称为 is-a
关系,其中 C 的实例也是 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 中扩展了协方差的概念。
问题的例子没有证明泛型的不变性。
一个证明这一点的例子是:
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
和 re-write 通过用 C
填充菱形,您将看到以下内容无法编译:
Container<B> contc = new Container<C>(new C()); // does not compile
这会给您带来与我的 ArrayList
示例相同的“不兼容类型”编译错误。
Java 7 引入的其中一个好处是 so-called diamond operator <>
.
它已经伴随我们很长时间了,以至于很容易忘记每次在实例化泛型时使用 diamond class 编译器应该 infer 来自上下文的通用类型。
如果我们定义一个变量,它将保存对 Person
个对象列表的引用,如下所示:
List<Person> people = new ArrayList<>(); // effectively - ArrayList<Person>()
编译器会根据左边people
变量的类型推断ArrayList
实例的类型。
在 Java 语言规范 中,表达式 new ArrayList<>()
被描述为 class instance creation expression,因为它没有指定通用类型参数并在 context 中使用,它应该 class 化为 poly expression。引用规范:
A class instance creation expression is a poly expression (§15.2) if
it uses the diamond form for type arguments to the class, and it
appears in an assignment context or an invocation context (§5.2,
§5.3).
即当 diamond <>
与通用 class 实例一起使用时,实际类型将取决于它所在的 context出现。
下面的三个语句代表了so-called 赋值上下文的情况。所有三个实例 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
。 IE。可以提供 B
或其任何子类型的实例。因此,在 1
的情况下,我们得到一个编译错误,同时 2
和 3
(B
和 B
的子类型)将正确编译。
并且它不违反不变行为。这样想:我们可以在 List<Number>
中存储 Integer
、Byte
、Double
等的实例,这不会导致任何问题,因为它们都可以代表它们的超类型 Number
。但是编译器不允许将此列表分配给任何非 List<Number>
类型的列表,否则无法确保此分配是安全的。这就是不变性的意思——我们只能将 List<Number>
赋值给 List<Number>
类型的变量(但我们可以在其中自由存储 Number
的任何子类型,这是安全的)。
例如,让我们考虑在 Container
class:
中有一个 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
当我们使用菱形 <>
处理 class 实例创建表达式时,它作为参数传递给方法,类型将从 调用上下文中推断出来 正如上面提供的规范中的引述所述。
因为 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 引入了一个名为 target types 和 多边形表达式 的功能(即出现在 上下文中的表达式 ) 确保 context 将始终被类型推断机制考虑在内。
我在 Java,但我对它们感到困惑。
我正在使用 Java 11,并且我有一个 class 层次结构 A => B => C
(意味着 C
是 B
的子类型并且 A
,并且 B
是 A
) 的子类型和 class 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
if in 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 class extends B,那么 C 是 B 的子 class。C 和 B 之间的这种关系也称为 is-a
关系,其中 C 的实例也是 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 中扩展了协方差的概念。
问题的例子没有证明泛型的不变性。
一个证明这一点的例子是:
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
和 re-write 通过用 C
填充菱形,您将看到以下内容无法编译:
Container<B> contc = new Container<C>(new C()); // does not compile
这会给您带来与我的 ArrayList
示例相同的“不兼容类型”编译错误。
Java 7 引入的其中一个好处是 so-called diamond operator <>
.
它已经伴随我们很长时间了,以至于很容易忘记每次在实例化泛型时使用 diamond class 编译器应该 infer 来自上下文的通用类型。
如果我们定义一个变量,它将保存对 Person
个对象列表的引用,如下所示:
List<Person> people = new ArrayList<>(); // effectively - ArrayList<Person>()
编译器会根据左边people
变量的类型推断ArrayList
实例的类型。
在 Java 语言规范 中,表达式 new ArrayList<>()
被描述为 class instance creation expression,因为它没有指定通用类型参数并在 context 中使用,它应该 class 化为 poly expression。引用规范:
A class instance creation expression is a poly expression (§15.2) if it uses the diamond form for type arguments to the class, and it appears in an assignment context or an invocation context (§5.2, §5.3).
即当 diamond <>
与通用 class 实例一起使用时,实际类型将取决于它所在的 context出现。
下面的三个语句代表了so-called 赋值上下文的情况。所有三个实例 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
。 IE。可以提供 B
或其任何子类型的实例。因此,在 1
的情况下,我们得到一个编译错误,同时 2
和 3
(B
和 B
的子类型)将正确编译。
并且它不违反不变行为。这样想:我们可以在 List<Number>
中存储 Integer
、Byte
、Double
等的实例,这不会导致任何问题,因为它们都可以代表它们的超类型 Number
。但是编译器不允许将此列表分配给任何非 List<Number>
类型的列表,否则无法确保此分配是安全的。这就是不变性的意思——我们只能将 List<Number>
赋值给 List<Number>
类型的变量(但我们可以在其中自由存储 Number
的任何子类型,这是安全的)。
例如,让我们考虑在 Container
class:
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
当我们使用菱形 <>
处理 class 实例创建表达式时,它作为参数传递给方法,类型将从 调用上下文中推断出来 正如上面提供的规范中的引述所述。
因为 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 引入了一个名为 target types 和 多边形表达式 的功能(即出现在 上下文中的表达式 ) 确保 context 将始终被类型推断机制考虑在内。