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

时间:2013-06-12 20:47:30

标签: java try-catch language-design final

我相信这里

final int i;
try { i = calculateIndex(); }
catch (Exception e) { i = 1; }
如果控制到达catch-block,则可能无法分配

i。但是,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块中i 明确地未分配的一个特殊情况。

更新

经过一番思考后,我更加确定这只是JLS模型的一个弱点:如果我在所提出的例子中声明了公理,当控制到达catch-block时,i肯定是未分配的“,它不会与任何其他公理或定理发生冲突。在catch块中分配i之前,编译器不允许读取任何i,因此无法观察到{{1}}是否已被分配。

12 个答案:

答案 0 :(得分:35)

JLS狩猎:

  

如果分配了最终变量,那么这是一个编译时错误,除非在分配之前它是明确未分配的(§16)。

第16章:

  如果满足以下所有条件,则

在catch块之前肯定是未分配的:

     

在尝试阻止后,V肯定是未分配的。
  在每个属于try块的return语句之前,V肯定是未分配的   对于属于try块的表单throw e的每个语句中的e后,V肯定是未分配的   在try块中发生的每个assert语句之后,V肯定是未分配的   在每个属于try块的break语句之前,V肯定是未分配的,其break目标包含(或者是)try语句。
  在每个继续语句属于try块并且其continue目标包含try语句之前,V肯定是未分配的。

大胆是我的。在try阻止后,不清楚是否已分配i

此外在示例中

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

粗体文字是仅 条件,可防止实际错误的作业i=1被视为非法。因此,这足以证明需要“绝对未分配”的更好条件才能允许原始帖子中的代码。

如果修改了规范以用

替换此条件
  

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

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

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

答案 1 :(得分: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之后的skip是三个独立的操作。您可以 (但不太可能)在步骤4和步骤5之间发生异常(Thread.interrupt()似乎是最好的示例)。这将导致进入catch块< em>在之后设置了。

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

答案 2 :(得分:7)

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

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

答案 3 :(得分:4)

这是对支持论文的最强有力论据的总结,即在不破坏一致性的情况下不能放宽当前的明确指配规则(A),其次是我的反驳(B):

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

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

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

    < / LI>
  • B :实际上,调试工具总是可以访问未初始化的局部变量,这将在典型的实现上具有任意值。未初始化的变量与在实际写入发生后初始化完成的变量之间没有本质区别。无论此处考虑的特殊情况如何,该工具必须始终使用其他元数据来为每个局部变量知道该变量明确分配的指令范围,并且只允许在执行发现时观察其值本身就在范围内。

最终结论:

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

答案 4 :(得分:3)

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

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

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

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

这是正确的,但是如果在分配i之后发生对bar()的调用(例如在finally子句中),或者我们使用try-with-resources语句和close方法抛出的资源,那么这是正确的。例外。

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

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

final int i = calculateIndex();

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

显然我被分配了。

简而言之,我认为添加此功能会增加规范的复杂性,但收效甚微。

答案 5 :(得分: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行设置一个断点,你可以在 1为assignd之前观察变量i 的状态。 因此放松观察到的行为将违背 Java™ Virtual Machine Tool Interface

答案 6 :(得分: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

这意味着它不能是最终的

答案 7 :(得分:2)

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

似乎没有JLS要求编译器通过区分您是抓住Exception还是Error / Throwable来确定我是否可以在达到catch阻止时预先设置,这意味着这是JLS模型的弱点。

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

(Integer Wrapper Type + finally +“Elvis”运算符,用于测试是否为null):

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;}
}

答案 8 :(得分: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.

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

答案 9 :(得分: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;
    }
}

现在,上面的COULD已经重构为最初使用'calculateIndex'方法发布的。但是,如果完全应用了福勒定义的“提取方法”重构,那么就可以得到这个[注意:删除“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回复所指出的,人们不能假设只有一个默认值。如果设置它的逻辑增长到稍微复杂,它仍然可以容易地驻留在封装的异常处理程序中。但是,在某些时候,它也可能需要重构为它自己的方法。这两种情况仍然允许封装的方法执行它的功能并真正计算索引。

总之,当我到这里看看我认为正确的代码时,就没有编译问题需要讨论。 (我非常肯定你不会同意:这很好,我只是想更清楚我的观点。)对于错误代码的编译器警告,这些帮助我首先意识到出了问题。在这种情况下,需要重构。

答案 10 :(得分:1)

根据规范JLS狩猎由“djechlin”完成,规范告诉我什么时候变量肯定是未分配的。因此规范说在这些情况下允许赋值是安全的。除了规范中提到的情况之外,还有其他情况,其中case变量仍然可以被取消分配,如果它可以检测到,它将依赖编译器做出明智的决定并允许作业。

在您指定的场景中没有提到Spec,编译器应该标记错误。所以它依赖于规范的编译器实现,如果它足够智能来检测这种情况。

<强>参考: Java语言规范 Definite Assignment 部分“16.2.15尝试语句”

答案 11 :(得分: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()有时计算索引,但我们可以对parseInt()说同样的话吗?还没有计算CalcuInde​​x()的作用,因为当它不可能时抛出并因此不计算索引,然后使它返回错误的值(1在你的重构中是任意的)是非常糟糕的。

@Marko,我不理解你对Joeg关于 AFTER第4行和第5行的回复......我在java世界中还不够强大(25y)但是我认为编译器是正确的:我可以在Joeg的情况下初始化两次。

[我所说的一切都非常谦虚]