「Java中try.catch.finally执行顺序问题」

558 阅读12分钟

「Java中try.catch.finally执行顺序问题」

一、写在前面

最近在Review代码的时候发现了小伙伴出现了比较粗心的代码,在finally中作了return操作。因为Kotlin实在是太香了,Java的一些东西可能都已经淡忘了。这里重新整理一下也提醒一下自己平时多注意这些细节,原代码是这样的

public String getStr(ParamVO paramVO) {
  try {
    //fixme do something
  } catch (Exception e) {
    e.printStackTrace();
  } finally {
    return ""
  }
}

这段代码的想法很好理解,在try内部可能有异常的情况发生,当发生异常的情况时,希望通过finally中的return语句操作返回一个空字符串,以不至于使程序崩溃。但是这里已经是没有理解,finally的执行时机,以及finally块中存在return情况下的值返回情况。而平时在开发过程中,大多数接触到finally的情景基本都是在操作IO流时对流的关闭操作。那么finally的执行时机,以及finally块中包含return时的值返回情况到底是怎样的呢?一起来了解一下。

二、所有情况

罗列出所有的tryfinally以及return的情况,主要分为如下几种,分别讨论每一种情况:

  • A. tryfinally中均包含return字段
  • B. try中包含return字段,finally中没有,并且不修改待返回的值
  • C. try中包含return字段,finally中没有,并且修改待返回的值(基本数据类型)
  • D. try中包含return字段,finally中没有,并且修改待返回的值(对象的属性)
三、分情况讨论
1.情况A tryfinally中均包含return字段

以测试代码为例如下:

public class ReturnTest {
  public static void main(String[] args) {
    System.out.println("result = " + getStr(10));
  }

  private static int getStr(int number) {
    try {
      ++number;
      System.out.println("try was executed");
      return number;
    } catch (Exception e) {
      e.printStackTrace();
      System.out.println("catch was executed");
      number += 10;
      return number;
    } finally{
      System.out.println("finally was executed");
      number += 100;
      return number;
    }
  }
}

tryfinally都包含return语句,那么执行后的返回值是多少呢?11还是101又或者是111?直观上看这不是肯定返回11嘛,没有发生异常catch不会被执行,执行到try中,自增操作然后返回,很明显结果是11啊。看一下打印的信息:

try was executed
finally was executed
result = 111

Process finished with exit code 0

意不意外惊不惊喜,结果上看,在try中参数10被自增加1,但是此时值并没有返回。而是执行到finally中,并且在try的结果之上自增了100,因此最终结果为111,并且返回的结果为111try中的return没有被最终执行。当然这里改变的是基本数据类型,那么如果是对象中的属性值呢?是否一样会被覆盖呢?看第二个测试代码,新增一个UserVO对象:

public class UserVO {
    private int age;
    private String name;
    private String address;

