【问题标题】:Effect of "finally" block on return values from "try" block [duplicate]“finally”块对“try”块返回值的影响[重复]
【发布时间】:2014-03-31 20:23:47
【问题描述】:

我正在阅读此question 并得到以下代码 sn-p:

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 尝试了同样的方法,下面是我的代码sn-p 和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。

【问题讨论】:

  • stackoverflow.com/questions/16030858/…你可以参考这篇文章了解更多详情..
  • 我已经阅读了答案,他们说 StringBuilder 对象指向相同的引用,所以我们可以更改如果我引用 null 怎么办我仍然得到 ReturnCool 作为答案为什么不是null
  • @Vishrant 如果引用null,则将引用变量的值更改为null。如果只是修改StringBuilder 本身,则根本不会更改引用变量的值。它仍然指向同一个对象。将变量视为一个整数,其中包含一个对象的内存地址。
  • @Vishrant 这正是同一个问题(和答案),即使示例不同。 intint,引用变量是内存地址。规则是一样的。考虑一个引用变量,就像您返回一个 int 一样,它是一个数据数组的索引。您可以从 finally 块更改该数组中的数据,但不能更改返回的 index。稍后,如果您访问该索引处的数组,您将看到 finally 块对数据所做的更改,但返回的值没有更改。原语和参考值是一样的。
  • 请注意,不变性与这里的一般概念无关。如果返回值是等于 42 的 int、等于 98.6 的 double 或等于 0x12345678 的内存地址,则没有区别。返回的值不能被finally 块修改,尽管finally 块当然可以修改它希望的任何其他数据,包括返回的内存地址恰好指向的数据。这个问题和这个一样,答案也是一样的。

标签: java


【解决方案1】:

TL;DR:您已经告诉方法要返回什么。 finally 块发生在您完成此操作之后。此外,引用变量只是指向对象的指针,虽然finally 块不能更改引用本身,但它肯定可以更改引用指向的对象。

长答案:

这里发生了两件事:


首先,您已经告诉方法要返回什么。 JLS 非常简单地说明了这一点,来自14.20.2(强调我的):

带有 finally 块的 try 语句通过首先执行 try 块来执行。

也就是说,try 块在 finally 块运行之前完全执行。另外,来自14.7

...执行这样的返回语句首先评估表达式

这意味着try 块具有的所有效果(包括指定方法应返回的值)都是完整的,并且要返回的值已被完全评估并且现在“一成不变” ,可以这么说。

让我们首先专注于此。在这里,我们看一下原始示例的略微修改版本。您发布的原始示例不是一个很好的示例,因为整数的初始值 10 加上 finally 块中的 10 also 恰好等于 20,这掩盖了真正发生的事情.所以让我们考虑一下:

protected int setOne(){
    int value = 5;
    try{
        value = 20;
        return value;
    }finally{
        value = value + 10;
    }
}

这个方法的返回值是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;
    }
}

此方法返回 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”。此方法返回对恰好包含数据“CoolReturn+1”的StringBuilder 的引用。

在此示例中,评估第一行 [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 的引用。为什么?由于上面已经描述的所有原因。这一行:

str = str + "fail";

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


结论:

你可以想出无数个例子,但是规则总是一样的:返回值是在return语句中求值的,这个值以后不能改变。引用变量只是保存对象内存地址的值,并且该内存地址值不能更改,即使该地址处的对象肯定可以被finally修改。

另请注意,不变性与这里的一般概念无关。在String 示例中,这有点牵强附会。请记住,即使Strings 可变的,我们也永远不会期望二进制 + 运算符修改其左操作数的字段(例如,即使字符串具有 @987654363 @ 方法,a = a + b 不会修改 a 的任何字段,它会返回一个新对象,然后将对该对象的引用存储在 a 中,而原始对象保持不变)。这里的一个混淆来源是 Java 允许在 String 上使用 + 以方便。没有其他对象直接支持这样的运算符(不包括原始包装器的自动拆箱)。

我确实将此问题标记为与Why does changing the returned variable in a finally block not change the return value? 重复。我相信,一旦您了解了这里的概念,就会清楚那里的问题和答案与这里的问题和答案基本相同。

【讨论】:

  • 这在技术上是正确的,但并不能真正解释发生了什么......
  • 好的,那么它也适用于第一种情况。那么它不只是返回CoolReturn?
  • -1,第一个示例与您刚才描述的完全一样,结果出乎意料。
  • @JustinC 不,它没有。要返回的值是对StringBuilder引用。该值保持不变。这并不意味着该引用指向的对象不能被修改。
  • @Vishrant 不。它根本没有返回字符串。它返回对StringBuilder 的引用。返回的引用与return 指定的引用相同。修改reference指向的对象对返回的实际参考值没有影响;还是一样的StringBuilder
【解决方案2】:

第一个和第二个方法返回对对象的引用,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 加到变量的值上,为变量分配一个新值。

我使用了这个例子,因为它的字节码比字符串缓冲区更容易阅读,但是你可以用它来做,你会得到类似的结果。

【讨论】:

  • +1。这说得好。执行return..语句后需要返回的已经缓存了,在Stringbuilder的情况下通过引用,最终更新缓存值中的代码,其余情况更新对缓存值无效。
  • 这与不变性无关。
  • +1,这就是我的答案。
  • @SotiriosDelimanolis 我试图解释字符串对象不能被修改,它解释得不好,我已经编辑了它;)
  • 编辑详细说明
【解决方案3】:

在第一个不工作的例子中,你做

return str;

然后在 finally 块中你做

str = str + "fail";

finally块中的代码等价于

StringBuilder temp = new StringBuilder();
temp.apend(str);
temp.append("fail");
str = temp.toString();

这不会影响返回的对 str 的原始引用。 str 的原始值被保存以返回,然后您将 str 更改为指向其他位置。不会返回该新引用。

【讨论】:

    【解决方案4】:

    您在第二个示例中得到success,因为StringBuilder 类在使用这些方法时不会创建字符串的新实例。在第二个示例中,您将返回一个包含“成功”的 str 实例,但在 finally 子句中,您将创建一个包含 successfail 的新实例。

    【讨论】:

      【解决方案5】:

      所有其他答案(到目前为止)都是正确的,但都很难说出真正发生的事情。

      当您编写return x; 时,您正在分配一个不可见的变量,该变量保存函数将在 finally 块执行后返回的值。在您的三个示例中,该变量以不同的方式保存 StringBuilder 引用、String 引用或原始 int 值。

      当您编写finally { x = ...; } 时,您正在为局部变量 x 分配一个新值。这对保存返回值的隐藏变量没有任何影响。在您的第二个示例中,finally 语句中的赋值构造了一个新的 String 实例,并将对它的引用存储在本地 var str 中。但是,隐藏变量仍然引用了原来的 String 实例。

      当你写finally { builder.append(...); } 时,会发生一些不同的事情。在那里,您根本没有分配任何变量。在这种情况下,builder 和隐藏变量都引用同一个 StringBuilder 实例,append(...) 调用会修改该实例。

      【讨论】:

        猜你喜欢
        • 2017-07-20
        • 1970-01-01
        • 2015-06-14
        • 2013-11-22
        • 2015-02-16
        • 1970-01-01
        • 2016-03-28
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多