【问题标题】:Second order generics seem to behave differently than first order generics二阶泛型的行为似乎与一阶泛型不同
【发布时间】:2017-09-05 08:26:26
【问题描述】:

我认为我对泛型有一定的了解。例如,我明白为什么

private void addString(List<? extends String> list, String s) {
    list.add(s); // does not compile
    list.add(list.get(0)); // doesn't compile either
}

不编译。 I even earned some internet karma with the knowledge.

但我认为同样的论点不应该编译:

private void addClassWildcard(List<Class<? extends String>> list, Class<? extends String> c) {
    list.add(c);
    list.add(list.get(0));
}

也不应该这样:

private void addClass(List<Class<? extends String>> list, Class<String> c) {
    list.add(c);
    list.add(list.get(0));
}

但两者都可以编译。为什么?和上面的例子有什么区别?

我会很感激普通英语的解释以及指向 Java 规范或类似内容的相关部分的指针。

【问题讨论】:

  • 这两种情况有些不同。在第一种情况下,List 的泛型参数本身是有界通配符,但在第二种情况下,List 的泛型参数的泛型参数具有通配符。为了使它们更相似,您可以使用List&lt;? extends Class&lt;...&gt;&gt;,但在第二种情况下,List 不会增加任何实际价值。与以下内容几乎相同:Class&lt;? extends String&gt; cls1 = c; Class&lt;? extends String&gt; cls2 = cls1;

标签: java generics


【解决方案1】:

第二种情况是安全的,因为Class&lt;String&gt; 的所有实例都是Class&lt;? extends String&gt; 的实例。

Class&lt;? extends String&gt; 的实例添加到List&lt;Class&lt;? extends String&gt; 并没有什么不安全的地方- 您将使用get(int)iterator() 等返回Class&lt;? extends String&gt; 的实例- 所以这是允许的。


从某种意义上说,Class 中的通配符只有在实际遇到它的实例时才会被考虑。考虑以下示例(从String 切换到Number,因为String 是最终的)。

private void addClass(List<Class<? extends Number>> list, Class<Number> c) {
    list.add(c);
    list.add(list.get(0));
}

private void tryItSubclass() {
    List<Class<Integer>> ints = new ArrayList<>();

    addClass(ints, Number.class); // does not compile 
}

这里ints 只能包含Class&lt;Integer&gt; 的实例,但Number.class 也是Class&lt;? extends Number&gt;,其中? 被捕获为Number,因此这两种类型不兼容。

private void tryItBound() {
    List<Class<Number>> ints = new ArrayList<>();

    addClass(ints, Number.class); // does not compile
}

这里ints 只能包含Class&lt;Number&gt; 的实例,但Integer.class 也是Class&lt;? extends Number&gt;,其中? 被捕获为Integer,因此这两种类型不兼容。

private void tryItWildcard() {
    List<Class<? extends Number>> ints = new ArrayList<>();

    addClass(ints, Number.class); // does compile

    Class<? extends Number> aClass = ints.get(0);
}

第一种情况是不安全的,因为 - 是否存在扩展 String 的假设类(没有,因为 Stringfinal;但是,泛型忽略 final),List&lt;? extends String&gt; 可能成为List&lt;HypotheticalClass&gt;。因此,您不能将String 添加到List&lt;? extends String&gt;,因为您希望该列表中的所有内容都是HypotheticalClass 的实例:

List<HypotheticalClass> list = new ArrayList<>();
List<? extends String> list2 = list;
list2.add("");  // Not allowed, but pretend it is.
HypotheticalClass h = list.get(0);  // ClassCastException.

【讨论】:

  • 但是String.class(Class的实例)不是Class的实例,这就证明你的第一句话是错的,不是吗?
  • 第一句话“没有什么不安全的……”我不清楚。
  • @OleksandrPapchenko 将T 添加到List&lt;T&gt; 并没有什么不安全的,对吧?所以,如果T? extend String,那也没什么不安全的。
  • @JensSchauder 我并不是说Class&lt;String&gt;Class&lt;HypotheticalClass&gt; 的一个实例——我是说它是Class&lt;? extends String&gt;? extends String != HypotheticalClass - 它包含扩展String所有类。
  • 我想我现在明白了。我添加了几个例子,至少对我来说更清楚。如果您不喜欢编辑,请告诉我。我会把它放在一个单独的答案中。
