繁体   English   中英

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

[英]Could a final variable be reassigned in catch, even if assignment is last operation in try?

我相信这里

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

我认为这个用例是一个非常好的动机,允许一个特殊的情况, i 肯定在catch块中未分配

UPDATE

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

JLS狩猎:

如果分配了最终变量,则为编译时错误,除非在分配之前它是明确未分配的(第16段)。

第16章:

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

在try块之后,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,我希望它不会造成太大的伤害。

遗憾的是,我认为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是三个独立的操作。 可以 (但不太可能)发生异常(Thread.interrupt()似乎是步骤4和步骤5之间的最佳示例)。这将导致在设置i 进入catch块。

我不知道,你可以故意做一吨的线程和中断这种情况发生(和编译器不会让你写的代码反正),但它因此理论上是可能的 ,我可以进行设置,你可以进入即使使用这个简单的代码,异常处理块也是如此。

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

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

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

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

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

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

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

定论:

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

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

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

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

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

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

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

最后,有一个简单的工作:

final int i = calculateIndex();

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

这很明显我被分配了。

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

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™虚拟机工具界面

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

这意味着它不可能是最终的

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

似乎有编译器,以确定何时到达catch块,通过区分无论你是否追赶我可能是先前设置没有JLS要求ExceptionError / Throwable ,这意味着它的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;}
}

我认为有一种情况是这种模式可以起到救命的作用。 考虑下面给出的代码:

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块将具有not null i 如果JVM不遵循这个模态,那么在这种情况下允许最终变量被分配两次!

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

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

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

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

参考: Java语言规范定义分配部分“16.2.15试用语句”

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

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()说同样的话吗? 是不是这也是calculateIndex()的作用,因为当它不可能时抛出并因此不计算索引,然后使它返回错误的值(1在你的重构中是任意的)是非常糟糕的。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM