【问题标题】:Useless expectation from compiler when dealing with generics?处理泛型时编译器的无用期望?
【发布时间】:2012-11-30 00:35:17
【问题描述】:

必须翻译泛型类型或方法(任何语言,而不仅仅是 Java)的编译器原则上有两种选择:

代码专业化。编译器为 泛型类型或方法的每个实例化。例如, 编译器将生成整数列表和其他代码, 字符串列表、日期列表、日期列表的不同代码 缓冲区等。

代码共享。编译器只为一种表示生成代码 泛型类型或方法,并映射 泛型类型或方法的唯一表示,执行类型 在需要时检查和类型转换。

Java 使用代码共享方法。我相信 C# 遵循 code specialization 方法,所以根据我使用 C# 的方式,下面的所有代码都是合乎逻辑的。

假设这个Java代码sn-p:

public class Test {

    public static void main(String[] args) {
        Test t = new Test();
        String[] newArray = t.toArray(new String[4]);
    }

    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        //5 as static size for the sample...
        return (T[]) Arrays.copyOf(a, 5, a.getClass());
    }
}

代码共享方式类型擦除发生后会导致此代码

public class Test {

    public static void main(String[] args) {
       Test t = new Test();
       //Notice the cast added by the compiler here
       String[] newArray = (String[])t.toArray(new String[4]);
    }

    @SuppressWarnings("unchecked")
    public Object[] toArray(Object[] a) {
       //5 as static size for the sample...
       return Arrays.copyOf(a, 5, a.getClass());
    }
}

所以我的问题是:

需要精确这个初始演员阵容吗? :

(T[]) Arrays.copyOf(a, 5, a.getClass());

而不是简单地做(在​​类型擦除之前,在编码时):

Arrays.copyOf(a, 5, a.getClass());

编译器真的需要这种转换吗?

好的,Arrays.copyOf 返回Object[] 并且在没有明确向下转换的情况下不能被更具体的类型直接引用。

但是编译器不能在这种情况下做出努力,因为它处理的是泛型类型(返回类型!)?

确实,编译器对方法的调用行应用显式强制转换还不够? :

(String[])t.toArray(new String[4]);

已更新------------------------------- --------------------------

感谢@ruakh 的回答。

这里有一个示例,它证明了即使只是在编译时出现的显式转换也是相关的:

public static void main(String[] args) {
   Test t = new Test();
   String[] newArray = t.toArray(new String[4]);
}


public <T> T[] toArray(T[] a) {
   return (T[]) Arrays.copyOf(a, 5, Object[].class);
}

投射到T[] 是向用户发出警告的唯一方法,表明投射可能不相关。事实上,这里我们最终将Object[] 向下转换为String[],这导致在运行时ClassCastException

所以,说到“编译器对方法的调用行应用显式转换还不够”,答案是:

开发人员不掌握这种转换,因为它是在编译步骤自动创建的,因此此运行时功能不会警告用户在启动编译之前深入检查其代码的安全性。

简而言之,这个演员阵容值得出席。

【问题讨论】:

  • 上一个示例中的t 是什么?
  • t 只是Test 示例类的实例。它只是上面整个代码的摘录。

标签: c# java generics compiler-construction casting


【解决方案1】:

你的推理有两个问题。

一个问题是显式转换既是编译时特性(静态类型系统的一部分)又是运行时特性(动态类型系统的一部分)。在编译时,它们将一种静态类型的表达式转换为另一种静态类型的表达式。在运行时,它们通过强制要求动态类型实际上是该静态类型的子类型来确保类型安全。当然,在您的示例中,运行时功能被跳过,因为擦除意味着没有足够的信息来在运行时强制强制转换。但是编译时特性仍然是相关的。

考虑这种方法:

private void printInt(Number n)
{
    Integer i = (Integer) n;
    System.out.println(i + 10);
}

您认为以下内容应该有效吗:

Object o = 47;
printInt(o);            // note: no cast to Number

因为foo 无论如何都会立即将其参数转换为Integer,因此无需要求调用者将其转换为Number