    public UserVO(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "UserVO{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

修改一下测试代码:

private static UserVO getStr(UserVO userVO) {
  if (null == userVO) {
    return null;
  }
  try {
    userVO.setName("sai");
    System.out.println("try was executed");
    return userVO;
  } catch (Exception e) {
    e.printStackTrace();
    System.out.println("catch was executed");
    userVO.setName("apple");
    return userVO;
  } finally{
    System.out.println("finally was executed");
    userVO.setName("mac");
    return userVO;
  }
}

//main 方法中
public static void main(String[] args) {
  System.out.println("result = " + getStr(10));
  
  System.out.println("--------------------------------");
  UserVO userVO = new UserVO(5, "sai");
  System.out.println("result = " + getStr(userVO));
}

这里在finally中对UserVO对象中的属性name作了修改并返回了对象,按照之前的测试用例,代码执行到finally直接返回,那么UserVO中的name属性应该同样会被覆盖并且跟是否是基本数据类型或者是对象没有关系。看一下打印的信息:

try was executed
finally was executed
result = 111
--------------------------------
try was executed
finally was executed
result = UserVO{age=5, name='mac', address='null'}

Process finished with exit code 0

结果很明显,跟猜测的结果是一样的,属性的值同样会被覆盖了,那么可以得出结论:

  • tryfinally均包含return操作时,无论返回值是基本数据类型还是对象,返回值都会被finally中操作覆盖,当然如果是基本数据类型,finally中的操作是基于在try中操作后的基础上。
2. 情况C、D try中包含return字段,finally中没有,并且修改待返回的值(属性)

还是以为上述的代码测试,这里将finally中的return操作删除,分别测试对基本数据类型与对象的操作,来看看最终的结果是什么样子的。首先以基本数据类型为例,修改后的代码:

private static int getStr(int number) {
  try {
    ++number;
    System.out.println("try was executed");
    return number;
  } catch (Exception e) {
    e.printStackTrace();
    System.out.println("catch was executed");
    number += 10;
    return number;
  } finally{
    System.out.println("finally was executed");
    number += 100;
    System.out.println("finally was executed" + "number = " + number);
    //return number;
  }
}

//main method
public static void main(String[] args) {
  System.out.println("result = " + getStr(10));
}

那么此时返回的是11还是111呢?答案是11

try was executed
finally was executed
finally was executednumber = 111
result = 11

这里虽然结果为11,但是可以发现的是,在finally块中,number的值还是被增加了,只不过由于finally中没有返回字段,而返回了try中值11。其实finally中的操作明显是在tryreturn操作之前。现在即使不测试对UserVOname属性的修改应该也可以猜到,对于对象中的属性会被finally所覆盖。因为finally块中的操作是先于tryreturn字段之前的(如果try中包含有return字段),对于对象的属性修改,即使finally不返回,try中返回的属性也是被finally修改过后的值。看测试用例:

private static UserVO getStr(UserVO userVO) {
  if (null == userVO) {
    return null;
  }
  try {
    userVO.setName("sai");
    System.out.println("try was executed");
    return userVO;
  } catch (Exception e) {
    e.printStackTrace();
    System.out.println("catch was executed");
    userVO.setName("apple");
    return userVO;
  } finally{
    System.out.println("finally was executed");
    userVO.setName("mac");
   //return userVO;
  }
}

//main
public static void main(String[] args) {
    UserVO userVO = new UserVO(5, "sai");
    System.out.println("result = " + getStr(userVO));
}

打印的信息如下:

try was executed
finally was executed
result = UserVO{age=5, name='mac', address='null'}

Process finished with exit code 0

可以看到确确实实是被finally中的操作覆盖了。可以得出结论:

  • try中包含有returnfinally没有,并且finally块中作值的修改,或者对象的属性修改时;对于基本数据类型如int,即使finally中修改值,返回结果依然是try中期望计算的结果值。但是如果为对象的属性,那么返回的结果会与try中的结果有偏差,期望结果(属性值)被finally中的修改所覆盖。
3.情况B try中包含return字段,finally中没有,并且不修改待返回的值
  • 这种情况比较简单,理论上这种仅仅是打印一些Log信息,finally中没有return字段同时也不对待返回值进行修改,那么返回值就是try中计算或者操作的期望值。但是需要注意的是,finally的操作是先tryreturn操作之前完成的。
四、异常情况

上面讨论了正常的情况,try块中正常执行没有异常的情况,那么如果try中如果存在异常,执行顺序与返回值会是什么情况呢?来看看异常的情况。分别对上述A、B、C、D四种情况加上异常。

1.情况A tryfinally中均包含return字段(try中抛出异常)

修改测试代码如下:

private static int getStr(int number) {
  try {
    ++number;
    System.out.println("try was executed");
    //return number;
    throw new Exception("模拟异常的情况");
  } catch (Exception e) {
    e.printStackTrace();
    System.out.println("catch was executed");
    number += 10;
    return number;
  } finally{
    System.out.println("finally was executed");
    number += 100;
    System.out.println("finally was executed" + "number = " + number);
    return number;
  }
}

//main 
public static void main(String[] args) {
  System.out.println("result = " + getStr(10));
  System.out.println("--------------------------------");
}

此时是不是跟正常情况一样呢?catch的值被finally所覆盖?看一下打印的结果:

try was executed
catch was executed
finally was executed
finally was executednumber = 121
result = 121
--------------------------------
java.lang.Exception: 模拟异常的情况
	at main.test.ReturnTest.getStr(ReturnTest.java:21)
	at main.test.ReturnTest.main(ReturnTest.java:10)

Process finished with exit code 0

可以发现结果跟预想的是一样的,当try中发生异常情况,代码执行到catch中,并且对number执行了自增操作赋值此时,number = 21,因为trynumber被增加了1,当然这种情况不是确定的,具体的还的看代码实现。当catch中被赋值后并没有直接返回结果,而是执行到finally中并且返回了最终的结果121。由此可以得出结论:

  • tryfinally均包含return操作时,并且try中存在异常,在catch中即使对基本数据类型数据进行操作,返回值还是会被finally所覆盖。

那么对于对象的属性修改是不是同样的呢?修改测试代码,继续来一下打印的信息:

private static UserVO getStr(UserVO userVO) {
  if (null == userVO) {
    return null;
  }
  try {
    userVO.setName("sai");
    System.out.println("try was executed");
    throw new Exception("模拟异常的情况");
    // return userVO;
  } catch (Exception e) {
    e.printStackTrace();
    System.out.println("catch was executed");
    userVO.setName("apple");
    return userVO;
  } finally{
    System.out.println("finally was executed");
    userVO.setName("mac");
    return userVO;
  }
}

//main 
public static void main(String[] args) {
  UserVO userVO = new UserVO(5, "sai");
  System.out.println("result = " + getStr(userVO));
}

//打印信息
try was executed
catch was executed
finally was executed
result = UserVO{age=5, name='mac', address='null'}
java.lang.Exception: 模拟异常的情况
	at main.test.ReturnTest.getStr(ReturnTest.java:44)
	at main.test.ReturnTest.main(ReturnTest.java:13)

Process finished with exit code 0

其实结论是一样的,属性依然是最终被finally所覆盖了,修改一下结论:

  • tryfinally均包含return操作时,并且try中存在异常,在catch中即使对基本数据类型数据进行操作或者对对象属性的修改,返回值还是会被finally所覆盖。
2.情况C、D try中包含return字段,finally中没有,并且修改待返回的值(属性),并且try抛出异常

直接修改测试代码,去掉finallyreturn字段:

//基本数据类型
private static int getStr(int number) {
  try {
    ++number;
    System.out.println("try was executed");
    //return number;
    throw new Exception("模拟异常的情况");
  } catch (Exception e) {
    e.printStackTrace();
    System.out.println("catch was executed");
    number += 10;
    return number;
  } finally{
    System.out.println("finally was executed");
    number += 100;
    System.out.println("finally was executed" + "number = " + number);
    //return number;
  }
}
//对象的属性
private static UserVO getStr(UserVO userVO) {
  if (null == userVO) {
    return null;
  }
  try {
    userVO.setName("sai");
    System.out.println("try was executed");
    throw new Exception("模拟异常的情况");
    // return userVO;
  } catch (Exception e) {
    e.printStackTrace();
    System.out.println("catch was executed");
    userVO.setName("apple");
    return userVO;
  } finally{
    System.out.println("finally was executed");
    userVO.setName("mac");
    //return userVO;
  }
}
//main
public static void main(String[] args) {
  System.out.println("result = " + getStr(10));
  System.out.println("--------------------------------");
  UserVO userVO = new UserVO(5, "sai");
  System.out.println("result = " + getStr(userVO));
}

//打印的信息
try was executed
catch was executed
finally was executed
finally was executednumber = 121
result = 21
--------------------------------
try was executed
catch was executed
finally was executed
result = UserVO{age=5, name='mac', address='null'}
java.lang.Exception: 模拟异常的情况
	at main.test.ReturnTest.getStr(ReturnTest.java:21)
	at main.test.ReturnTest.main(ReturnTest.java:10)
java.lang.Exception: 模拟异常的情况
	at main.test.ReturnTest.getStr(ReturnTest.java:44)
	at main.test.ReturnTest.main(ReturnTest.java:13)

Process finished with exit code 0

finally中没有return字段,这里分别对基本数据类型以及对象属性进行测试,在try中抛出异常,虽然在catch中对字段、属性重新赋值,但是同时也在finally进行修改。可以发现情况与try正常情况是一致的,即对于基本数据类型期望值以catch为准,而对象属性在经过finally操作后依然会被覆盖了。为什么会存在上述这些情况呢?那具体的代码执行流程就需要借助字节码信息了,看看到底是怎样执行的。

五、字节码信息

借助Java自带的工具,在终端Terminal可以直接将 .java文件编译成 .class文件,命令:

 javac xxxxxxx.java

这里使用的编译器是IDEA,存在一个小问题,会提示 找不到文件,只需要在移动到当前待编译的Java文件目录下执行命令就可以了。得到 .class文件后,执行命令:

javap -verbose -p xxxxxx.class

就可以在终端中显示具体的信息了,这里只截取了几个关键的信息,方便查看:

finally字节码.png

其中iinc表示自增操作,至于其他的信息可以对照字节码表对照来看,主要关心的是为什么,finally的执行顺序以及存在return时的执行机制。观察字节码信息,可以发现,明明我们只写了一个finally,但是编译后为什么会存在三处finally?稍加思考就可以明白,这其实是为了保证finally一定会被执行到。注意是一定会执行。无论try中是否存在异常,还是正常的情况,finally一定会执行,区别在于基本数据类型与对象属性以及返回的值是不是我们所期望的,对于属性,由于java值传递可以很好解释,即使finally中不包含return,依然会覆盖了。而基本数据类型的值返回情况,字节码信息中也很清晰,有兴起的可以编译出来看看就一目了然了。以前仅仅知道,或者是记住finally代码一定会执行,通过编译出来,就可以很清晰的知道jvm到底是怎样保证这个机制的。

六、结论
  • try、catch、finally均有return字段时,对于基本数据类型或是对象的属性,无论是异常情况还是正常的情况,finally中的代码一定会被执行,并且此时的返回值不是try或者catch中计算的期望值,而是finally最终修改的值。

  • try、catchreturn字段而finally中没有时,对于正常的情况下并且是基本数据类型,返回值是try中计算的期望值,而对于对象的属性依然会被finally所覆盖。(另外虽然基本数据类型在finally中没有被返回,但其实值已经被修改,try中返回的值是其首次计算得到的结果的保存值)。

  • try、catchreturn字段而finally中没有时,对于异常的情况下并且是基本数据类型,返回值是catch中计算的期望值,而对于对象的属性依然会被finally所覆盖。(另外虽然基本数据类型在finally中没有被返回,但其实值已经被修改,catch中返回的值是其首次计算得到的结果的保存值)。

  • finally一定会被执行,这是从编译信息中得出的,这就要求在平时编码的时候要特别注意,并且最好是不要在其内部执行return操作,一般场景下,接触的比较多的就是对资源的释放,比如IO流的关闭操作等。