58

我非常确信这里

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

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

我在这里是否还遗漏了一些微妙之处,或者这只是 Java 语言规范用来识别潜在重新分配的模型的一个弱点?我主要担心的是Thread.stop(),这可能会导致“凭空”抛出异常,但我仍然不知道在分配后如何抛出它,这显然是 try-block 中的最后一个操作.

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

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

我认为这个用例是一个很好的动机,允许 在 catch-blocki绝对未分配的一个特殊情况。

更新

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

4

12 回答 12

35

JLS狩猎:

如果最终变量被分配给它是一个编译时错误,除非它在分配之前立即被明确地未分配(第 16 节)。

第十六章:

如果满足以下所有条件,则 V 在 catch 块之前肯定是未分配的:

在 try 块之后 V 肯定是未分配的。
V 在属于 try 块的每个 return 语句之前肯定是未分配的。
在属于 try 块的每个 throw e 形式的语句中,在 e 之后,V 肯定是未分配的。
在 try 块中出现的每个 assert 语句之后,V 肯定是未分配的。
在属于 try 块并且其 break 目标包含(或是)try 语句的每个 break 语句之前,V 肯定是未分配的。
在属于 try 块并且其 continue 目标包含 try 语句的每个 continue 语句之前,V 肯定是未分配的。

粗体是我的。块后try不清楚是否i已分配。

此外在示例中

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

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

如果修改规范以替换此条件

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

那么我相信你的代码是合法的。(根据我的特别分析。)

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

于 2013-06-12T20:57:43.217 回答
18

遗憾的是,我认为 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

所以,虽然你不能轻易地编写代码来测试它,因为它不会编译,但方法的调用、值的存储和 catch 后的跳转是三个独立的操作。您可能(但不太可能)在步骤 4 和步骤 5 之间发生异常(Thread.interrupt() 似乎是最好的例子)。这将导致在设置 i进入 catch 块。

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

于 2013-06-20T16:24:42.940 回答
7

不太干净(我怀疑你已经在做什么)。但这只会增加 1 行。

final int i;
int temp;
try { temp = calculateIndex(); }
catch (IOException e) { temp = 1; }
i = temp;
于 2013-06-12T20:53:27.153 回答
4

这是支持以下论点的最强论点的总结,即在不破坏一致性的情况下不能放松当前的明确分配规则(A),然后是我的反驳论点(B):

  • :在字节码级别上,对变量的写入不是 try 块中的最后一条指令:例如,最后一条指令通常是goto跳过异常处理代码;

  • B : 但是如果规则声明在 catch-block 中肯定是未分配i的,那么它的值可能不会被观察到。不可观察的价值与没有价值一样好;

  • :即使编译器声明i绝对未赋值,调试工具仍然可以看到该值;

  • B:事实上,调试工具总是可以访问一个未初始化的局部变量,在典型的实现中该变量将具有任意值。未初始化的变量和在实际写入发生后突然完成初始化的变量之间没有本质区别。无论此处考虑的特殊情况如何,该工具都必须始终使用附加元数据来了解每个局部变量的指令范围,该变量在该范围内被明确分配,并且只允许在执行发现自己处于该范围内时观察其值。

定论:

该规范可以始终如一地接收更细粒度的规则,这将允许我发布的示例进行编译。

于 2013-06-22T13:20:06.087 回答
3
1   final int i;
2   try { i = calculateIndex(); }
3   catch (Exception e) { 
4       i = 1; 
5   }

OP 已经指出,在第 4 行我可能已经被分配了。例如通过 Thread.stop(),这是一个异步异常,参见 http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5

现在在第 4 行设置断点,您可以观察变量 i分配 1 之前的状态。因此,放宽观察到的行为会违反 Java™ 虚拟机工具接口

于 2013-06-19T20:07:36.483 回答
3

你是对的,如果赋值是 try 块中的最后一个操作,我们知道在进入 catch 块时变量不会被赋值。但是,将“最后一次操作”的概念形式化会大大增加规范的复杂性。考虑:

