简体   繁体   English

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

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

I am quite convinced that here 我相信这里

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

i cannot possibly have already been assigned if control reaches the catch-block. 如果控制到达catch-block, i可能不会被分配。 However, Java compiler disagrees and claims the final local variable i may already have been assigned . 但是,Java编译器不同意并声称the final local variable i may already have been assigned

Is there still some subtlety I am missing here, or is this just a weakness of the model used by the Java Language Specification to identify potential reassignments? 我还缺少一些细微之处,或者这只是Java语言规范用于识别潜在重新分配的模型的弱点? My main worry are things like Thread.stop() , which may result in an exception being thrown "out of thin air," but I still don't see how it could be thrown after the assignment, which is apparently the very last action within the try-block. 我主要担心的是像Thread.stop()这样的事情,这可能导致异常被“凭空捏造”抛出,但我仍然看不到它如何在赋值后被抛出,这显然是最后一次动作在try-block中。

The idiom above, if allowed, would make many of my methods simpler. 如果允许,上面的成语将使我的许多方法更简单。 Note that this use case has first-class support in languages, such as Scala, which consistently employ the Maybe monad: 请注意,此用例具有一流的语言支持,例如Scala,它始终使用Maybe monad:

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

I think this use case serves as a quite good motivation to allow that one special case where i is definitely unassigned within the catch-block. 我认为这个用例是一个非常好的动机,允许一个特殊的情况, i 肯定在catch块中未分配

UPDATE UPDATE

After some thought I am even more certain that this is just a weakness of the JLS model: if I declare the axiom "in the presented example, i is definitely unassigned when control reaches the catch-block", it will not conflict with any other axiom or theorem. 经过一番思考后,我更加确定这只是JLS模型的一个弱点:如果我在所提出的例子中声明了公理,当控制到达catch-block时i肯定是未分配的“,它不会与任何其他模块冲突公理或定理。 The compiler will not allow any reading of i before it is assigned in the catch-block, so the fact whether i has been assigned to or not cannot be observed. 编译器在catch块中分配之前不允许读取任何i ,因此无法观察到是否已分配i

JLS hunting: JLS狩猎:

It is a compile-time error if a final variable is assigned to unless it is definitely unassigned (§16) immediately prior to the assignment. 如果分配了最终变量,则为编译时错误,除非在分配之前它是明确未分配的(第16段)。

Quoth chapter 16: 第16章:

V is definitely unassigned before a catch block iff all of the following conditions hold: 如果满足以下所有条件,则v在catch块之前肯定是未分配的:

V is definitely unassigned after the try block. 在try块之后,V绝对是未分配的。
V is definitely unassigned before every return statement that belongs to the try block. 在每个属于try块的return语句之前,V绝对是未分配的。
V is definitely unassigned after e in every statement of the form throw e that belongs to the try block. 对于属于try块的表单throw e的每个语句中的e后,V肯定是未分配的。
V is definitely unassigned after every assert statement that occurs in the try block. 在try块中发生的每个assert语句之后,V肯定是未分配的。
V is definitely unassigned before every break statement that belongs to the try block and whose break target contains (or is) the try statement. 在每个属于try块的break语句之前,V绝对是未分配的,并且其break目标包含(或者是)try语句。
V is definitely unassigned before every continue statement that belongs to the try block and whose continue target contains the try statement. 在每个继续语句属于try块并且其continue目标包含try语句之前,V肯定是未分配的。

Bold is mine. 大胆是我的。 After the try block it is unclear whether i is assigned. try阻止之后, i不清楚是否已分配。

Furthermore in the example 此外,在示例中

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

The bold text is the only condition preventing the actual erroneous assignment i=1 from being illegal. 粗体文本是防止实际错误分配i=1非法的唯一条件。 So this is sufficient to prove that a finer condition of "definitely unassigned" is necessary to allow the code in your original post. 因此,这足以证明需要“绝对未分配”的更好条件才能允许原始帖子中的代码。

