处理泛型时,编译器给出的期望是无用的吗?

7
一个编译器必须翻译一个通用类型或方法(不仅限于Java),原则上有两种选择:

代码特化。编译器为每个通用类型或方法的实例生成新的表示。例如,编译器将为整数列表生成代码,并为字符串列表、日期列表、缓冲区列表等生成不同的代码。

代码共享。编译器仅为一个通用类型或方法的表示生成代码,并将所有通用类型或方法的实例映射到唯一的表示中,在需要时执行类型检查和转换。

Java使用代码共享方法。我认为C#遵循代码特化方法,因此以下所有代码都是基于我使用C#的逻辑。

假设这是Java的代码片段:

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是什么? - Bhesh Gurung
t 只是 Test 示例类的一个实例。它只是上面整个代码的一部分。 - Mik378
2个回答

3
你的推理有两个问题。
一个问题是显式转换既是编译时功能(静态类型系统的一部分),也是运行时功能(动态类型系统的一部分)。在编译时,它们将一个静态类型的表达式转换为另一个静态类型的表达式。在运行时,它们确保类型安全,通过强制要求动态类型实际上是该静态类型的子类型来实现。当然,在你的例子中忽略了运行时特性,因为擦除意味着没有足够的信息在运行时执行转换。但编译时功能仍然是相关的。
考虑这个方法:
private void printInt(Number n)
{
    Integer i = (Integer) n;
    System.out.println(i + 10);
}

您认为以下内容是否有效:

(注:此处为HTML代码,无法直接翻译)
Object o = 47;
printInt(o);            // note: no cast to Number

基于 foo 会立即将其参数强制转换为 Integer,所以没有必要要求调用者将其转换为 Number,这是你的论据。但是,你的推理中存在另一个问题,尽管类型擦除和未检查强制转换会牺牲一些类型安全性,但编译器通过发出警告来补偿这种牺牲。如果你编写的 Java 程序没有产生任何未检查(或原始类型)警告,则可以确保它不会由于隐式、仅在运行时生成的编译器生成的向下转换而引发任何 ClassCastException(当然,如果你正在抑制此类警告除外)。在你的示例中,你有一个声称是通用的方法,并且声称其返回类型与其参数类型相同。通过提供对 T[] 的显式转换,你为编译器提供了机会发出警告,并告诉你它无法在该位置强制执行该声明。如果没有这样的转换,就没有地方可以警告调用方法可能导致的 ClassCastException

实际上,我认为有两种思考方式。我选择了错误的一种。我选择只考虑“为什么缺少转换会阻止编译器完成工作?!”这个推理,而不是更关注用户代码的安全性以及警告他的重要性 ;) - Mik378
我不太明白这个。我认为我误解的根源是:为什么a.getClass()的类型与Class<? extends T[]>不同? - Samuel Edwin Ward
@SamuelEdwinWard:getClass()有一些神奇;它的返回类型仅为Class<?>,但规范的第4.3.2节定义了由其调用组成的表达式的静态类型。具体而言,它表明这种表达式foo.getClass()的静态类型是Class<? extends |T|>,其中Tfoo的静态类型,||是一种表示大致上“在剥离泛型之后”的符号。换句话说,例如,如果strList的类型为List<String>,则strList.getClass()的类型为Class<? extends List>。这在考虑*[续]*时是有意义的。 - ruakh
继续上文,实际的Class实例将是List.class,而不是某种类型特定的List<String>.class。由于这个规则,TT[]被削减为ObjectObject[]。当你考虑到T可能代表(比如)List<String>时,这有点说得通,这意味着Class<? extends T>表示Class<? extends List<String>>,即使“正确”的静态类型应该是Class<? extends List>。如果没有这个规则,你可以编写一个方法<T> Class<? extends T> getClass(T t),绕过将List<String>削减为List的操作。 - ruakh

2
假设(概念上)"a" 的类型为 List<String>[]
我们无法获取 a 的完整类型。我们使用 a.getClass(),它返回 List[]。副本也是 List[],您想将其转换为 List<String>[]。编译器无法推断出转换是安全的。您可以理解,因为复制品中的所有元素都是 List<String>。您比编译器更了解,因此需要显式转换,并且有权抑制“未经检查”的警告。
即使您的代码(像许多未经检查的转换一样)在今天的Java平台上运行得很好,但从理论上讲,它是错误的。编译器发出警告并不是轻率的。但我们别无选择。
根本冲突是对擦除的态度。
编译器生活在理想世界中,好像所有类型都是完整类型。编译器不承认已擦除的类型。有这样微弱的希望,即Java有一天将通过放弃擦除来实现理想的类型系统,因此编译器今天不基于擦除的假设工作。
我们程序员生活在已擦除的世界中。我们必须使用已擦除的类型,并将它们假装成完整的类型,因为我们无法访问真正的完整类型。
我们的代码只在已擦除的世界中起作用;如果Java有一天摆脱了擦除,我们的代码将全部失效。将 List[] 强制转换为 List<String>[]?荒谬!不允许!
但是今天我们别无选择。依赖于擦除的代码是无处不在的。如果Java想要摆脱擦除,这是一个巨大的问题。Java可能永远不会这样做。它被诅咒了。

好的解释。我也在我的帖子中添加了一个。谢谢。 - Mik378

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接