try {
    foo = bar();
    if (foo) {
        i = 4;
    } else {
        i = 7;
    }
}

这个功能有用吗?我不这么认为,因为最终变量必须只分配一次,而不是最多一次Error在您的情况下,如果抛出an ,则该变量将未被分配。如果变量超出范围,您可能并不关心,但情况并非总是如此(可能有另一个 catch 块Error在相同或周围的 try 语句中捕获 , )。例如,考虑:

final int i;
try {
    try {
        i = foo();
    } catch (Exception e) {
        bar();
        i = 1;
    }
} catch (Throwable t) {
    i = 0;
}

这是正确的,但如果对 bar() 的调用发生在分配 i 之后(例如在 finally 子句中),或者我们将 try-with-resources 语句与 close 方法引发异常的资源一起使用,则不会如此。

考虑到这一点会给规范增加更多的复杂性。

最后,有一个简单的解决方法:

final int i = calculateIndex();

int calculateIndex() {
    try {
        // calculate it
        return calculatedIndex;
    } catch (Exception e) {
        return 0;
    }
}

这很明显我被分配了。

简而言之,我认为添加此功能会显着增加规范的复杂性,而不会带来什么好处。

于 2013-06-19T05:33:39.093 回答
2

根据 OP 的问题编辑回复

这真的是对评论的回应:

你所做的只是写了一个稻草人论证的明确例子:你间接地引入了一个默认假设,即必须始终存在一个且只有一个默认值,对所有调用站点都有效

我相信我们正在从相反的两端处理整个问题。似乎您从下往上看的——从字面上看是从字节码到 Java。如果这不是真的,您正在从规范的“代码”合规性中查看它。

从相反的方向,从“设计”向下接近这个,我看到了问题。我认为是 M. Fowler 将各种“难闻的气味”收集到了这本书中:“重构:改进现有代码的设计”。这里(可能还有很多很多其他地方)描述了“提取方法”重构。

因此,如果我想象一个没有“calculateIndex”方法的代码的虚构版本,我可能会有这样的事情:

public void someMethod() {
    final int i;
    try {
        int intermediateVal = 35;
        intermediateVal += 56;
        i = intermediateVal*3;
    } catch (Exception e) {
        // would like to be able to set i = 1 here;
    }
}

现在,上面的内容可以重构为最初使用“calculateIndex”方法发布的内容。但是,如果完全应用了 Fowler 定义的“提取方法”重构,那么就会得到这个 [注意:删除“e”是为了与您的方法区分开来。]

public void someMethod() {
    final int i =  calculateIndx();
}

private int calculateIndx() {
    try {
        int intermediateVal = 35;
        intermediateVal += 56;
        return intermediateVal*3;
    } catch (Exception e) {
        return 1;  // or other default values or other way of setting
    }
}

因此,从“设计”的角度来看,问题在于您拥有的代码。您的“calculateIndex”方法不计算索引。它只是有时。其余时间,异常处理程序进行计算。

此外,这种重构更能适应变化。例如,如果您必须将我假设的默认值“1”更改为“2”,没什么大不了的。但是,正如引用的 OP 回复所指出的那样,不能假设只有一个默认值。如果设置它的逻辑变得稍微复杂一点,它仍然可以很容易地驻留在封装的异常处理程序中。但是,在某些时候,它也可能需要重构为自己的方法。这两种情况仍然允许封装的方法执行其功能并真正计算索引。

总之,当我到达这里并查看我认为正确的代码时,就没有编译器问题可供讨论了。(我很确定你不会同意:没关系,我只是想更清楚地说明我的观点。) 至于出现错误代码的编译器警告,那些帮助我首先意识到有问题。在这种情况下,需要进行重构。

于 2013-06-20T16:53:32.273 回答
2

i可能被分配两次

    int i;
    try {
        i = calculateIndex();  // suppose func returns true
        System.out.println("i=" + i);
        throw new IOException();
    } catch (IOException e) {
        i = 1;
        System.out.println("i=" + i);
    }

