最后一个variables是否可以重新分配,即使赋值是在try中的最后一个操作?
我在这里相当确信
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()
这样的事情,这可能会导致exception抛出“空气”,但我仍然不知道如何可以抛出后,这显然是最后一个行动在try-block内。
如果允许的话,上面的习语会使我的许多方法更简单。 请注意,这个用例对语言有一stream的支持,比如Scala,它始终使用Maybe monad:
final int i = calculateIndex().getOrElse(1);
我认为这个用例是一个非常好的动机,可以允许我在一个特定的情况下, i
绝对没有在catch-block内分配 。
UPDATE
经过一番思考,我更加确定这只是JLS模型的一个弱点:如果我在上面的例子中声明了公理,当控制到达catch-block时, i
肯定是未分配的,它不会和其他任何冲突公理或定理。 编译器将不会允许i
在读取i
之前将其分配给catch-block,所以无论i
是否被分配,都不能被观察到。
JLS狩猎:
如果最后一个variables被分配给它,那么这是一个编译时错误,除非它在赋值之前立即被赋值(第16章)。
第十六章:
如果满足以下所有条件,则V必须在catch块之前未赋值:
在try块之后,V肯定是未分配的。
在每个属于try块的return语句之前,V肯定是未分配的。
V在e的每一个语句中都是未赋值的,这个语句在属性try块的forms中被抛出。
在try块中发生的每个断言语句之后,V肯定是未赋值的。
V在每个属于try块的break语句之前都是未赋值的,而break语句的目标包含try语句。
在每个属于try块的continue语句之前,V肯定是未分配的,并且其继续目标包含try语句。
大胆是我的。 try
块后, i
不清楚i
是否被分配。
另外在这个例子中
final int i; try { i = foo(); bar(); } catch(Exception e) { // e might come from bar i = 1; }
粗体文本是防止实际错误分配i=1
非法的唯一条件。 因此,这足以certificate“绝对未分配”的更好条件是允许您的原始文章中的代码所必需的。
如果规范修改,以取代这种情况
如果catch块捕获一个未检查的exception,则V在try块之后肯定是未赋值的。
如果catch块捕获一个未经检查的exception,则在最后一条语句能够抛出由catch块捕获的types的exception之前,V肯定是未分配的。
那么我相信你的代码是合法的。 (就我的临时分析而言)
我为此提交了一个JSR,我希望这个JSR被忽略,但我很好奇,看看这些是如何处理的。 技术上传真号码是必填字段,我希望在那里input+ 1-000-000-000不会造成太大的损失。
我认为JVM是可悲的,正确的。 虽然从查看代码的直观上来说是正确的,但在查看IL的上下文中是有意义的。 我创build了一个简单的run()方法,主要模仿你的情况(这里简单的注释):
0: aload_0 1: invokevirtual #5; // calculateIndex 4: istore_1 5: goto 17 // here's the catch block 17: // is after the catch
所以,虽然你不能轻易地编写代码来testing这个,但是因为它不会编译,调用方法,存储这个值,以及catch之后的跳转是三个单独的操作。 你可以 (但是不太可能)有exception发生(Thread.interrupt()似乎是最好的例子)之间的步骤4和步骤5.这将导致进入catch块后,我已被设置。
我不确定你是否可以用大量的线程和中断来实现这个目的(编译器不会让你编写这个代码),但是理论上我可以设置,并且你可以inputexception处理块,即使有这个简单的代码。
不是很干净(我怀疑你已经在做什么)。 但是这只增加了一个额外的线。
final int i; int temp; try { temp = calculateIndex(); } catch (IOException e) { temp = 1; } i = temp;
这是对有利于论点的最有力论据的总结,即现有的明确分配规则在不违反一致性的情况下不能放松(A),其次是我的反驳(B):
-
答 :在字节码级上,对variables的写入不是try-block中的最后一个指令:例如,最后的指令通常是跳过exception处理代码的跳转;
-
B :但是如果这些规则说明了
i
在catch-block内肯定是未分配的,那么它的值可能不会被观察到。 不可观测的价值与无价值一样好。 -
答 :即使编译器声明
i
为绝对未分配 ,debugging工具仍然可以看到值; -
B :实际上,一个debugging工具总是可以访问一个未初始化的局部variables,这在典型的实现中会有任意的值。 未初始化的variables和在实际写入发生之后初始化完成的variables之间没有本质区别。 不pipe在这里考虑的特殊情况如何,工具必须总是使用额外的元数据来知道每个局部variables指定variables的指令范围,只有在执行发现自己在范围内时才允许观察它的值。
定论:
规范可以始终如一地接收更多细粒度的规则,这将允许我的发布示例进行编译。
你是正确的,如果赋值是try块中的最后一个操作,我们知道在进入catch块时variables不会被赋值。 然而,正式确定“最后一次操作”的概念会使规范复杂化。 考虑:
try { foo = bar(); if (foo) { i = 4; } else { i = 7; } }
这个function会有用吗? 我不这么认为,因为最后一个variables必须被分配一次,而不是最多一次。 在你的情况下,如果引发Error
,variables将被取消分配。 如果variables超出了范围,你可能不会在乎,但事实并非总是如此(在相同或者周围的try语句中,可能有另一个catch块捕获Error
)。 例如,考虑:
final int i; try { try { i = foo(); } catch (Exception e) { bar(); i = 1; } } catch (Throwable t) { i = 0; }
这是正确的,但是如果在分配i之后(例如在finally子句中)发生对bar()的调用,或者我们对资源的close方法抛出exception使用try-with-resources语句,则不会这样。
考虑到这一点会增加规格的复杂性。
最后,有一个简单的工作:
final int i = calculateIndex();
和
int calculateIndex() { try { // calculate it return calculatedIndex; } catch (Exception e) { return 0; } }
这显然是我分配。
简而言之,我认为增加这个function会给规范带来很大的复杂性,但是效果不大。
1 final int i; 2 try { i = calculateIndex(); } 3 catch (Exception e) { 4 i = 1; 5 }
OP已经说过,在第4行我可能已经被分配了。 例如,通过asynchronousexceptionThread.stop(),请参阅http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5
现在在第4行设置一个断点,您可以在 1被分配之前观察variablesi的状态。 因此,放松观察到的行为将违背Java™虚拟机工具界面
浏览javadoc,似乎没有任何子类的Exception
可能会被抛出后,我分配。 从JLS理论的angular度来看,似乎Error
可能会在我被分配之后抛出(例如VirtualMachineError
)。
似乎没有JLS要求编译器通过区分是否捕获Exception
或Error
/ Throwable
来确定我是否可以在catch块达到之前设置,这意味着这是JLS模型的弱点。
为什么不尝试以下? (已编译和testing)
(Integer Wrappertypes+ finally +“猫王”运算符testing是否为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编译器按照以下顺序创build对象(伪代码):
mem = allocate(); //Allocate memory ctorInteger(instance);//Invoke constructor for Singleton passing instance. i = mem; //Make instance i non-null
但是,一些JIT编译器会无序写入 。 以上步骤重新sorting如下:
mem = allocate(); //Allocate memory i = mem; //Make instance i non-null ctorInteger(instance); //Invoke constructor for Singleton passing instance.
现在假设, JIT
在执行(1)中创build对象时执行out of order writes
。 假设执行构造函数时抛出一个exception。 在这种情况下, catch
块将有i
not null
。 如果JVM不遵循这个模式,那么在这种情况下finalvariables被允许分配两次!
但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
这意味着它不可能是最终的
根据OP的问题编辑响应
这真的是在回应评论:
你所做的一切都写下了一个清楚的稻草人论点的例子:你在替代地假设必须总是有一个唯一的默认值,对所有呼叫站点都有效
我相信我们正在从对立的angular度来处理整个问题。 看起来你是从底层看的 – 从字节码到Java。 如果不是这样,那么您就是从符合规范的“代码”来看待它。
从相反的方向走,从“devise”下来,我看到了问题。 我认为是福勒先生在“重构:改进现有代码的devise”一书中收集了各种“难闻的气味”。 这里(可能还有许多其他地方)描述了“提取方法”重构。
因此,如果我想象没有“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定义的“提取方法”重构被完全应用,那么就会得到这个[注意:删除'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 } }
所以从“devise”的angular度来看问题就是你的代码。 你的'calculateIndex'方法不计算索引。 它只是有时 。 剩下的时间,exception处理程序进行计算。
而且,这种重构更适应变化。 例如,如果你不得不改变我所假设的默认值'1'到'2',没有什么大不了的。 不过,正如OP引述的答复所指出的,人们不能认为只有一个默认值。 如果设置这个逻辑变得稍微复杂,它仍然可以很容易地驻留在封装的exception处理程序中。 然而,在某些时候,它也可能需要被重构成它自己的方法。 这两种情况仍然允许封装的方法执行它的function并真正地计算索引。
总之,当我到达这里并看看我认为是正确的代码时,就没有讨论的编译器问题了。 (我很肯定你不会同意:这很好,我只是想更清楚一些我的观点。)对于编译器提出的不正确代码的警告,这些帮助我首先意识到有什么地方是错误的。 在这种情况下,需要重构。
根据规范JLS狩猎由“djechlin”完成,specs告诉什么时候variables肯定是未分配的。 所以规范说,在这些情况下,允许赋值是安全的。除了规范中提到的情况之外,情况下variables仍然可以被分配,并且如果它能够检测到并允许分配。
Spec在你指定的场景中绝不会提到,编译器应该标记一个错误。 所以这取决于规范的编译器实现是否足够智能来检测这种情况。
参考: Java语言规范明确赋值部分“16.2.15 try语句”
我面对同样的问题马里奥,并阅读这个非常有争议的讨论。 我刚刚解决了我的问题:
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,我必须承认,我非常喜欢你关于devise的post,特别是那个句子: calculateIndx()有时计算索引 ,但是我们可以说parseInt()是一样的吗? calculateIndex()的作用并非如此,因此在不可能的情况下不计算索引,然后使其返回错误的值(1在重构中是任意的)是不好的。
@Marko,我不明白你对Joeg关于AFTER第4行和第5行之前的回复…我还不够强大,但在Java世界(C ++的25年,但只有1在Java …),但我这种情况下编译器是正确的:我可以在Joeg的情况下初始化两次。
[我所说的只是一个非常非常谦虚的意见]