"finally"块对"try"块返回值的影响

Vis*_*ant 6 java

我正在阅读这个问题,我得到了以下代码片段:

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

答案是: 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";
    }
}
Run Code Online (Sandbox Code Playgroud)

为什么答案是: 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这也是为什么答案是: 20为什么不是30.

Jas*_*n C 8

TL; DR:您已经告诉该方法要返回什么.该finally块发生后,你这样做.另外,引用变量只是指向对象的指针,虽然finally块不能更改引用本身,但它肯定可以更改引用指向的对象.

答案很长:

这里有两件事:


首先,您已经告诉该方法返回什么.JLS从14.20.2开始相当简单地说明了(强调我的):

通过首先执行try块来执行具有finally块的try语句.

也就是说,try块在finally块运行之前完全执行.另外,从14.7开始:

...执行这样的return语句首先评估Expression.

这意味着该try块具有的任何和所有效果(包括指定该方法应该返回的值)都是完整的,并且要返回的值已经完全评估,现在可以说是"一成不变".

让我们首先集中精力.在这里,我们看一下原始示例的略微修改版本.您发布的原始示例不是一个很好的示例,因为10整数的初始值加上10finally块中的值恰好等于20,这使实际发生的事情变得模糊.所以让我们考虑一下:

protected int setOne(){
    int value = 5;
    try{
        value = 20;
        return value;
    }finally{
        value = value + 10;
    }
}
Run Code Online (Sandbox Code Playgroud)

这种方法的回报是20.不是15,不是30,而是20.为什么?因为在try块中,你设置value = 20,然后你告诉方法返回20; valuereturn语句中进行求值,并且它在那时的值是20. finally块没有做什么可以改变你已经告诉方法返回20的事实.

好的,容易的.


现在,在您的其他示例中,正在发生的第二件事是引用变量指向对象.也就是说,它们本质上是保持对象内存地址的原始整数变量.它们遵循与上述原始类型相同的规则!在我们查看其余示例之前,请考虑以下事项:

int array[] = new int[] { 100, 200, 300 };

int example () {
    int index = 1;
    try {
       return index;
    } finally {
       array[index] = 500;
       index = 2;
    }
}
Run Code Online (Sandbox Code Playgroud)

此方法返回1,而不是2(由于上述原因).finally块也会修改array[1].那么,value在以下内容之后包含的内容如下:

int index = example();
int value = array[index];
Run Code Online (Sandbox Code Playgroud)

当然是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]
    }
}
Run Code Online (Sandbox Code Playgroud)

在你的问题中,你已经说过你很困惑因为这个方法返回"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";
    }
}
Run Code Online (Sandbox Code Playgroud)

这将返回对String包含数据"成功" 的引用.为什么?由于上面已经描述的所有原因.这一行:

str = str + "fail";
Run Code Online (Sandbox Code Playgroud)

只需创建一个新String对象,它是两个字符串的串联,然后str为该新对象分配一个引用.但是,与原始int示例一样,我们已经告诉函数返回对"成功"的引用String,无论我们做什么,我们都无法改变它!


结论:

您可以提供无数个示例,但规则将始终相同:返回值在return语句中计算,并且以后不能更改该值.引用变量只是保存对象的内存地址的值,并且即使该地址处的对象当然可以被修改,也无法更改内存地址值finally.

另请注意,不变性与此处的一般概念无关.在这个String例子中,它有点像红鲱鱼.请记住,即使Strings 可变的,我们永远不会期望二元+运算符修改其左操作数的字段(例如,即使字符串有一个append()方法,a = a + b也不会期望修改任何字段a,它将是期望返回一个新对象,然后存储对该对象的引用a,使原始对象保持不变.这里混淆的一个原因是Java允许+String作为方便; 没有其他对象直接支持这样的运算符(不计算原始包装器的自动拆箱).

我确实将此问题标记为重复为什么更改finally块中的返回变量不会更改返回值?.我相信,一旦你的头脑被这里的概念所包围,很明显,那里的问题和答案与问题和答案基本相同.

  • 这在技术上是正确的,但并没有真正解释发生了什么...... (6认同)

Gui*_*ino 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

此函数检索对名为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
Run Code Online (Sandbox Code Playgroud)

对于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
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,在第二种情况下,finally语句访问引用的变量,增加它的值.但在int示例中,它只将变量的值加10,为变量赋值.

我已经使用过这个例子,因为它的字节码比字符串缓冲区更容易被读取,但你可以用它做,你会有类似的结果.

  • +1.这很好.返回的内容已经在执行return ..语句后缓存,在Stringbuilder情况下通过引用缓存,代码在最终更新的缓存值中,剩余的案例更新对缓存值无效. (2认同)