【问题标题】:How do generics of generics work?泛型的泛型如何工作?
【发布时间】:2013-05-03 05:19:43
【问题描述】:

虽然我确实了解泛型的一些极端情况,但我在以下示例中遗漏了一些东西。

我有以下课程

1 public class Test<T> {
2   public static void main(String[] args) {
3     Test<? extends Number> t = new Test<BigDecimal>();
4     List<Test<? extends Number>> l =Collections.singletonList(t);
5   }
6 }

第 4 行给了我错误

Type mismatch: cannot convert from List<Test<capture#1-of ? extends Number>> 
to List<Test<? extends Number>>`. 

显然,编译器认为不同的? 并不真正相等。虽然我的直觉告诉我,这是正确的。

谁能提供一个例子,如果第 4 行是合法的,我会得到一个运行时错误?

编辑:

为避免混淆,我将第 3 行中的 =null 替换为具体赋值

【问题讨论】:

  • 你可以通过写Collections.&lt;Test&lt;? extends Number&gt;&gt; singletonList(t)来编译。
  • 通过使用?,您告诉编译器它们不一定相等,如果您希望它们被视为相等,您可以使用T 或其他占位符类型
  • KennyTM:现在我完全糊涂了。这可行,但为什么呢?
  • @Jonathan,我的猜测是在您想要传递不同类型的情况下,使用T 和拥有T extends Number 可以更好地服务每个扩展数字(而不是让事情爆炸)。看起来您正在寻找灵活性来“做正确的事情”,引入了泛型(集合只是对象的集合)来整理(强制执行一些类型安全的表象)

标签: java generics wildcard capture


【解决方案1】:

原因是编译器不知道你的通配符类型是相同的类型。

它也不知道您的实例是null。尽管null 是所有类型的成员,但编译器在类型检查时只考虑声明的类型,而不考虑变量的 可能包含的内容。

如果代码被执行,它不会导致异常,但这只是因为值是空的。仍然存在潜在的类型不匹配,这就是编译器的工作 - 禁止类型不匹配。

【讨论】:

  • 正如我已经写过的,我明白,这有潜在的危险。问题是,如果该行是合法的,是否存在运行时错误的示例
【解决方案2】:

看看type erasure。问题是“编译时间”是 Java 必须强制执行这些泛型的唯一机会,所以如果它让它通过它就无法判断您是否尝试插入无效的东西。这实际上是一件好事,因为这意味着一旦程序编译,泛型就不会在运行时产生任何性能损失。

让我们尝试以另一种方式查看您的示例(让我们使用两种扩展 Number 但行为非常不同的类型)。考虑以下程序:

import java.math.BigDecimal;
import java.util.*;

public class q16449799<T extends Number> {
  public T val;

  public static void main(String ... args) {
    q16449799<BigDecimal> t = new q16449799<>();
    t.val = new BigDecimal(Math.PI);

    List<q16449799<BigDecimal>> l = Collections.singletonList(t);
    for(q16449799<BigDecimal> i : l) {
      System.out.println(i.val);
    }
  }
}

这个输出(正如人们所期望的那样):

3.141592653589793115997963468544185161590576171875

现在假设您提供的代码没有导致编译器错误:

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicLong;

public class q16449799<T extends Number> {
  public T val;

  public static void main(String ... args) {
    q16449799<BigDecimal> t = new q16449799<>();
    t.val = new BigDecimal(Math.PI);

    List<q16449799<AtomicLong>> l = Collections.singletonList(t);
    for(q16449799<AtomicLong> i : l) {
      System.out.println(i.val);
    }
  }
}

您希望输出是什么?您无法合理地将 BigDecimal 强制转换为 AtomicLong(您可以从 BigDecimal 的值构造 AtomicLong,但强制转换和构造是不同的事情,泛型被实现为编译时糖以确保强制转换成功)。至于@KennyTM 的评论,当您最初的示例时正在寻找一个具体的类型,但尝试编译它:

import java.math.BigDecimal;
import java.util.*;

public class q16449799<T> {
  public T val;

  public static void main(String ... args) {
    q16449799<? extends Number> t = new q16449799<BigDecimal>();

    t.val = new BigDecimal(Math.PI);

    List<q16449799<? extends Number>> l = Collections.<q16449799<? extends Number>>singletonList(t);
    for(q16449799<? extends Number> i : l) {
      System.out.println(i.val);
    }
  }
}

当您尝试将值设置为 t.val 时,这将出错。

【讨论】:

  • 装箱/拆箱费用如何?此外,其他语言中使用的“真实”泛型的性能损失是什么?
  • 装箱/拆箱仅涉及从原始到对象的转换(适用于数字并具有运行时间成本)。 Java 中的泛型是在 Java 的类型系统之上实现的,并且惊人地向后兼容(retroweaver 通过向类路径添加 jar 将它们添加到 Java 1.4(您仍然必须使用 java 1.5 来编译它们)。我可以'不过不会说其他语言。
  • 正如我已经写过的:我确实对代码有一种不好的直觉,但我想不出具体的例子会出错。那么,有吗?
  • @JasonSperske 对,抱歉,选词不当。我的意思是向上转型/向下转型。我相信具有类型擦除的泛型通过将输入转换为Object,然后从Object 转换任何输出来工作(正是为了保持您所说的兼容性)。那正确吗?在这种情况下,与诸如 C# 之类的语言相比,这种转换不会产生开销吗?它们实际上是 JIT 编译泛型类的不同具体实现并且不会在两者之间进行任何转换?
【解决方案3】:

也许这可以解释编译器的问题:

List<? extends Number> myNums = new ArrayList<Integer>();

这个通用通配符列表可以包含从 Number 扩展而来的任何元素。所以可以给它分配一个整数列表。但是现在我可以将 Double 添加到 myNums 因为 Double 也是从 Number 扩展的,这会导致运行时问题。所以编译器禁止对 myNums 的每次写访问,我只能对它使用 read 方法,因为我只知道我得到的可以转换为 Number。

所以编译器抱怨你可以用这样的通配符泛型做很多事情。有时他会对你可以确保它们安全无虞的事情感到愤怒。

但幸运的是,有一个技巧可以解决此错误,因此您可以自行测试可能会破坏此错误的方法:

public static void main(String[] args) {

    List<? extends Number> list1 = new ArrayList<BigDecimal>();
    List<List<? extends Number>> list2 = copyHelper(list1);


}

private static <T> List<List<T>> copyHelper(List<T> list) {
    return Collections.singletonList(list);

}

【讨论】:

  • 我知道该代码,它在 Oracle 教程中的某个地方,不幸的是,它不适用于泛型的泛型。我尝试了我的示例和您的代码,在这两种情况下我都得到了cannot convert 错误
  • 是的,你是对的。我的 IDE 不再抱怨它,但是当我制作整个项目时,我也得到了错误(使用 Java 1.7)。也许他们通过一些 Java 更新解决了这个问题。然而问题在于“?”,它不能用于创建对象(这就是 singeltonList 试图做的)。
【解决方案4】:

没有潜在的运行时错误,只是编译器无法静态确定。每当您进行类型推断时,它会自动生成一个新的&lt;? extends Number&gt; 捕获,并且两个捕获不被认为是等效的。

因此,如果您通过为其指定 &lt;T&gt; 从调用 singletonList 中删除推理:

List<Test<? extends Number>> l = Collections.<Test<? extends Number>>singletonList(t);

它工作正常。生成的代码与您的调用合法时没有什么不同,这只是编译器的一个限制,它无法自行解决。

推理创建捕获和捕获不兼容的规则是阻止本教程示例编译然后在运行时崩溃的原因:

public static void swap(List<? extends Number> l1, List<? extends Number> l2) {
    Number num = l1.get(0);
    l1.add(0, l2.get(0));
    l2.add(0, num);
}

是的,语言规范和编译器可能会变得更复杂,以区分您的示例,但事实并非如此,而且它很简单,可以解决。

【讨论】:

    【解决方案5】:

    正如肯尼在他的评论中指出的那样,您可以通过以下方式解决这个问题:

    List<Test<? extends Number>> l =
        Collections.<Test<? extends Number>>singletonList(t);
    

    这立即告诉我们该操作并非不安全,它只是有限推理的受害者。如果它不安全,则上述内容将无法编译。

    由于在上述泛型方法中使用显式类型参数只需要充当提示,我们可以推测这里需要它是推理引擎的技术限制。事实上,Java 8 编译器目前预定与many improvements to type-inference 一起发布。我不确定您的具体情况是否会得到解决。

    那么,到底发生了什么?

    好吧,我们得到的编译错误表明Collections.singletonList 的类型参数T 被推断为capture&lt;Test&lt;? extends Number&gt;&gt;。换句话说,通配符有一些与之关联的元数据,将其链接到特定的上下文。

    • 考虑捕获通配符 (capture&lt;? extends Foo&gt;) 的最佳方式是将其视为具有相同边界的 未命名 类型参数(即 &lt;T extends Foo&gt;,但无法引用 @987654329 @)。
    • “释放”捕获功能的最佳方法是将其绑定到泛型方法的命名类型参数。我将在下面的示例中对此进行演示。请参阅 Java 教程 "Wildcard Capture and Helper Methods"(感谢 @WChargin 的参考)进一步阅读。

    假设我们想要一个移动列表的方法,将其换行到后面。那么让我们假设我们的列表有一个未知(通配符)类型。

    public static void main(String... args) {
        List<? extends String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
        List<? extends String> cycledTwice = cycle(cycle(list));
    }
    
    public static <T> List<T> cycle(List<T> list) {
        list.add(list.remove(0));
        return list;
    }
    

    这很好用,因为T 被解析为capture&lt;? extends String&gt;,而不是? extends String。如果我们改为使用这种非泛型的循环实现:

    public static List<? extends String> cycle(List<? extends String> list) {
        list.add(list.remove(0));
        return list;
    }
    

    它将无法编译,因为我们没有通过将捕获分配给类型参数来使其可访问。

    所以这开始解释为什么singletonList 的使用者会受益于类型推断器将T 解析为Test&lt;capture&lt;? extends Number&gt;,从而返回List&lt;Test&lt;capture&lt;? extends Number&gt;&gt;&gt; 而不是List&lt;Test&lt;? extends Number&gt;&gt;

    但为什么不能将一个分配给另一个?

    为什么我们不能将List&lt;Test&lt;capture&lt;? extends Number&gt;&gt;&gt; 分配给List&lt;Test&lt;? extends Number&gt;&gt;

    好吧,如果我们考虑capture&lt;? extends Number&gt; 相当于一个匿名类型参数,其上限为Number,那么我们可以把这个问题变成“为什么下面的代码不能编译?” (它没有!):

    public static <T extends Number> List<Test<? extends Number>> assign(List<Test<T>> t) {
        return t;
    } 
    

    这有一个很好的理由不编译。如果是这样,那么这将是可能的:

    //all this would be valid
    List<Test<Double>> doubleTests = null;
    List<Test<? extends Number>> numberTests = assign(doubleTests);
    
    Test<Integer> integerTest = null;
    numberTests.add(integerTest); //type error, now doubleTests contains a Test<Integer>
    

    那么为什么显式有效呢?

    让我们回到开头。如果上面不安全,那怎么会允许:

    List<Test<? extends Number>> l =
        Collections.<Test<? extends Number>>singletonList(t);
    

    为此,它意味着允许以下操作:

    Test<capture<? extends Number>> capturedT;
    Test<? extends Number> t = capturedT;
    

    好吧,这不是有效的语法,因为我们不能明确地引用捕获,所以让我们使用与上面相同的技术来评估它!让我们将捕获绑定到“assign”的不同变体:

    public static <T extends Number> Test<? extends Number> assign(Test<T> t) {
        return t;
    } 
    

    这编译成功。不难看出为什么它应该是安全的。这就是

    之类的用例
    List<? extends Number> l = new List<Double>();
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-07-25
      • 1970-01-01
      相关资源
      最近更新 更多