If the spec were revised to replace this condition with 如果修改了规范以替换此条件

V is definitely unassigned after the try block, if the catch block catches an unchecked exception. 如果catch块捕获未经检查的异常,则在try块之后肯定是未分配的。
V is definitely unassigned before the last statement capable of throwing an exception of a type caught by the catch block, if the catch block catches an unchecked exception. 如果catch块捕获未经检查的异常,则在最后一个能够抛出catch块捕获的类型的异常的语句之前,V肯定是未分配的。

Then I believe your code would be legal. 然后我相信你的代码是合法的。 (To the best of my ad-hoc analysis.) (根据我的特别分析。)

I submitted a JSR for this, which I expect to be ignored but I was curious to see how these are handled. 我提交了一个JSR,我希望被忽略,但我很想知道如何处理这些。 Technically fax number is a required field, I hope it won't do too much damage if I entered +1-000-000-000 there. 技术上传真号码是必填字段,如果我在那里输入+ 1-000-000-000,我希望它不会造成太大的伤害。

I think the JVM is, sadly, correct. 遗憾的是,我认为JVM是正确的。 While intuitively correct from looking at the code, it makes sense in the context of looking at the IL. 虽然从查看代码看起来直观正确,但在查看IL的上下文中是有意义的。 I created a simple run() method that mostly mimics your case (simplified comments here): 我创建了一个简单的run()方法,主要模仿你的情况(这里的简化评论):

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

So, while you can't easily write code to test this, because it won't compile, the invoke of the method, the store the value, and the skip to after the catch are three separate operations. 所以,虽然你不能轻易编写代码来测试这个,因为它不会编译,方法的调用,存储的值,以及catch之后的skip是三个独立的操作。 You could (however unlikely that may be) have an exception occur (Thread.interrupt() seems to be the best example) between step 4 and step 5. This would result in entering into the catch block after i has been set. 可以 (但不太可能)发生异常(Thread.interrupt()似乎是步骤4和步骤5之间的最佳示例)。这将导致在设置i 进入catch块。

I'm not sure you could intentionally make that happen with a ton of threads and interrupts (and the compiler won't let you write that code anyway), but it is thus theoretically possible that i could be set, and you could enter in the exception handling block, even with this simple code. 我不知道,你可以故意做一吨的线程和中断这种情况发生(和编译器不会让你写的代码反正),但它因此理论上是可能的 ,我可以进行设置,你可以进入即使使用这个简单的代码,异常处理块也是如此。

Not quite as clean (and I suspect what you are already doing). 不太干净(我怀疑你已经在做什么)。 But this only adds 1 extra line. 但这只增加了1行。

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

This is a summary of the strongest arguments in favor of the thesis that the current rules for definite assignment cannot be relaxed without breaking consistency (A), followed by my counterarguments (B): 这是对支持这一论点的最有力论据的总结,即在不打破一致性的情况下不能放宽现行的明确指配规则(A),其次是我的反驳(B):

  • A : on the bytecode level the write to the variable is not the last instruction within the try-block: for example, the last instruction will typically be a goto jump over the exception handling code; :在字节码级别,对变量的写入不是try-block中的最后一条指令:例如,最后一条指令通常是异常处理代码的goto跳转;

  • B : but if the rules state that i is definitely unassigned within the catch-block, its value may not be observed. B :但是如果规则声明i在catch-block中肯定未分配的 ,则可能无法观察到其值。 An unobservable value is as good as no value; 不可观察的价值与无价值一样好;

  • A : even if the compiler declares i as definitely unassigned , a debug tool could still see the value; :即使编译器声明i 绝对是未分配的 ,调试工具仍然可以看到该值;

  • B : in fact, a debug tool could always access an uninitialized local variable, which will on a typical implementation have any arbitrary value. B :实际上,调试工具总是可以访问未初始化的局部变量,这在典型的实现中具有任意值。 There is no essential difference between an uninitialized variable and a variable whose initialization completed abruptly after the actual write having occurred. 未初始化的变量与在实际写入发生后初始化完成的变量之间没有本质区别。 Regardless of the special case under consideration here, the tool must always use additional metadata to know for each local variable the range of instructions where that variable is definitely assigned and only allow its value to be observed while execution finds itself within the range. 无论此处考虑的特殊情况如何,该工具必须始终使用其他元数据来为每个局部变量知道该变量明确赋值的指令范围,并且只允许在执行发现自己在该范围内时观察其值。

