当涉及类型参数时,为什么 Java 无法找出一些明显的非法强制转换?

avl*_*vli 0 java generics casting jls

考虑以下示例:

public class Example {
    public static <T> void f(T obj) {
        Integer i = (Integer) obj; // runtime error
    }
    public static void main(String[] args) {
        f("hello");
    }
}
Run Code Online (Sandbox Code Playgroud)

有什么理由让 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
Run Code Online (Sandbox Code Playgroud)

其中涉及类型参数但在编译时成功检测到错误。

如果答案是“编译器不够聪明”,那么有什么理由(为了向后兼容?)为什么不能让它变得更聪明?

Zab*_*uza 6

解释

Java中,基于当前的规则集(见JLS)的治疗方法的内容和它的调用点 分开


方法内容

演员阵容

(Integer) obj
Run Code Online (Sandbox Code Playgroud)

必须在编译时允许,因为T可能是Integer. 毕竟,像这样的电话

f(4)
Run Code Online (Sandbox Code Playgroud)

应该成功并被允许。

Java 不允许考虑方法的调用点。此外,这意味着 Java 将必须扫描所有调用站点,但这是不可能的,因为如果您正在编写库,这还将包括尚未编写或稍后包含的可能的未来调用站点


呼叫站点

调用站点也必须是合法的,因为 Java 不允许考虑方法内容。

签名要求T( extends Object) 和String满足它。所以可以这样称呼。

如果你的Java同时检查内容,想象你会躲在投3级depper一些其他方法调用。然后 Java 不仅要检查fs 代码,还要检查这些方法的代码,可能还有它们的所有if语句,以检查是否达到了错误转换的行。在编译时证明 100% 确定性是 NP 难的,因此它也不是规则集的一部分。


为什么?

虽然我们看到这种情况并不总是很容易检测到,而且在所有可能的情况下实际证明它甚至可能是不可能的(NP-hard),但 Java 设计者当然可以添加一些较弱的规则来部分覆盖危险情况。

此外,实际上在一些类似的情况下,Java 会帮助您解决较弱的规则。例如一个演员像

House a = new House();
Dog b = (Dog) a;
Run Code Online (Sandbox Code Playgroud)

是被禁止的,因为 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;
}
Run Code Online (Sandbox Code Playgroud)

为了证明这一点,Java 现在必须将调用站点从main中的内容连接f到 中的调用站点g。如果您添加更多级别的嵌套,这将很快失控,并且需要进行递归深入分析,然后才能证明任何事情。

分枝

编译器的另一个非常常见但困难的情况是,如果您在分支代码流中隐藏错误转换:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,Java 需要了解isMonday()编译时返回的内容,这是不可能的。

但是如果 Java 标记了这一点,那就很糟糕了。因为,如果我们从外部确保我们只在周一启动该计划呢?它应该工作。

  • @avli:编译器*可以*做到这一点,但Java语言的规则根本不是这样设置的。 (2认同)
  • `f("hello")` 必须是合法的,因为不能考虑方法内容,在您的情况下可能很容易证明,但有更复杂的情况无法再证明(我添加了一些答案的段落),这是 NP 难的。设计者不想包含有时有效有时无效的规则。然而,他们当然可以添加诸如_“仅扫描一层深度,...”之类的内容,但他们没有。有趣的是,类似的情况确实存在,例如静态初始化的递归检测,Java 会检测到一些问题,但不是全部。 (2认同)