为什么 Java 在涉及类型参数时无法找出一些明显的非法转换?
Why Java couldn't figure out some obvious illegal casts when type parameters are involved?
考虑以下示例:
public class Example {
public static <T> void f(T obj) {
Integer i = (Integer) obj; // runtime error
}
public static void main(String[] args) {
f("hello");
}
}
Java有什么理由无法弄清楚第 3 行中的强制转换在编译时是非法的吗?当然,由于类型擦除,函数在运行时的签名将是 f(Object obj)
,但在我看来,在编译时它有足够的信息来捕获错误。
对比案例:
List<String> ls = new ArrayList<>();
ls.add(42); // compile-time error
ls.add("foo");
Integer i = ls.get(0); // compile-time error
其中涉及类型参数但在编译时成功检测到错误。
如果答案是“编译器不够智能”,那么有什么理由(为了向后兼容?)为什么它不能变得更智能?
说明
Java,根据其当前规则集(参见 JLS)必须处理 方法内容 及其 调用站点 分别.
方法内容
演员阵容
(Integer) obj
必须在编译时允许,因为 T
可能是 Integer
。毕竟,像
这样的电话
f(4)
应该成功并被允许。
Java 不允许考虑方法的调用位置。此外,这意味着 Java 必须扫描 所有呼叫站点 但这是不可能的,因为这还包括可能的 未来呼叫站点 尚未编写或稍后包含的内容,以防您正在编写库。
调用站点
调用站点也必须是合法的,因为 Java 不允许考虑方法内容。
签名要求 T
(extends Object
) 并且 String
满足。所以允许这样称呼。
如果Java 也会检查内容,想象一下你会在其他一些方法调用中隐藏 cast 3 levels depper。然后 Java 不仅要检查 f
的代码,还要检查那些方法的代码,可能还要检查它们所有的 if
语句,以检查是否达到了错误转换的行。
在编译时以 100% 的确定性证明它是 NP-hard,因此它也不属于规则集。
为什么?
虽然我们看到这种情况并不总是很容易检测到,而且在所有可能的情况下实际证明它甚至可能 不可能(NP-hard),Java 设计师当然可以添加一些较弱的规则来部分涵盖危险情况。
此外,实际上有一些类似的情况 Java 会帮助您解决较弱的规则。例如像
这样的演员表
House a = new House();
Dog b = (Dog) a;
被禁止,因为 Java 可以很容易地证明类型完全不相关。但是一旦设置变得更加复杂,类型来自其他方法、泛型,Java 就无法再轻松地检查它了。
总而言之,您必须向 Java 语言设计师询问决策制定的确切原因。就是这样。
静态代码分析
你在这里所拥有的通常是静态代码分析器的工作(就像大多数 IDE 已经包含的那样)。他们实际上会扫描您的代码、所有用法等,并尝试找出您当前的代码流是否存在问题。
重要的是要注意,这也包括很多误报,因为我们刚刚了解到并非所有此类用法实际上都是错误的,一些危险的设置可能是故意的。
附录:评论
根据评论中的讨论,让我强调一个事实,即您的特定示例确实很容易被证明是错误的。因此,在这种特殊情况下,调用站点 很容易被禁止 (任何静态代码分析器都会很高兴地针对此代码向您发出警告)。
但是,我们可以对代码进行非常简单的修改,这已经说明了为什么在将调用站点与方法内容连接时实际证明错误如此困难。
所以 tldr 是几乎所有真实的代码情况都需要更多的努力才能让工具 100% 证明调用是不正确的。此外,对此进行编程要困难得多,并且不能始终确保没有误报。这就是为什么这些东西通常不是由编译器完成,而是由静态代码分析器完成的原因。
这两个常见的例子是方法嵌套和代码分支。
嵌套
假设你用另一种方法隐藏演员表 (Integer) obj
一个水平下降:
public static void main(String[] args) {
f("hello");
}
public static <T> void f(T obj) {
g(obj);
}
public static <T> void g(T obj) {
Integer i = (Integer) obj;
}
为了证明这一点,Java 现在必须将调用站点从 main
连接到 f
中的内容,再连接到 [=25] 中的调用站点=].如果您添加更多级别的嵌套,这很快就会失控,需要在证明任何事情之前进行递归深入分析。
分支
另一种非常常见但对编译器来说很困难的情况是,如果您在分支代码流中隐藏了错误的转换:
public static void main(String[] args) {
f("hello");
}
public static <T> void f(T obj) {
if (isMonday()) {
Integer a = (Integer) obj;
} else {
String b = (String) obj;
}
}
现在,Java 需要在编译时了解 isMonday()
returns,这是不可能的。
但是如果 Java 标记这个,那就不好了。因为,如果我们从外部确保我们只在星期一启动程序怎么办?然后它应该可以工作。
考虑以下示例:
public class Example {
public static <T> void f(T obj) {
Integer i = (Integer) obj; // runtime error
}
public static void main(String[] args) {
f("hello");
}
}
Java有什么理由无法弄清楚第 3 行中的强制转换在编译时是非法的吗?当然,由于类型擦除,函数在运行时的签名将是 f(Object obj)
,但在我看来,在编译时它有足够的信息来捕获错误。
对比案例:
List<String> ls = new ArrayList<>();
ls.add(42); // compile-time error
ls.add("foo");
Integer i = ls.get(0); // compile-time error
其中涉及类型参数但在编译时成功检测到错误。
如果答案是“编译器不够智能”,那么有什么理由(为了向后兼容?)为什么它不能变得更智能?
说明
Java,根据其当前规则集(参见 JLS)必须处理 方法内容 及其 调用站点 分别.
方法内容
演员阵容
(Integer) obj
必须在编译时允许,因为 T
可能是 Integer
。毕竟,像
f(4)
应该成功并被允许。
Java 不允许考虑方法的调用位置。此外,这意味着 Java 必须扫描 所有呼叫站点 但这是不可能的,因为这还包括可能的 未来呼叫站点 尚未编写或稍后包含的内容,以防您正在编写库。
调用站点
调用站点也必须是合法的,因为 Java 不允许考虑方法内容。
签名要求 T
(extends Object
) 并且 String
满足。所以允许这样称呼。
如果Java 也会检查内容,想象一下你会在其他一些方法调用中隐藏 cast 3 levels depper。然后 Java 不仅要检查 f
的代码,还要检查那些方法的代码,可能还要检查它们所有的 if
语句,以检查是否达到了错误转换的行。
在编译时以 100% 的确定性证明它是 NP-hard,因此它也不属于规则集。
为什么?
虽然我们看到这种情况并不总是很容易检测到,而且在所有可能的情况下实际证明它甚至可能 不可能(NP-hard),Java 设计师当然可以添加一些较弱的规则来部分涵盖危险情况。
此外,实际上有一些类似的情况 Java 会帮助您解决较弱的规则。例如像
这样的演员表House a = new House();
Dog b = (Dog) a;
被禁止,因为 Java 可以很容易地证明类型完全不相关。但是一旦设置变得更加复杂,类型来自其他方法、泛型,Java 就无法再轻松地检查它了。
总而言之,您必须向 Java 语言设计师询问决策制定的确切原因。就是这样。
静态代码分析
你在这里所拥有的通常是静态代码分析器的工作(就像大多数 IDE 已经包含的那样)。他们实际上会扫描您的代码、所有用法等,并尝试找出您当前的代码流是否存在问题。
重要的是要注意,这也包括很多误报,因为我们刚刚了解到并非所有此类用法实际上都是错误的,一些危险的设置可能是故意的。
附录:评论
根据评论中的讨论,让我强调一个事实,即您的特定示例确实很容易被证明是错误的。因此,在这种特殊情况下,调用站点 很容易被禁止 (任何静态代码分析器都会很高兴地针对此代码向您发出警告)。
但是,我们可以对代码进行非常简单的修改,这已经说明了为什么在将调用站点与方法内容连接时实际证明错误如此困难。
所以 tldr 是几乎所有真实的代码情况都需要更多的努力才能让工具 100% 证明调用是不正确的。此外,对此进行编程要困难得多,并且不能始终确保没有误报。这就是为什么这些东西通常不是由编译器完成,而是由静态代码分析器完成的原因。
这两个常见的例子是方法嵌套和代码分支。
嵌套
假设你用另一种方法隐藏演员表 (Integer) obj
一个水平下降:
public static void main(String[] args) {
f("hello");
}
public static <T> void f(T obj) {
g(obj);
}
public static <T> void g(T obj) {
Integer i = (Integer) obj;
}
为了证明这一点,Java 现在必须将调用站点从 main
连接到 f
中的内容,再连接到 [=25] 中的调用站点=].如果您添加更多级别的嵌套,这很快就会失控,需要在证明任何事情之前进行递归深入分析。
分支
另一种非常常见但对编译器来说很困难的情况是,如果您在分支代码流中隐藏了错误的转换:
public static void main(String[] args) {
f("hello");
}
public static <T> void f(T obj) {
if (isMonday()) {
Integer a = (Integer) obj;
} else {
String b = (String) obj;
}
}
现在,Java 需要在编译时了解 isMonday()
returns,这是不可能的。
但是如果 Java 标记这个,那就不好了。因为,如果我们从外部确保我们只在星期一启动程序怎么办?然后它应该可以工作。