输出

i=0
i=1

这意味着它不能是最终的

于 2013-06-12T22:14:10.477 回答
2

浏览 javadoc,似乎Exception在分配 i 之后不能抛出的子类。从 JLS 理论的角度来看,它似乎Error可以在分配 i 之后被抛出(例如VirtualMachineError)。

似乎没有 JLS 要求编译器通过区分你是在捕获Exception还是Error/来确定 i 是否可以在到达 catch 块时预先设置Throwable,这意味着它是 JLS 模型的一个弱点。

为什么不尝试以下?(已编译和测试)

(Integer Wrapper Type + finally + "Elvis" 运算符来测试是否为空):

import myUtils.ExpressionUtil;
....
Integer i0 = null; 
final int i;
try { i0 = calculateIndex(); }   // method may return int - autoboxed to Integer!
catch (Exception e) {} 
finally { i = nvl(i0,1); }       


package myUtils;
class ExpressionUtil {
    // Custom-made, because shorthand Elvis operator left out of Java 7
    Integer nvl(Integer i0, Integer i1) { return (i0 == null) ? i1 : i0;}
}
于 2013-06-18T08:05:31.973 回答
2

我认为在一种情况下,这种模型可以充当救生员。考虑下面给出的代码:

final Integer i;
try
{
    i = new Integer(10);----->(1)
}catch(Exception ex)
{
    i = new Integer(20);
}

现在考虑第 (1) 行。大多数 JIT 编译器按以下顺序创建对象(伪代码):

mem = allocate();   //Allocate memory 
ctorInteger(instance);//Invoke constructor for Singleton passing instance.
i = mem;        //Make instance i non-null

但是,一些 JIT 编译器会乱序写入。以上步骤重新排序如下:

mem = allocate();   //Allocate memory 
i = mem;        //Make instance i non-null
ctorInteger(instance);  //Invoke constructor for Singleton passing instance.

现在假设,在第 (1) 行创建对象时JIT执行。out of order writes并假设在执行构造函数时抛出异常。在这种情况下,该catch块将具有iwhich is not null。如果 JVM 不遵循这种模式,那么在这种情况下,最终变量允许分配两次!!!

于 2013-06-18T17:33:21.773 回答
1

根据“djechlin”完成的规范 JLS 搜索,规范告诉变量何时明确未分配。所以规范说,在那些情况下,允许分配是安全的。除了规范中提到的情况之外,可能还有其他情况,在这种情况下,变量仍然可以未分配,如果它可以检测到,它将取决于编译器做出明智的决定并允许分配。

Spec 绝不会在您指定的场景中提及该编译器应标记错误。因此,它是否足够智能以检测此类情况取决于规范的编译器实现。

参考: Java 语言规范定义赋值部分“16.2.15 try 语句”

于 2013-06-20T18:37:58.520 回答
0

我遇到了完全相同的问题马里奥,并阅读了这个非常有趣的讨论。我刚刚解决了我的问题:

private final int i;

public Byte(String hex) {
    int calc;
    try {
        calc = Integer.parseInt(hex, 16);
    } catch (NumberFormatException e) {
        calc = 0;
    }
    finally {
      i = calc;
    }
}

@Joeg,我必须承认我非常喜欢你关于设计的帖子,尤其是那句话: calculateIndx() 有时计算 index,但我们可以对 parseInt() 说同样的话吗?在不可能的情况下,calculateIndex() 的作用不是抛出并因此不计算索引,然后使它返回错误的值(1 在您的重构中是任意的)是恕我直言不好。

@Marko,我不明白你对Joeg关于第4行之后和第5行之前的回复......我在java世界中还不够强大(25y的c ++但只有1在java......),但我这种情况是编译器正确的情况:在乔格的情况下,我可以被初始化两次。

[我所说的都是非常非常谦虚的意见]

于 2014-11-25T00:39:35.930 回答