【Java】Java 关键字 - finally 关键字

Posted by 西维蜀黍 on 2019-01-18, Last Modified on 2021-09-21

[例子 1] final 的意义

public static int test1(){
        int i = 0;
        try{
            System.out.println("try"); //1
            return i;
        }finally {
            System.out.println("finally"); //2
        }
    }

执行结果

try
finally

分析

无论 try 语句或 catch 语句中是否有 return 语句,finally 语句都一定会被执行。

总结

总结来说,我们可以把必须要执行的操作(比如资源释放操作:关闭流、关闭数据库连接,释放锁)放到 finally 语句里,以确保其一定会被执行。

  • 在某些情况下,try 语句压根就没有执行到,那么 finally 语句也一定就不会执行到了。
  • 还有一种情况,就是在 try 块中有 System.exit(0); 这样的语句,System.exit(0); 会终止 Java 虚拟机(JVM)。因此,连 JVM 都停止了,当然 finally 语句也不会被执行到。

[例子 2]

我们再来看一个程序:

public class Main {
    public static void main(String[] args) {
        System.out.println(test4());
    }

    public static int test4() {
        int b = 20;
        try {
            System.out.println("try block");
            b = b / 0;

            return 1;
        } catch (Exception e) {
            b += 15;
            System.out.println("catch block");
        } finally {
            System.out.println("finally block");
            if (b > 25) {
                System.out.println("b>25, b = " + b);
            }
            b += 50;
        }
        return 204;
    }
}

结果

try block
catch block
finally block
b>25, b = 35
204

说明

打印 204 说明,当在执行 try 语句块的过程中抛出异常,则会直接进入 catch 语句(前提当然是存在 catch 语句),而对于在 try 语句块中的抛出异常位置后的代码,不会被执行。

[例子 3]

再来看一个程序:

public class FinallyTest1 {
    public static void main(String[] args) {
        System.out.println(test11());
    }    
    public static String test11() {
        try {
            System.out.println("try block");
           return test12();
      } finally {
           System.out.println("finally block");
       }
  }

  public static String test12() {
       System.out.println("test12");
       return "test12 return";
   }    
}

结果

try block
test12
finally block
test12 return

说明

return statement 先于 finally block 被打印,这说明:

  • System.out.println("try block"); 先被执行
  • 开始执行 return test12(); 中的 test12()
    • 执行 System.out.println("return statement");
    • 执行 test12()return "test12 return",即函数 test11 的返回值被赋值为 test12 return
  • 执行 finally 语句中的 System.out.println("finally block");
  • test11() 函数返回,返回值为 test12 return
  • 打印 test11() 函数的返回值,即打印 test12 return

结论

try 语句块或 catch 语句块中的 return 语句会先被执行(但不会立即返回),之后再去执行 finally 语句块。

而当等到 finally 语句块执行完成后,函数返回才会执行。

[例子 4] - 值类型

再来看一个程序:

public class Main {
    public static void main(String[] args) {
        int j = query();
        System.out.println(j);
    }
    public static int query() {
        int i = 0;
        try {
            System.out.print("try\n");
            return i += 40;
        } catch (Exception e) {
            System.out.print("catch\n");
            i += 20;
        } finally {
            System.out.print("finally1  i:"+i + "\n");
            i += 10;
            System.out.print("finally2  i:"+i + "\n");
            System.out.print("finally3\n");
            //return i;
        }
        System.out.print("finish");
        return 200;
    }
}

执行结果

try
finally1  i:40
finally2  i:50
finally3
40

分析

  1. 输出 finally1 i:40,说明在执行 final 语句块之前,i += 40 会被执行。
  2. 输出 finally1 i:40,同时说明即使 try 语句块中有 return 语句块,fianlly 还是会被执行。这就是所谓的”finally 一定会执行 “。
  3. 输出 finally2 i:50 finally3 40,说明当执行 try 语句中的 return i += 40; 时,JVM 会先执行 i += 40(等价于 i = i + 40),因此此时 i 的值为 40,并且将变量 i 复制一份(暂且称为 i_return)。此后,JVM 继续去执行 finally 语句(注意,finally 语句中没有 return 语句)。
  4. finish 没有被输出,说明只要在 try 语句块和其对应的 finally 语句块中包含 return 语句,则在整个 try 结构后的代码永远不会被执行。

