Java 异常处理中篇:finally 中的陷阱(finally 中 return 会发生什么)

2023-12-15 20:35:33 浏览数 (2)

前言

  • 在上一篇文章中,我们介绍了 Java 异常的基本概念,Throwable 、异常处理关键字:try-catch-finally、throw、throws;本篇文章我们将更加深入的了解 finally 在异常处理中的常见问题和底层原理。

版本

  • Java 8

finally 中的陷阱

  • 我们知道无论是否发生异常还是 try 或 catch 中存在 returnfinally 都会执行,下面我们来看看下面几种场景:

finally 中使用 return

  • 当我们在 finally 中使用 return 时,trycatch 中的 return 会失效或异常丢失(见下文),会在 finally 直接返回。
代码语言:java复制
public class Main {
    public static void main(String[] args) {
        System.out.println(extracted());
    }

    private static int extracted() {
        int a = 1;
        try {
            a = 2;
            a = a / 0;
            return a;
        } catch (Exception e) {
            System.out.println(e);
            return a;
        } finally {
            System.out.println("this is finally");
            return -1;
        }
    }
}

// 输出 finally 中直接 return -1 
java.lang.ArithmeticException: / by zero
this is finally
-1 

finally 中修改数据的影响

  • 如果你在 finally 代码块中修改了数据,你可能会有一些奇妙的体验。

基本类型

代码语言:java复制
public class Main {
    public static void main(String[] args) {
        System.out.println(extracted());
    }

    private static int extracted() {
        int a = 1;
        try {
            a = 2;
            return a;
        } finally {
            System.out.println("this is finally");
            a  = 3;
        }
    }
}

// 输出
this is finally
2
  • 我们可以得出结论在 finally 中修改基本类型不会影响 try 、catchreturn 中的返回值(但是会影响 finally 中的 return ,见下面的案例)。
代码语言:java复制
public class Main {
    public static void main(String[] args) {
        System.out.println(extracted());
    }

    private static int extracted() {
        int a = 1;
        try {
            a = 2;
            return a;
        } finally {
            System.out.println("this is finally");
            a  = 3;
            return a;
        }
    }
}

// 输出
this is finally
5

引用类型

代码语言:java复制
// 案例一
public class Main {
    public static void main(String[] args) {
        System.out.println(extracted());
    }

    private static Object extracted() {
        Person person = new Person();
        try {
            return person;
        } finally {
            System.out.println("this is finally");
            person.age = 5;
        }
    }
}

class Person {
    int age;

    @Override
    public String toString() {
        return "Person age= "   age;
    }
}

// try 中的 return 被修改
this is finally
Person age= 5

// 案例二
public class Main {
    public static void main(String[] args) {
        System.out.println(extracted());
    }

    private static Object extracted() {
        Person person = new Person();
        try {
            return person;
        } finally {
            System.out.println("this is finally");
            person = (new Person());
            person.age = 3;
        }
    }
}

class Person {
    int age;

    @Override
    public String toString() {
        return "Person age= "   age;
    }
}

// try 中的 return 没有被修改
this is finally
Person age= 0
  • 上面的结果看着有点奇怪但实际上很好理解,我们在以前的文章中讲过,Java 实际上只有值传递而不存在引用传递,当为返回值为引用类型时,返回的其实是一个地址,在案例一中我们使用地址修改了原内容,而在案例二中,我们其实将 person 指向了新的地址(new Person()),因此并没有修改原返回值地址的内容。

finally 中的代码 “非最后” 执行

  • 有时候我们发现 finally 中的代码 “非最后” 执行,那么有可能是并行执行了,比如:
代码语言:java复制
public class Main {
    public static void main(String[] args) {
       extracted();
    }

    private static void extracted() {
        try {
            throw new IllegalStateException();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("this is finally");
        }
    }
}

// 比较难出现
this is finally
java.lang.IllegalStateException
	at Main.extracted(Main.java:9)
	at Main.main(Main.java:4)
  • 实际上是因为 e.printStackTrace() 使用的是 System.err,而 System.out.println 使用的是 System.out,标准输出流和标准错误输出流是彼此独立执行的,且 JVM 为了高效的执行会让二者并行运行,所以会出现finally 中的代码 “非最后” 执行的场景。

finally 代码块一定会执行?

  • 虽然这里有一定抬杠的嫌疑,但实际上确实有一些场景下 finally 代码块不会执行,比如:
代码语言:text复制
在 try-catch 语句中执行了 System.exit
在 try-catch 语句中出现了死循环
在 finally 执行之前 JVM 崩溃
  • try-catch 语句中执行了 System.exit
代码语言:java复制
public class Main {
    public static void main(String[] args) {
       extracted();
    }

    private static void extracted() {
        try {
            // 此代码块执行完程序退出
            System.exit(0);
            throw new IllegalStateException();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("this is finally");
        }
    }
}

异常丢失

  • 如果我们在 finally 代码块中抛出异常或使用 retrun,将会导致我们 try-catch 中的异常丢失。
代码语言:java复制
// 案例一
public class Main {
    public static void main(String[] args) throws Exception {
        extracted();
    }

    private static void extracted() throws Exception {
        try {
            throw new IllegalStateException();
        } finally {
            throw new Exception("Exception");
        }
    }
}

// 输出
Exception in thread "main" java.lang.Exception: Exception
	at Main.extracted(Main.java:11)
	at Main.main(Main.java:4)

// 案例二
public class Main {
    public static void main(String[] args) throws Exception {
        extracted();
    }

    private static int extracted() throws Exception {
        try {
            throw new IllegalStateException();
        } finally {
            return 1;
        }
    }
}

finally 底层原理分析

  • 《The JavaTM Virtual Machine Specification, Second Edition》 一书中我们可以知道 Java 虚拟机是如何编译 finally:实际上,Java 虚拟机会把 finally 语句块作为 subroutine 直接插入到 try 语句块或者 catch 语句块的控制转移语句之前。还有另外一个不可忽视的因素,那就是在执行 subroutine(也就是 finally 语句块)之前,try 或者 catch 语句块会保留其返回值(基本类型值或地址)到本地变量表(Local Variable Table)中,待 subroutine 执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过 return 或者 throw 语句将其返回给该方法的调用者(invoker)。
  • 理解了 JVMfinally 的实现,我们其实就很好理解 finally 中修改数据的影响 中的案例,有兴趣的朋友可以下去深入了解。

总结

  • 本文我们结合了 finally 在实际使用中可能出现的问题并进行分析对应的原因,最后介绍了 finallyJVM 中的实现原理,帮助我们在日常开发的更好的使用 finally,下篇文章将会介绍实际异常处理中的一些最佳实践。

个人简介

0 人点赞