这个问题只是问为什么不使用通用强制转换。关于擦除的问题似乎并不能解释这种边缘情况。
在开始之前,让我先说一下我对类型推断不感兴趣,如下所述:
这只是为了避免混淆。
我感兴趣的是为什么下面的代码可以工作而不会抛出 ClassCastException
?
import java.sql.SQLException;
public class GenericThrows {
static <T extends Exception> void test(Exception d) throws T {
throw (T) d;
}
public static void main(String[] args) {
GenericTest.<RuntimeException>test(new SQLException());
}
}
使用以下命令编译代码:
javac -source 1.7 -target 1.7 GenericThrows.java
它产生:
Exception in thread "main" java.sql.SQLException
at GenericTest.main(GenericTest.java:9)
我对 Java 泛型和类型擦除的心理模型(以及为什么我认为这没有意义):
当静态方法编译时:
static <T extends Exception> void test(Exception d) throws T {
throw (T) d;
}
类型擦除会清除所有泛型类型并将它们替换为给定类型的上限,因此该方法实际上变为:
static void test(Exception d) throws Exception {
throw (Exception) d;
}
我希望我是对的。
main方法编译时:
static <T extends Exception> void test(Exception d) throws T {
throw (T) d;
}
public static void main(String[] args) {
GenericTest.<RuntimeException>test(new SQLException());
}
类型参数被具体类型替换:java.lang.RuntimeException
。
因此该方法实际上变为:
static void test(Exception d) throws RuntimeException {
throw (RuntimeException) d;
}
我希望我是对的。
因此,当我尝试将 a 转换SQLException
为 a时,RuntimeException
我应该得到 a ClassCastException
,这正是如果我编写不带泛型的代码时会发生的情况:
import java.sql.SQLException;
public class NonGenericThrows {
static void test(Exception d) throws RuntimeException {
throw (RuntimeException) d;
}
public static void main(String[] args) {
NonGenericThrows.test(new SQLException());
}
}
编译与执行:
javac -source 1.7 -target 1.7 NonGenericThrows.java
java NonGenericThrows
结果:
Exception in thread "main" java.lang.ClassCastException: class java.sql.SQLException cannot be cast to class java.lang.RuntimeException (java.sql.SQLException is in module java.sql of loader 'platform'; java.lang.RuntimeException is in module java.base of loader 'bootstrap')
at NonGenericThrows.test(NonGenericThrows.java:5)
at NonGenericThrows.main(NonGenericThrows.java:9)
那么为什么通用版本没有给出 ClassCastException ?
我的思维模式哪里出了问题?
你的误解在这里:
这不是 Java 中泛型的工作方式 - 该
test()
方法不会重新编译,也不会以任何方式进行更改。方法是:由于
T
can by anyException
,该强制转换(T) d
实际上是一个无操作,并且编译器不会为此强制转换生成任何代码(它不能执行强制转换:调用者不会传入类型T
,并且代码必须适用于 allT extends Exception
,但是无论如何d
,它必然是一个Exception
,因为这是参数的声明类型。)编译器仅检查调用站点。传递的参数 (
SQLException
) 是 的一个Exception
或其子类吗Exception
?是的,SQLException
是 的子类Exception
,因此调用可以编译。类型擦除
您是正确的,类型变量被删除到最左边的边界。Java 语言规范(JLS)的第 4.6 节类型擦除对此进行了说明。
这意味着删除:
是:
但请注意,这仅依赖于类型参数。类型参数是无关紧要的。与 C++ 等语言不同,模板的每个唯一参数化都会生成唯一的编译代码,而 Java 只编译泛型一次。因此,如果您使用以下命令调用上述内容:
您最终不会得到
T
's 边界更改为 的字节代码RuntimeException
。最左边的边界仍然是Exception
。铸件
来自JLS 的§5.5 铸造上下文:
以及来自§5.1.6.2 检查和未检查缩小参考转换:
以及来自§5.1.6.3 在运行时缩小引用转换:
考虑到所有这些,如下:
是一个完全未经检查的演员阵容。因此,没有检查运行时。您可以通过查看字节码来验证这一点。
您将泛型与模板(类似于 C++)混淆了。您提到类型擦除是正确的,顾名思义,它会擦除编译时类型信息的任何痕迹。
因此,你的转换并没有真正发生,或者即使发生了,它也不是你期望的类型。
您需要使用
Class#cast
来获得所需的行为,但是获取实例Class<T>
对于您的用例来说并不方便。