Final Conclusion: 定论:

The specification could consistently receive more fine-grained rules which would allow my posted example to compile. 规范可以一致地接收更细粒度的规则,这将允许我发布的示例进行编译。

You are correct that if the assignment is the very last operation in the try block, we know that upon entering the catch block the variable will not have been assigned. 你是正确的,如果赋值是try块中的最后一个操作,我们知道在进入catch块时,将不会分配变量。 However, formalizing the notion of "very last operation" would add significant complexity to the spec. 然而,正式化“最后一次操作”的概念会增加规范的复杂性。 Consider: 考虑:

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

Would that feature be useful? 该功能会有用吗? I don't think so, because a final variable must be assigned exactly once, not at most once. 我不这么认为,因为最终的变量必须精确分配一次,不是最多一次。 In your case, the variable would be unassigned if an Error is thrown. 在您的情况下,如果抛出Error ,则该变量将被取消分配。 You may not care about that if the variable runs out of scope anyway, but such is not always the case (there could be another catch block catching the Error , in the same or a surrounding try statement). 如果变量无论如何都超出了范围,你可能并不在乎,但情况并非总是如此(可能有另一个catch块在相同或周围的try语句中捕获Error )。 For instance, consider: 例如,考虑:

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

That is correct, but wouldn't be if the call to bar() occured after assigning i (such as in the finally clause), or we use a try-with-resources statement with a resource whose close method throws an exception. 这是正确的,但如果在分配i之后发生了对bar()的调用(例如在finally子句中),或者我们使用带有close方法抛出异常的资源的try-with-resources语句。

Accounting for that would add even more complexity to the spec. 考虑到这一点会增加规范的复杂性。

Finally, there is a simple work around: 最后,有一个简单的工作:

final int i = calculateIndex();

and

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

that makes it obvious that i is assigned. 这很明显我被分配了。

In short, I think that adding this feature would add significant complexity to the spec for little benefit. 简而言之,我认为添加此功能会增加规范的复杂性,但收效甚微。

1   final int i;
2   try { i = calculateIndex(); }
3   catch (Exception e) { 
4       i = 1; 
5   }

OP already remarks that at line 4 i may already have been assigned. OP已经评论说,在第4行,我可能已经被分配了。 For example through Thread.stop(), which is an asynchronous exception, see http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5 例如,通过Thread.stop(),这是一个异步异常,请参阅http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5

Now set a breakpoint at line 4 and you can observe the state of the variable i before 1 is assignd. 现在在第4行设置一个断点,你可以 1为assignd 之前观察变量i的状态。 So loosening the observed behaviour would go against the Java™ Virtual Machine Tool Interface 因此,放松观察到的行为将违反Java™虚拟机工具界面

But i may be assigned twice 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);
    }

output 产量

i=0
i=1

and it means it cannot be final 这意味着它不可能是最终的

Browsing the javadoc, it seems no subclass of Exception could be thrown just after i is assigned. 浏览javadoc时,似乎在分配i之后不会抛出Exception子类。 From a JLS theoretical perspective, it seems Error could be thrown just after i is assigned (eg VirtualMachineError ). 从JLS理论的角度来看,似乎Error可能在我被分配后被抛出(例如VirtualMachineError )。