【解决方案2】:

这与捕获转换有关。安迪的回答很好,但它没有解释规范是如何工作的。我在这里的答案很长,因为,这是 JLS 中相当密集的部分,但我认为它没有得到太多解释,如果你一步一步地走过它并不难。

捕获转换是一个过程,其中编译器采用通配符类型并用非通配符类型替换(部分)通配符。

带有通配符的参数化类型的超类型是捕获转换后该类型的超类型:

4.10.2. Subtyping among Class and Interface Types

给定一个泛型类型声明C&lt;F<sub>1</sub>,...,F<sub>n</sub>&gt; (n > 0),参数化类型C&lt;R<sub>1</sub>,...,R<sub>n</sub>&gt; 的直接超类型至少有一个R<sub>i</sub> (1 ≤ i n) 是通配符类型参数,是参数化类型C&lt;X<sub>1</sub>,...,X<sub>n</sub>&gt; 的直接超类型,是对C&lt;R<sub>1</sub>,...,R<sub>n</sub>&gt; 应用捕获转换的结果。

带通配符的参数化类型的成员(包括方法)的类型是捕获转换后该类型的成员的类型:

4.5.2. Members and Constructors of Parameterized Types

C 为具有类型参数A<sub>1</sub>,...,A<sub>n</sub> 的泛型类或接口声明,并令C&lt;T<sub>1</sub>,...,T<sub>n</sub>&gt;C 的参数化,其中,对于 1 ≤ i nT<sub>i</sub> 是一种类型(而不是通配符)。那么:

  • [因无关而跳过]

如果C的参数化中的任何类型参数是通配符,那么:

  • C&lt;T<sub>1</sub>,...,T<sub>n</sub>&gt;中的字段、方法和构造函数的类型是C&lt;T<sub>1</sub>,...,T<sub>n</sub>&gt;的捕获转换中的字段、方法和构造函数的类型。

那么捕获转换是如何工作的呢?

假设我们得到以下类声明(选择以更完整地说明过程的某些部分):

class C<V, W extends List<V>> {

    void m(V v, W w) {
    }
}

以及下面使用这种类型:

C<Number, ?> c = new C<>();

Double       tArg = 1.0;
List<Number> uArg = new ArrayList<>();
c.m(tArg, uArg);

为了确定if the argument types may be assigned to the parameter types,我们如何确定c.m的类型?

好吧,首先,如上所述,c.m的参数类型是C&lt;Number, ?&gt;的捕获转换中m的参数类型:

5.1.10. Capture Conversion

Gn 类型参数A<sub>1</sub>,...,A<sub>n</sub> 和对应的边界U<sub>1</sub>,...,U<sub>n</sub> 命名一个泛型类型声明。

对于这个例子:

  • GC
  • A<sub>1</sub>V,绑定 U<sub>1</sub>Object
  • A<sub>2</sub>W,绑定 U<sub>2</sub>List&lt;V&gt;

存在从参数化类型G&lt;T<sub>1</sub>,...,T<sub>n</sub>&gt; 到参数化类型G&lt;S<sub>1</sub>,...,S<sub>n</sub>&gt;捕获转换...

对于这个例子,G&lt;T<sub>1</sub>,...,T<sub>n</sub>&gt;C&lt;Number, ?&gt;

  • T<sub>1</sub>Number
  • T<sub>2</sub>?

...,其中,对于 1 ≤ in

  • 如果T<sub>i</sub>? 形式的通配符类型参数,则S<sub>i</sub> 是一个新类型变量,其上限为U<sub>i</sub>[A<sub>1</sub>:=S<sub>1</sub>,...,A<sub>n</sub>:=S<sub>n</sub>],下限为null 类型。

  • 如果T<sub>i</sub>? extends B<sub>i</sub> 形式的通配符类型参数,则S<sub>i</sub> 是一个新类型变量,其上限为glb(B<sub>i</sub>, U<sub>i</sub>[A<sub>1</sub>:=S<sub>1</sub>,...,A<sub>n</sub>:=S<sub>n</sub>]),下限为null 类型。

    glb(V<sub>1</sub>,...,V<sub>m</sub>) 定义为V<sub>1</sub> &amp; ... &amp; V<sub>m</sub>