结论

  • finally 语句是在 return 语句执行后,return 语句返回之前执行的。
    • 从 JVM 管理内存的角度来分析,return 返回后,就代表着方法执行结束,相应的该方法的栈帧就出栈了。而这个时候也就意味着,return 返回必须在最后执行(否则会出现该方法对应的栈帧已经出栈,但是该方法(的 finally 语句块)任然在执行),所以 finally 语句是在 return 返回之前执行的!
  • 当在 try 语句块或 catch 语句块中包含 return 语句(而 finally 语句块中不包含 return 语句),且 return 语句中包含赋值操作(或修改值操作),且赋值或修改的变量是值类型时:
    1. return 语句中的赋值先被执行
    2. 赋值后的新变量会被复制一份
    3. 执行 finally 语句块。因此,在 fianlly 语句块中修改 try 语句块或 catch 语句块中 return 语句中的变量不会影响 return 的结果

[例子 5] - 值类型

再来看一个程序:

public class Main {
    public static void main(String[] args) {
        int j = query();
        System.out.println(j);
    }
    public static int query() {
        int i = 0;
        try {
            System.out.print("try\n");
            return i += 40;
        } catch (Exception e) {
            System.out.print("catch\n");
            i += 20;
        } finally {
            System.out.print("finally1  i:"+i + "\n");
            i += 10;
            System.out.print("finally2  i:"+i + "\n");
            System.out.print("finally3\n");
            return i;
        }
        System.out.print("finish");
        return 200;
    }
}

执行结果

try
finally1  i:40
finally2  i:50
finally3
50

分析

注意!这个例子上上面一个例子唯一的区别在于,在 finally 语句块中存在一个 return 语句。

输出 50,说明:

  1. 当执行 try 语句块中的 return i += 40; 时,JVM 会先执行 i += 40(等价于 i = i + 40),因此执行后 i 的值为 40,并且将变量 i 复制一份(暂且称为 i_return1)。此后,JVM 继续去执行 finally 语句块。
  2. 执行 finally 语句块中的 i += 10;(此后,i 的值为 50)。
  3. 执行 finally 语句块中的 return i;,此时,会将变量 i 复制一份(称为 i_return2),并且将变量 i_return2 的值作为函数的返回值返回。

总结

当在 try 语句块或 catch 语句块中包含 return 语句,且 finally 语句块中也包含 return 语句),且 return 语句中包含赋值操作(或修改值操作),且赋值或修改的变量是值类型时:

  1. try 语句块中的 return 语句中的赋值先被执行
  2. 赋值后的新变量会被复制一份
  3. 执行 finally 语句块,
    1. finally 语句块中的 return 语句被执行
    2. 将 finally 语句块中的 return 语句中的值作为函数的返回值(而 try 语句块中的 return 语句相当于被覆盖了)

[例子 6] - 引用类型

如果这个返回值是引用类型,情况又会有所不同。

import java.util.ArrayList;
import java.util.List;
public class Main {
   public static void main(String[] args) {
       List<String> cats = new ArrayList<>();
       cats  = query(cats);
       System.out.println("----");
       for(String cat : cats)
           System.out.println(cat);
   }
   public static List<String> query(List<String> cats) {
       int i = 0;
       try {
           System.out.print("try\n");
           cats.add("xiaoMeng");
           return cats;
       } catch (Exception e) {
           System.out.print("catch\n");
       } finally {
           System.out.print("finally\n");
           cats.add("qiaoGeLi");
       }
       System.out.println("finish");
       return null;
   }
}

执行结果

try
finally
----
xiaoMeng
qiaoGeLi

分析

由于 cats 是 List 类型,即一个引用类型。对于引用类型的复制只能在当前进程栈帧的局部变量表中复制一份,而其指向到位于堆(Heap)中的内存地址并没有改变(本质上,仍然是同一个对象)。

结论

当在 try 语句块或 catch 语句块中包含 return 语句,且 finally 语句块中也包含 return 语句),且 return 语句中包含赋值操作(或修改值操作),且赋值或修改的变量是引用类型时,无论 finally 语句块中有没有 return 语句,若在 finally 语句块中修改这个引用对象,都会影响最终函数返回值的结果。

总结

  • finally 语句总是在 retrun 语句执行后,return 返回之前执行的,也就是说 finally 必执行(当然是建立在 try 执行的基础上)
  • 当在 try 语句或 catch 语句中有 return 语句时,finally 语句中修改了返回值的值。若修改的返回值为基本数据类型(比如 int,double 之类),finally 语句中没有 return 语句,则 finally 语句中对返回值的修改不会影响最终的返回结果
  • 当在 try 语句或 catch 语句中有 return 语句时,finally 语句中修改了返回值的值。若改返回值为引用类型(比如对象,list,map 之类),无论 finally 语句中有没有 return 语句,finally 语句中对返回值的修改都会影响最终的返回结果

Reference