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

当涉及类型参数时,为什么Java无法找出一些明显的非法强制转换?,java,generics,casting,jls,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"); } } 其中涉及类型参数,但在编译时成功检测到错误 如果答案是编译器不够智

考虑以下示例:

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,根据其当前的规则集,请参见JLS必须单独处理方法内容及其调用站点

方法内容 演员阵容

(Integer) obj
必须在编译时允许,因为T可以是整数。毕竟,像这样的电话

f(4)
应该成功并被允许

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

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

签名要求T扩展对象,字符串填充该对象。所以可以这样称呼它

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

为什么? 虽然我们看到这样的情况并不总是很容易检测到,而且对于所有可能的情况实际证明它甚至可能是不可能的NP-hard,但Java设计人员当然可以添加一些较弱的规则来部分覆盖危险情况

此外,实际上还有一些类似的情况,Java可以帮助您解决较弱的规则。比如一个演员

House a = new House();
Dog b = (Dog) a;
是禁止的,因为Java可以很容易地证明这些类型是完全不相关的。但是一旦设置变得更复杂,类型来自其他方法、泛型,Java就不能再轻松地检查它了

总之,在决策过程中,您必须向Java语言设计师询问确切的原因。事实就是这样

静态代码分析 这里的工作通常是静态代码分析器的工作,就像大多数IDE已经包含的一样。他们实际上会扫描您的代码、所有用法等,并试图找出您当前的代码流是否存在可能的问题

需要注意的是,这也包括很多误报,因为我们刚刚了解到,并非所有此类用法都可能是错误的,可能会有一些危险的设置

附件:评论 根据评论中的讨论,让我强调一个事实,即你的具体例子确实很容易证明是错误的。因此,调用站点很容易被禁止,在这种特殊情况下,任何静态代码分析器都会很高兴地为此代码向您发出警告

但是,我们可以对已经存在的代码进行非常简单的修改,这说明了为什么在将调用站点与方法内容连接时,要实际证明错误是如此困难

因此tldr是,几乎所有真实的代码情况都需要更多的努力,才能让工具100%地证明调用是错误的。此外,对其进行编程要困难得多,并且不能始终确保没有误报。这就是为什么这些东西通常不是由编译器完成的,而是由静态代码分析器完成的

两个常见的例子是方法嵌套和代码分支

筑巢 假设您在另一种方法中隐藏强制转换整数obj a level depper:

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需要知道isMonday在编译时返回什么,这是不可能的


但是如果Java标记这一点,那将是不好的。因为,如果我们从外部确保我们只在周一启动该计划会怎么样?那么它应该可以工作。

在第一种情况下,您使用的是cast,它的字面意思是信任我,编译器!我向你保证这会奏效的!。编译器发现在某些情况下这是可行的,并且信任您。事实上,JLS并不允许所有强制转换。如果可以证明强制转换是不可能的,它就不会编译。例如,如果你有两个局部变量,一个是汽车,你正试图把它扔给狗@Sweeper@Zabuzard是的,这就是为什么我加入了“编译器认为在某些情况下这会起作用”这句话。请注意,void fT obj实际上非常接近void fObject obj,这恰好是它的擦除,这里更清楚地说明了它被接受的原因:对象变量引用的对象可以是整数,因此,铸造是允许的。如果不是,则会出现运行时错误。然后,如果您单独查看一个调用站点:一个对象参数
显然可以接受字符串参数,因为它是对象的子类型。@avli:可能出现错误类型转换的唯一原因是您创建了一个无意义的类型签名。方法定义是它停止查找的边界,这在所有语言中都很常见。如果你在方法定义中做了一些毫无意义的事情,那么你就可以开枪打自己的脚。我不明白你为什么要像编译器应该知道的那样重新表述这个问题,这是显而易见的。这是关于调用站点的一个很好的观点。也许,我在这个问题上不够清楚。我很清楚为什么参数化方法可以接受任何扩展对象的东西。在我的例子中,打电话应该是非法的。我不确定我是否理解你关于未来电话的观点。仅检查编译时存在的调用站点还不够吗?@avli:编译器可以这样做,但Java语言的规则根本不是这样设置的。fhello必须是合法的,因为方法内容不能被考虑在内,在你的案例中,证明可能很容易,但在更复杂的案例中,当它无法被证明时,我在答案中添加了一些段落,这是NP难的。设计师们也不想加入那些有时有效有时无效的规则。然而,他们当然可以添加一些东西,比如只扫描一层深度,…,但他们没有。有趣的是,类似的情况确实存在,例如静态初始化上的递归检测,Java将检测一些问题,但不是全部。
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;
}
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;
    }
}