是否可以在 catch 中重新分配最后一个变量,即使赋值是 try 中的最后一个操作?

2022-08-31 16:59:44

我完全相信,在这里

final int i;
try { i = calculateIndex(); }
catch (Exception e) { i = 1; }

i如果控制到达捕获块,则不可能已经分配。但是,Java编译器不同意并声称.the final local variable i may already have been assigned

我在这里是否仍然缺少一些微妙之处,或者这只是Java语言规范用于识别潜在重新分配的模型的弱点?我主要担心的是这样的事情,这可能会导致异常被“凭空”抛出,但我仍然不明白如何在分配后抛出它,这显然是尝试块中的最后一个操作。Thread.stop()

如果允许,上面的成语将使我的许多方法更简单。请注意,此用例在 Scala 等语言中具有一流的支持,这些语言始终使用 Maybe monad:

final int i = calculateIndex().getOrElse(1);

我认为这个用例是一个很好的动机,允许一个特殊情况,其中绝对未在捕获块中分配。i

更新

经过一番思考,我更加确定这只是JLS模型的一个弱点:如果我声明公理“在所呈现的示例中,当控制到达捕获块时肯定是未分配的”,它不会与任何其他公理或定理冲突。编译器在被分配到 catch 块中之前将不允许任何读取,因此无法观察到是否已分配给的事实。iii


答案 1

JLS狩猎:

如果将最终变量赋值给,则这是编译时错误,除非在赋值之前它肯定是未赋值的 (§16)。

Quoth 第 16 章:

在以下所有条件都成立的情况下,在捕获块之前,V 肯定是未分配的:

V 在 try 块之后肯定是未分配的。
V 在属于 try 块的每个 return 语句之前肯定是未赋值的。
V 在属于 try 块的表单 throw e 的每个语句中,在 e 之后肯定是未赋值的。
在 try 块中出现的每个断言语句之后,V 肯定是未赋值的。
在属于 try 块的每个中断语句之前,V 肯定是未赋值的,并且其中断目标包含(或是)try 语句。
在属于 try 块的每个 continue 语句之前,V 肯定是未赋值的,并且其 continue 目标包含 try 语句。

粗体是我的。在块之后,不清楚是否被分配。tryi

此外,在示例中

final int i;
try {
    i = foo();
    bar();
}
catch(Exception e) { // e might come from bar
    i = 1;
}

粗体文本是防止实际错误分配非法的唯一条件。因此,这足以证明需要更精细的“绝对未分配”条件才能允许在原始帖子中使用代码。i=1

如果规范已修订以将此条件替换为

如果 catch 块捕获了未经检查的异常,则在 try 块之后,V 肯定是未分配的。
如果 catch 块捕获了未选中的异常,则在能够引发由 catch 块捕获的类型异常的最后一个语句之前,V 肯定是未赋值的。

那么我相信你的代码是合法的。(在我的特别分析中,尽我所能。

我为此提交了一个JSR,我希望它被忽略,但我很好奇这些是如何处理的。从技术上讲,传真号码是必填字段,我希望如果我在那里输入+1-000-000-000,它不会造成太大的损害。


答案 2

可悲的是,我认为JVM是正确的。虽然从查看代码直观上是正确的,但在查看 IL 的上下文中它是有意义的。我创建了一个简单的run()方法,它主要模仿你的情况(简化的评论在这里):

0: aload_0
1: invokevirtual  #5; // calculateIndex
4: istore_1
5: goto  17
// here's the catch block
17: // is after the catch

因此,虽然您无法轻松编写代码来测试它,因为它不会编译,但方法的调用,存储值和在捕获之后的跳到是三个单独的操作。在步骤 4 和步骤 5 之间,您可能会(尽管可能性不大)发生异常(Thread.interrupt() 似乎是最好的示例)。这将导致在设置 i 进入 catch 块。

我不确定你是否能故意用大量的线程和中断来实现这一点(编译器无论如何都不会让你编写该代码),但因此理论上可以设置i,你可以进入异常处理块,即使使用这个简单的代码。


推荐