我正在阅读此question,我收到了以下代码段:
public void testFinally(){
System.out.println(setOne().toString());
}
protected StringBuilder setOne(){
StringBuilder builder=new StringBuilder();
try{
builder.append("Cool");
return builder.append("Return");
}finally{
builder.append("+1");
}
}
答案是: CoolReturn + 1
好的,然后我尝试使用String
和int
,我的代码片段为String
:
public void testFinally(){
System.out.println(setOne().toString());
}
protected String setOne(){
String str = "fail";
try{
str = "success";
return str;
}finally{
str = str + "fail";
}
}
为什么答案是: success
。为什么不successfail
,如在第一种情况下最终附加值,在这里我正在进行连接?
我也试过原始类型int
public void testFinally(){
System.out.println(setOne());
}
protected int setOne(){
int value = 10;
try{
value = 20;
return value ;
}finally{
value = value + 10;
}
}
这也是为什么答案是: 20为什么不是30。
答案 0 :(得分:8)
TL; DR :您已经告诉该方法要返回什么。在之后,finally
块发生了。另外,引用变量只是指向对象的指针,虽然finally
块不能更改引用本身,但它肯定可以更改引用所指向的对象。
答案很长:
这里有两件事:
首先,您已经告诉该方法返回的值。 JLS从14.20.2(强调我的)开始相当简单地说明了它:
带有finally块的try语句由首先执行try块执行。
也就是说,try
块在运行finally
块之前完全执行。另外,来自14.7:
...执行这样的返回语句首先评估表达式。
这意味着try
块具有的任何和所有效果(包括指定方法应返回的值)都已完成,并且要返回的值已经完全评估,现在是#34可以说是石头"。
让我们首先集中注意力。在这里,我们看一下原始示例的略微修改版本。您发布的原始示例并不是一个很好的示例,因为整数的初始值10
加上finally块中的10
也等于20,真正发生的事情。所以让我们考虑一下:
protected int setOne(){
int value = 5;
try{
value = 20;
return value;
}finally{
value = value + 10;
}
}
这种方法的回报是20.不是15,不是30,而是20.为什么?因为在try
块中,您设置了value = 20
,然后告诉方法返回20; value
语句在return
语句中进行评估,其当时的值为20. finally
块的任何内容都无法改变您已经告诉该方法返回的事实20。
好的,很简单。
现在,在您的其他示例中,第二件事是参考变量指向对象。也就是说,它们本质上是保持对象内存地址的原始整数变量。它们遵循与上述原始类型相同的规则!在我们查看其余示例之前,请考虑以下事项:
int array[] = new int[] { 100, 200, 300 };
int example () {
int index = 1;
try {
return index;
} finally {
array[index] = 500;
index = 2;
}
}
此方法返回1,而不是2(由于上述原因)。 finally块也修改array[1]
。那么,value
包含以下内容后会是什么:
int index = example();
int value = array[index];
当然是500。我们可以看到这一点而不需要太多解释。 example
方法将索引返回到数组中。 finally
块修改数组中的数据。当我们稍后查看该索引处的数据时,我们看到它包含500,因为finally
块将其设置为500. 但是更改数组中的数据与返回的索引是无关的事实无关仍然是1.
这与返回引用完全相同。将引用变量视为原始整数,它本质上是大型内存(堆)的索引。修改引用指向的对象就像修改该数组中的数据一样。现在,其余的例子应该更有意义。
让我们看看你的第一个例子:
protected StringBuilder setOne(){
StringBuilder builder=new StringBuilder();
try{
builder.append("Cool"); // [1]
return builder.append("Return"); // [2]
}finally{
builder.append("+1"); //[3]
}
}
在您的问题中,您已经表示您感到困惑,因为此方法会返回" CoolReturn + 1"。但是,这种说法从根本上说没有多大意义!此方法不返回" CoolReturn + 1"。此方法返回对恰好包含数据的StringBuilder
的引用," CoolReturn + 1"。
在此示例中,评估第一行[1]。然后评估行[2],并执行.append("Return")
。然后发生finally
块,并评估行[3]。然后,由于您已经告诉该方法返回对该StringBuilder
的引用,因此返回该引用。 StringBuilder
修改了返回引用所指向的finally
,这样就可以了。这不会影响方法返回的值,它只是对对象的引用(即"索引"进入我前面描述的那个大内存阵列)。
好的,让我们看看你的第二个例子:
protected String setOne(){
String str = "fail";
try{
str = "success";
return str;
}finally{
str = str + "fail";
}
}
这将返回对包含数据的String
的引用," success"。为什么?由于上面已经描述的所有原因。这一行:
str = str + "fail";
只需创建一个新的String
对象,它是两个字符串的串联,然后为str
指定对该新对象的引用。但是,与原始int
示例一样,我们已经告诉函数返回对"成功"的引用。 String
,无论我们做什么,我们都无法改变!
结论:
您可以提供无数个示例,但规则将始终相同:返回值在return
语句中计算,并且以后无法更改该值。引用变量只是保存对象内存地址的值,并且无法更改内存地址值,即使该地址的对象当然可以由finally
修改。
另请注意,不变性与此处的一般概念无关。在String
示例中,它有点像红鲱鱼。请记住,即使String
s 是可变的,我们也绝不会期望二进制+
运算符修改其左操作数的字段(例如即使字符串有,也就是说, ,append()
方法,a = a + b
不会修改a
的任何字段,应该返回一个新对象,然后在a
中存储对该对象的引用,保持原始状态不变。这里混淆的一个原因是Java允许在String上使用+
作为方便;没有其他对象直接支持这样的运算符(不计算原始包装器的自动拆箱)。
我确实将此问题标记为Why does changing the returned variable in a finally block not change the return value?的副本。我相信,一旦你的头脑被这里的概念所包围,很明显,那里的问题和答案与问题和答案基本相同。
答案 1 :(得分:7)
第一个和第二个方法返回对象的引用,finally
块稍后执行,第一个例子中发生的是你仍然保持对对象(构建器)的引用,这样你就可以修改它
在第二个示例中,您有一个字符串,它也是不可变的,因此您无法修改它,只能为变量分配一个新对象。但是您返回的对象未被修改。因此,当您执行str = str + "fail";
时,您将为变量str
分配一个新对象。
在第三个示例中,您有一个整数,它不是一个对象,它返回它的值,稍后在finally
块中,您将变量赋值给一个新的整数,但返回的一个不会被修改
详细说明:
想象一下第四种情况:
public static class Container{
public int value = 0;
}
protected static Container setOne(){
Container container = new Container();
try{
container.value = 20;
return container ;
}finally{
container.value = container.value + 10;
}
}
此函数检索对名为container的变量的引用,并在返回后将容器的value字段递增到+10,因此当您退出该函数时,container.value将为30,就像在StringBuilder示例中一样
让我们将此方法与第三个示例(int方法)进行比较:
如果你得到这两种方法的字节码:
以int:
为例bipush 10
istore_0
bipush 20
istore_0
iload_0
istore_2
iinc 0 10
iload_2
ireturn <- return point
astore_1
iinc 0 10 <- retrieve the variable value and add 10 to it's value
aload_1 <- Store the value of the result of the sum.
athrow
对于Container包装器类的示例:
new Test4$Container
dup
invokespecial Test4$Container/<init>()V
astore_0
aload_0
bipush 20
putfield Test4$Container/value I
aload_0
astore_2
aload_0
aload_0
getfield Test4$Container/value I
bipush 10
iadd
putfield Test4$Container/value I
aload_2
areturn <-- Return point
astore_1 <-- Stores the reference
aload_0
aload_0
getfield Test4$Container/value I <-- gets the value field from the object reference
bipush 10
iadd <-- add the value to the container.value field
putfield Test4$Container/value I <-- Stores the new value (30) to the field of the object
aload_1
athrow
正如您所看到的,在第二种情况下,finally
语句访问引用的变量,增加它的值。但是在int
示例中,它只将变量值加10,为变量赋值。
我已经使用过这个例子,因为它的字节码比字符串缓冲区更容易被读取,但你可以用它来做,你会有类似的结果。
答案 2 :(得分:1)
在第一个非工作示例中,您执行
return str;
然后在你做的finally块中
str = str + "fail";
finally块中的代码等同于
StringBuilder temp = new StringBuilder();
temp.apend(str);
temp.append("fail");
str = temp.toString();
这不会影响对返回的str的原始引用。保存str的原始值以便返回,然后将str更改为指向其他位置。该新引用不会被返回。
答案 3 :(得分:0)
在第二个示例中得到success
,因为StringBuilder
类在使用方法时不会创建字符串的新实例。在第二个示例中,您将返回包含“success”的str
实例,但是在finally子句中,您将创建一个包含successfail
的新实例。
答案 4 :(得分:0)
所有其他答案(到目前为止)都是正确的,但都很难说出真正发生的事情。
当您编写return x;
时,您正在分配一个不可见变量,该变量保存函数将在执行finally块后返回的值。该变量在三个示例中以各种方式保存StringBuilder引用,String引用或原始int值。
当您编写finally { x = ...; }
时,您要为局部变量x指定一个新值。这对保存返回值的隐藏变量没有任何影响。在第二个示例中,finally语句中的赋值构造一个新的String实例,并在本地var,str中存储对它的引用。但是,隐藏变量仍然引用原始的String实例。
当你写finally { builder.append(...); }
时,会发生一些不同的事情。在那里,你根本没有分配任何变量。在这种情况下,builder
和隐藏变量都引用相同的StringBuilder实例,append(...)
调用修改该实例。