Seems there's no JLS requirement for compiler to determine whether i could be previously set when catch block is reached, by distinguishing whether you're catching Exception or Error / Throwable , implying it's a weakness of JLS model. 似乎有编译器,以确定何时到达catch块,通过区分无论你是否追赶我可能是先前设置没有JLS要求ExceptionError / Throwable ,这意味着它的JLS模型的弱点。

Why not try the following? 为什么不尝试以下? (have compiled & tested) (已编译和测试)

(Integer Wrapper Type + finally + "Elvis" operator to test whether null): (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;}
}

I think there is one situation where this model act as life saver. 我认为有一种情况是这种模式可以起到救命的作用。 Consider the code given below: 考虑下面给出的代码:

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

Now Consider the line (1). 现在考虑第(1)行。 Most of the JIT compilers creates object in following sequence(psuedo code): 大多数JIT编译器按以下顺序创建对象(伪代码):

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

But, some JIT compilers does out of order writes . 但是,一些JIT编译器会乱序写入 And above steps is reordered as follows: 以上步骤重新排序如下:

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

Now suppose, the JIT performs out of order writes while creating the object in line (1). 现在假设, JIT在第(1)行中创建对象时执行out of order writes And suppose an exception is thrown while executing the constructor. 并假设在执行构造函数时抛出异常。 In that case, the catch block will have i which is not null . 在这种情况下, catch块将具有not null i If JVM doesn't follow this modal then in this case final variable is allowed to be assigned twice!!! 如果JVM不遵循这个模态,那么在这种情况下允许最终变量被分配两次!

EDITING RESPONSE BASED UPON QUESTIONS FROM OP 基于OP的问题编辑响应

This is really in response to the comment: 这真的是对评论的回应:

All you have done is written up a clear-cut example of a straw man argument: you are vicariously introducing the tacit assumption that there must always be one and only one default value, valid for all call sites 你所做的一切都写成了一个明确的例子:一个稻草人的论点:你正在代替默认的假设,即必须始终只有一个默认值,对所有呼叫站点都有效

I believe that we are approaching the entire question from opposite ends. 我相信我们正在从相反的目的接近整个问题。 It seems that you are looking at it from the bottom up - literally from the bytecode and going up to the Java. 看起来你是从下到上看它 - 从字面码开始直到Java。 If this is not true, you are looking at it from the "code" compliance to the spec. 如果不是这样,那么您可以从符合规范的“代码”中查看它。

Approaching this from the opposite direction, from the "design" down, I see problems. 从相反的方向接近这个,从“设计”下来,我看到了问题。 I think it was M. Fowler who collected various "bad smells" into the book: "Refactoring: Improving the Design of Existing Code". 我认为M. Fowler在书中收集了各种“难闻的气味”:“​​重构:改进现有代码的设计”。 Here (and probably many, many other places) the "Extract Method" refactoring is described. 这里(可能还有许多其他地方)描述了“提取方法”重构。

Thus, if I imagine a made-up version of your code without the 'calculateIndex' method, I might have something like this: 因此,如果我想象一个没有'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;
    }
}

Now, the above COULD have been refactored as originally posted with a 'calculateIndex' method. 现在,上面的COULD已经重构为最初使用'calculateIndex'方法发布。 However, if the 'Extract Method' Refactoring defined by Fowler is completely applied, then one gets this [note: dropping the 'e' is intentional to differentiate from your method.] 但是,如果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
    }
}

So from the 'design' perspective the problem is the code you have. 因此,从“设计”的角度来看,问题就是你拥有的代码。 Your 'calculateIndex' method does NOT calculate the index. 你的'calculateIndex'方法不计算索引。 It only does sometimes . 有时只会。 The rest of the time, the exception handler does the calculation. 其余的时间,异常处理程序进行计算。

