"终于"阻止来自"的返回值"尝试"块

时间:2014-03-31 20:23:47

标签: java

我正在阅读此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

好的,然后我尝试使用Stringint,我的代码片段为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。

5 个答案:

答案 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(...)调用修改该实例。