Java generics:为什么这个输出是可能的?

Java generics: why is this output possible?

我有这个class:

class MyClass<N extends Number> {
    N n = (N) (new Integer(8));
}

我想获得这些输出:

System.out.println(new MyClass<Long>().n);
System.out.println(new MyClass<Long>().n.getClass());
  1. 第一个 System.out.println() 语句的输出:

    8
    
  2. 第二个 System.out.println() 语句的输出:

    java.lang.ClassCastException: java.lang.Integer (in module: java.base)
        cannot be cast to java.lang.Long (in module: java.base)
    

为什么我得到第一个输出?不是也有演员表吗?为什么我在第二个输出中得到异常?

PS:我用Java9;我用 JShell 试了一下,两个输出都出现异常。然后我用 IntelliJ IDE 尝试了它并得到了第一个输出但第二个是异常。

发生这种情况是因为您已经将 n 定义为整数对象,因此它不会将其转换为 long

要么在 sysout 的 MyClass 中使用整数,例如

System.out.println(new MyClass<Integer>().n);

或将 n 定义为:N n =(N)(new Long(8));.

IntelliJ 显示的行为对我来说很清楚:

您在 MyClass 中有一个未经检查的转换。这意味着 new Integer(8) 不会立即转换为 Long 而是转换为擦除 Number (有效),当执行此行时: N n =(N)(new Integer(8));

现在让我们看看输出语句:

System.out.println(new MyClass<Long>().n);

归结为 String.valueOf(new MyClass<Long>().n) -> ((Object)new MyClass<Long>().n).toString() 效果很好,因为 n 是通过 Object 访问的,而且 toString() 方法是通过静态类型 Object -> 不会强制转换为 Longnew MyClass<Long>().n.toString() 会失败并出现异常,因为 toString() 试图通过静态类型 Long 访问。因此,将 n 强制转换为类型 Long 是不可能的(Integer 不能强制转换为 Long)。

执行第二条语句时发生同样的事情:

System.out.println(new MyClass<Long>().n.getClass()); 

试图通过静态类型Long访问Long类型的getClass方法(在Object中声明)。因此,将 n 强制转换为类型 Long 会产生强制转换异常。

JShell 行为:

我试图在 JShell 上重现第一个输出语句的结果异常 - Java 9 抢先体验 Build 151:

jshell> class MyClass<N extends Number> {
   ...>     N n = (N) (new Integer(8));
   ...> }
|  Warning:
|  unchecked cast
|    required: N
|    found:    java.lang.Integer
|      N n = (N) (new Integer(8));
|                ^--------------^
|  created class MyClass

jshell> System.out.println(new MyClass<Long>().n);
8

jshell> System.out.println(new MyClass<Long>().n.getClass());
|  java.lang.ClassCastException thrown: java.base/java.lang.Integer cannot be cast to java.base/java.lang.Long
|        at (#4:1)

但 JShell 似乎给出了与 IntelliJ 完全相同的结果。 System.out.println(new MyClass<Long>().n); 输出 8 - 无一例外。

这是因为 Java 擦除。

由于 Integer 扩展了 Number,编译器接受到 N 的转换。在运行时,由于 NNumber 替换(由于擦除),因此将 Integer 存储在 n.

中没有问题

方法System.out.println的参数是Object类型所以打印n的值没有问题。

但是,在 n 上调用方法时,编译器会添加类型检查以确保调用正确的方法。因此导致 ClassCastException.

异常和无异常都是允许的行为。基本上,这归结为编译器如何擦除语句,是否是像这样没有强制转换的东西:

System.out.println(new MyClass().n);
System.out.println(new MyClass().n.getClass());

或者类似这样的强制转换:

System.out.println((Long)new MyClass().n);
System.out.println(((Long)new MyClass().n).getClass());

或一对一陈述和另一个陈述。两个版本都是可以编译的有效 Java 代码。问题是编译器是否允许编译成一个版本、另一个版本或两者。

允许在此处插入强制转换,因为当您从类型为类型变量的通用上下文中获取某些内容,然后 return 将其放入类型变量采用的上下文时,通常会发生这种情况在特定类型上。例如,您可以将 new MyClass<Long>().n 分配给类型为 Long 的变量而不进行任何转换,或者将 new MyClass<Long>().n 传递到期望 Long 的地方而不进行任何转换,这两种情况显然需要编译器插入一个强制转换。当你有 new MyClass<Long>().n 时,编译器可以决定总是插入一个转换,这样做并没有错,因为表达式应该具有类型 Long.

另一方面,在这两个语句中不进行强制转换也是允许的,因为在这两种情况下表达式都在可以使用任何 Object 的上下文中使用,因此不进行强制转换需要使其编译并保持类型安全。此外,在这两个语句中,如果值确实是 Long,则强制转换或不强制转换不会对行为产生影响。在第一个语句中,它被传递给采用 Object.println() 版本,并且没有更具体的采用 LongNumberprintln 重载] 或类似的东西,因此无论参数被视为 Long 还是 Object,都会选择相同的重载。对于第二个语句,.getClass()是由Object提供的,所以无论左边的东西是Long还是Object,它都是可用的。由于擦除的代码在有和没有强制转换的情况下都是有效的,并且在有和没有强制转换的情况下行为都是相同的(假设这个东西确实是 Long),编译器可以选择优化强制转换。

编译器甚至可以在一种情况下进行强制转换而在另一种情况下不进行强制转换,这可能是因为它只优化了某些简单情况下的强制转换,但在更复杂的情况下却懒得执行分析。我们不需要详述为什么特定编译器决定为特定语句编译成一种或另一种形式,因为这两种形式都是允许的,您不应该依赖它以一种或另一种方式工作。