Furthermore, this refactoring is far more accommodating to changes. 此外,这种重构更容易适应变化。 For instance, if you have to change what I assumed was the default value of '1' to a '2', no big deal. 例如,如果您必须将我假设的默认值“1”更改为“2”,那么没什么大不了的。 However, as pointed out by the OP reply quoted, one cannot assume that there is only one default value. 但是,正如所引用的OP回复所指出的,人们不能假设只有一个默认值。 If the logic to set this grows to be only slightly complex it could still easily reside in the encapsulated exception handler. 如果设置它的逻辑增长到稍微复杂,它仍然可以容易地驻留在封装的异常处理程序中。 However, at some point, it too may need to be refactored into it's own method. 但是,在某些时候,它也可能需要重构为它自己的方法。 Both cases still allow the encapsulated method to perform it's function and truly calculate the index. 这两种情况仍然允许封装的方法执行它的功能并真正计算索引。

In summary, when I get here and look at what I believe is the correct code, then there is no compiler issue for discussion. 总而言之,当我到这里看看我认为是正确的代码时,就没有编译问题需要讨论。 (I am most certain you will not agree: that is fine, I just want to be clearer about my viewpoint.) As for compiler warnings that come up for incorrect code, those help me realize in the first place that something is wrong. (我非常肯定你不会同意:这很好,我只是想更清楚我的观点。)对于错误代码的编译器警告,这些帮助我首先意识到出了问题。 In this case, that refactoring is needed. 在这种情况下,需要重构。

As per specs JLS hunting done by "djechlin", specs tells when is the variable definitely unassigned. 根据规范JLS狩猎由“djechlin”完成,规范告诉我什么时候变量肯定是未分配的。 So spec says that in those scenarios it is safe to allow the assignment.There can be scenarios other than the one mentioned in the specs in which case variable can still be unassigned and it will depend on compiler to make that intelligent decision if it can detect and allow an assignment. 因此规范说在这些情况下允许赋值是安全的。除了规范中提到的情况之外,还有其他情况,其中case变量仍然可以被取消分配,如果它可以检测到,它将依赖编译器做出明智的决定并允许作业。

Spec in no way mentions in the scenario specified by you, that compiler should flag an error. 在您指定的方案中没有提及Spec,该编译器应标记错误。 So it depends on compiler implementation of spec if it is intelligent enough to detect such scenarios. 所以它依赖于规范的编译器实现,如果它足够智能来检测这种情况。

Reference: Java Language Specification Definite Assignment section "16.2.15 try Statements" 参考: Java语言规范定义分配部分“16.2.15试用语句”

I faced EXACTLY the same problem Mario, and read this very interresting discussion. 我完全面对马里奥的同样问题,并阅读这个非常有趣的讨论。 I just solved my issue by that: 我刚刚解决了我的问题:

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, I must admit that I liked a lot your post about design, especially that sentence: calculateIndx() calculates sometimes the index , but could we say the same about parseInt() ? @Joeg,我必须承认我很喜欢你关于设计的帖子,尤其是那句话: calculateIndx()有时会计算索引 ,但我们可以对parseInt()说同样的话吗? Isn't that also the role of calculateIndex() to throw and thus not calculate the index when it is not possible, and then making it returning a wrong value (1 is arbitrary in your refactoring) is imho bad. 是不是这也是calculateIndex()的作用,因为当它不可能时抛出并因此不计算索引,然后使它返回错误的值(1在你的重构中是任意的)是非常糟糕的。

@Marko, I didn't understand your reply to Joeg about the AFTER line 4 and BEFORE line 5 ... I'm not strong enough yet in java world (25y of c++ but only 1 in java...), but I thing this case is one where the compiler is right : i could be initialized twice in Joeg's case. @Marko,我不理解你对Joeg关于AFTER第4行和第5行之前的回复......我在java世界中还不够强大(25%的c ++,但只有1在java ...),但是我这种情况下编译器是正确的:我可以在Joeg的情况下初始化两次。

[All what I'm saying is a very very humble opinion] [我所说的都是一个非常谦虚的意见]

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

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