U<sub>i</sub>[A<sub>1</sub>:=S<sub>1</sub>,...,A<sub>n</sub>:=S<sub>n</sub>]A<sub>i</sub>(类型参数)的边界,每个类型参数替换为每个相应的类型参数。 (这就是为什么我用一个类型参数声明 C 的原因,该类型参数的边界引用了另一个类型参数:因为它说明了这部分的作用。)

在我们的示例中,对于T<sub>2</sub>(即?),S<sub>2</sub> 是一个新类型变量,其上限为U<sub>2</sub>(即List&lt;V&gt;Number 替换为V

S<sub>2</sub> 因此是一个新类型变量,其上限为List&lt;Number&gt;

为简单起见,我将忽略我们有有界通配符的情况,但有界通配符本质上只是将捕获转换为新类型变量,其边界为BoundOfWildcard &amp; BoundOfTypeParameter。此外,如果通配符有下限 (super),那么新类型变量也有下限。

如果T<sub>i</sub> 不是通配符,那么:

  • 否则,S<sub>i</sub> = T<sub>i</sub>

所以在我们的示例中,S<sub>1</sub> 就是 T<sub>1</sub>,即 Number

还有:

捕获转换不会递归应用。

我们稍后会谈到。

我们现在知道:

  • S<sub>1</sub>Number
  • S<sub>2</sub> 是编译器刚刚创建的某个类型变量 FRESH extends List&lt;Number&gt;

因此C&lt;Number, ?&gt;的捕获转化为C&lt;Number, FRESH&gt;

现在我们实际上可以回答这个问题了:DoubleList&lt;Number&gt; 是否可以分别分配给 NumberFRESH extends List&lt;Number&gt;?在前一种情况下,是的。在后一种情况下,没有。

这与我们自己以这种方式声明类型变量时表达式无法编译的原因相同:

static <FRESH extends List<Number>> void n() {
    C<Number, FRESH> c = new C<>();

    Double       tArg = 1.0;
    List<Number> uArg = new ArrayList<>();
    c.m(tArg, uArg);
}

The supertypes of a type variable are:

  • 类型变量的直接超类型是其绑定中列出的类型。

因此,List&lt;Number&gt; 可能分配给FRESH,因为List&lt;Number&gt;FRESH 的一个超类型

以此类推,我们也可以这样声明一个类:

class Fresh extends List<Number> {}
C<Number, Fresh> c = new C<>();

Double       tArg = 1.0;
List<Number> uArg = new ArrayList<>();
c.m(tArg, uArg);

这可能更熟悉,并且在这种情况下类型之间的关系如何工作并没有什么不同。

换句话说,在我们原来的例子中:

C<Number, ?> c = new C<>();

Double       tArg = 1.0;
List<Number> uArg = new ArrayList<>();
c.m(tArg, uArg);
//        ^^^^ this

只是一个更复杂的版本:

Object o = ...;
String s = o; // Error: attempting to assign a supertype to its subtype.

并且(在一天结束时)由于大致相同的原因无法编译。

总结

捕获转换采用通配符并将它们转换为类型变量(暂时)。之后,只是子类型化的常规规则会导致这些错误。

例如,给定问题中的代码:

private void addString(List<? extends String> list, String s) {
    list.add(s); // does not compile
    list.add(list.get(0)); // doesn't compile either
}

在查看表达式 list.add(s) 时,编译器会看到如下内容:

private <CAP#1 extends String>
void addString(List<? extends String> list, String s) {
    ((List<CAP#1>) list).add( s );
    list.add(list.get(0));
}

产生的错误如下:

error: no suitable method found for add(String)
        list.add(s); // does not compile
            ^
    method Collection.add(CAP#1) is not applicable
      (argument mismatch; String cannot be converted to CAP#1)
    method List.add(CAP#1) is not applicable
      (argument mismatch; String cannot be converted to CAP#1)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends String from capture of ? extends String

也就是说,编译器发现方法add(CAP#1)String不能转换为类型变量CAP#1

在查看表达式 list.add(list.get(0)) 时,编译器会看到如下内容:

private <CAP#1 extends String, CAP#2 extends String>
void addString(List<? extends String> list, String s) {
    list.add(s);
    ((List<CAP#2>) list).add( ((List<CAP#1>) list).get(0) );
}

产生的错误如下:

error: no suitable method found for add(CAP#1)
        list.add(list.get(0)); // doesn't compile either
            ^
    method Collection.add(CAP#2) is not applicable
      (argument mismatch; String cannot be converted to CAP#2)
    method List.add(CAP#2) is not applicable
      (argument mismatch; String cannot be converted to CAP#2)
  where CAP#1,CAP#2 are fresh type-variables:
    CAP#1 extends String from capture of ? extends String
    CAP#2 extends String from capture of ? extends String

也就是说,编译器发现list.get(0)返回CAP#1,发现方法add(CAP#2)CAP#1不能转换为CAP#2

(Source for errors.)

那么为什么List&lt;Class&lt;?&gt;&gt; 和其他类似类型有效?

回想一下:

  • 否则,[如果T<sub>i</sub> 不是通配符类型]S<sub>i</sub> = T<sub>i</sub>

还有:

捕获转换不会递归应用。

所以如果T<sub>i</sub> 是像Class&lt;?&gt; 这样的参数化类型,那么S<sub>i</sub> 就是Class&lt;?&gt;。此外,由于捕获转换不是递归应用的,因此算法在将T<sub>1</sub>,...,T<sub>n</sub> 转换为S<sub>1</sub>,...,S<sub>n</sub> 后停止。新类型未进行捕获转换,新类型变量的边界未进行捕获转换。

我们还可以通过引起一些有趣的错误来验证这确实是编译器所做的:

Map<?, List<?>> m = new HashMap<>();

List<?> list = new ArrayList<>();
list.add(m);

这会产生以下错误:

error: no suitable method found for add(Map<CAP#1,List<?>>)
        list.add(m);
            ^
    […]

(Source.)

请注意,Map 类型捕获中的类型参数 List&lt;?&gt; 会转换为自身。

还有一个:

Map<?, ? extends List<?>> m = new HashMap<>();

List<?> list = new ArrayList<>();
list.add(m);

这会产生以下错误:

error: no suitable method found for add(Map<CAP#1,CAP#2>)
        list.add(m);
            ^
    […]
  where CAP#1,CAP#2,CAP#3 are fresh type-variables:
    CAP#1 extends Object from capture of ?
    CAP#2 extends List<?> from capture of ? extends List<?>
    CAP#3 extends Object from capture of ?

(Source.)

请注意,这一次,? extends List&lt;?&gt; 是经过捕获转换的,而绑定的 List&lt;?&gt; 不是。

终于

上述问题的答案是List&lt;? extends String&gt; 中的通配符被捕获转换为新类型变量,但List&lt;Class&lt;? extends String&gt;&gt; 中的通配符不是。

【讨论】:

  • 我只想说这个答案是多么的难以置信有价值。感谢(多年后)您花时间解释这一点。
【解决方案3】:

您的示例忽略了一个事实(至少我是这么认为的),即(转到 IntegerNumber 现有示例)List&lt;Class&lt;Integer&gt;&gt; 不是List&lt;Class&lt;? extends Number&gt;&gt; 的有效实例。

所以,这不会编译:

public static void main(String[] args) {
    List<Class<Integer>> intClasses = new LinkedList<>();
    addClass(intClasses, Number.class); // compiler error
}

private static void addClass(List<Class<? extends Number>> list, Class<Number> c) {
    list.add(c);
    list.add(list.get(0));
}

【讨论】:

    猜你喜欢
    • 2023-03-10
    • 1970-01-01
    • 1970-01-01
    • 2019-08-17
    • 2020-03-11
    • 1970-01-01
    • 2020-08-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多