您的推理路线的另一个问题是,尽管擦除和未经检查的强制转换确实牺牲了一点类型安全性,但编译器通过发出警告来补偿这种牺牲。如果您编写的 Java 程序没有给出任何未经检查(或原始类型)警告,那么您可以确定它不会因为隐式的、仅运行时的、编译器生成的向下转换而抛出任何 ClassCastExceptions . (我的意思是,当然,除非您禁止此类警告。)在您的示例中,您有一个 声称 是通用的方法,并且 声称 它返回-type 与其参数类型相同。通过向T[] 提供显式强制转换,您可以让编译器有机会发出警告并告诉您它无法在该位置强制执行该声明。如果没有这样的演员表,就没有地方警告调用方法中可能产生的ClassCastException

【讨论】:

  • 其实我觉得有两种思路。我选错了。我选择了仅处理“为什么缺少强制转换会阻止编译器完成其工作?!”的推理。而不是关注用户的代码安全和警告他的重要性;)
  • 我不太明白。我想我误解的根源是:为什么a.getClass()的类型和Class&lt;? extends T[]&gt;的类型不一样?
  • @SamuelEdwinWard:getClass() 有点神奇;它的返回类型只是Class&lt;?&gt;,但规范的第 4.3.2 节定义了由调用它组成的表达式的静态类型。具体来说,它表示这种表达式foo.getClass() 的静态类型是Class&lt;? extends |T|&gt;,其中Tfoo 的静态类型,|| 是一个符号,大致意思是“在你剥离之后出泛型”。换句话说,例如,如果strList 的类型为List&lt;String&gt;,那么strList.getClass() 的类型为Class&lt;? extends List&gt;。当您考虑 [continued] 时,这是有道理的
  • [继续] Class 的实际实例将是 List.class,而不是某种特定类型的 List&lt;String&gt;.class。而且,由于这条规则,TT[] 被剥离为 ObjectObject[]。当您考虑到T 可以代表(例如)List&lt;String&gt; 时,这种排序是有意义的,在这种情况下,Class&lt;? extends T&gt; 表示Class&lt;? extends List&lt;String&gt;&gt;,即使“正确的”静态类型是Class&lt;? extends List&gt;。如果没有这个,你可以编写一个方法&lt;T&gt; Class&lt;? extends T&gt; getClass(T t),绕过将List&lt;String&gt; 剥离到List
【解决方案2】:

假设(概念上)"a" 的类型是List&lt;String&gt;[]

我们无法获得a 的完整类型。我们求助于a.getClass(),它返回List[]。该副本也是List[],您想将其转换为List&lt;String&gt;[]。编译器无法推断强制转换是安全的。您可以推断,因为副本中的所有元素都是List&lt;String&gt;。您比编译器更了解,因此您需要显式强制转换,并且您有理由禁止“未检查”警告。

尽管您的代码,就像许多未经检查的强制转换一样,在当今的 Java 平台上完美运行,但 理论上是错误的。编译器在发出警告时并不轻率。但我们别无选择。

根本冲突是对擦除的态度。

编译器生活在理想世界中,就好像所有类型都是完整类型一样。编译器不确认已擦除类型。有一种微弱的希望,希望有一天 Java 会通过删除擦除来实现理想的类型系统,因此今天的编译器不会在擦除的假设下工作。

我们程序员生活在被抹去的世界中。我们必须使用已擦除的类型,并假装它们是完整类型,因为我们无法访问真正的完整类型。

我们的代码只适用于被抹去的世界;如果有一天 Java 摆脱了擦除,我们的代码都会崩溃。将List[] 转换为List&lt;String&gt;[]?废话!不允许!

但我们今天别无选择。 依赖擦除的代码无处不在。如果 Java 想要摆脱擦除,那将是一个大问题。 Java 可能永远不会这样做。它被诅咒了。

【讨论】:

  • 很好的解释。我还在上面的帖子中添加了一个。谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-17
  • 1970-01-01
  • 2013-10-15
  • 1970-01-01
  • 2014-07-25
相关资源
最近更新 更多