Java基础:异常处理

145 阅读6分钟

基本概念

异常类层次结构图 image.png

Exception和Error的区别

Exception和Error都是继承自Throwable类

  • Exception;是程序本身可以处理的异常。可以通过try catch来进行捕获。Exception又分为checked Exception(受检查的异常,必须处理)和unchecked Exception(不受检查异常,可以不处理)
  • Error:是程序无法处理的错误。不建议通过catch进行捕获处理。比如:Java虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等。这些异常发送的时候,Java虚拟机一般会选择线程终止。

Checked Exception和Unchecked Exception有什么区别

  • Checked Exception是必须要处理的异常,代码中要通过try或throws进行处理,否则无法编译通过。
    • 除了RuntimeException及其子类之外,其他的Exception及其子类都是checked exception。常见的有:IO相关的异常、ClassNotFoundException、SQLException。
  • Unchecked Exception是不受检查异常,即使不处理也不影响代码编译。
    • RuntimeException 及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):
      • NullPointerException(空指针错误)
      • IllegalArgumentException(参数错误比如方法入参类型错误)
      • NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
      • ArrayIndexOutOfBoundsException(数组越界错误)
      • ClassCastException(类型转换错误)
      • ArithmeticException(算术错误)
      • SecurityException (安全错误比如权限不够)
      • UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)

Throwable类常用方法有哪些

  • String getMessage():返回异常发生时的简要描述
  • String toString():返回异常发生时的详细信息
  • String getLocalizedMessage():返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则返回的信息与getMessage()返回的结果相同
  • void printStackTrace():在控制台打印Throwable对象封装的异常信息

常见问题

try-catch-finally如何使用

  • try块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch块:用于处理 try 捕获到的异常。
  • finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。

try-catch-finally里有return时的执行结果

  • finally块中定义的返回值将会覆盖catch块、try块中定义的返回值;catch块中定义的返回值将会覆盖try块中定义的返回值

  • finally是在return语句执行之后,方法返回结果之前执行的(return语句之后执行,并没有实际返回运算后的值,而是先把要返回的值保存起来,不管finally中对这个变量做了什么操作,返回的值都不会改变,仍然是返回保存的数值)。所以如果finally中没有return,即使对数据有操作也不会影响返回值,即如果finally中没有return,函数返回值是在finally执行前就已经确定了

  • finally中如果包含return,那么程序将在这里返回,而不是try或catch中的return返回,返回值就不是try或catch中保存的返回值了。

finally语句块里修改基本类型是不影响 返回结果的。(传值) 修改list,map,自定义类等引用类型时,是影响返回结果的。(传址的)对象也是传址的 但是date类型经过测试是不影响的。

代码实战

public class TryCatchTest {
    public static void main(String[] args) {
        //String result = handleException1();
        //String result = handleException2();
        String result = handleException3();
        System.out.println(result);
    }

    // try块中有return,且无异常,try/catch外有return。此时最外层的return不会覆盖try块中的return。
    private static String handleException1() {
        try {
            System.out.println("");
            return "try块的返回值";
        } catch (Exception e) {
            System.out.println("捕获到了异常");
        } finally {
            System.out.println("finally块执行完毕了");
        }
        return "最终的结果";
    }

    //try和finally块中都有返回值,且无异常。此时finally块的返回值会覆盖try块的返回值
    private static String handleException2() {
        try {
            System.out.println("");
            return "try块的返回值";
        } catch (Exception e) {
            System.out.println("捕获到了异常");
        } finally {
            System.out.println("finally块执行完毕了");
            return "finally块的返回值";
        }
    }

    //try、catch、finally中都有return,且有异常。此时try块的返回值不会被执行到,finally块的返回值覆盖了catch块的返回值。
    private static String handleException3() {
        try {
            System.out.println("try开始");
            int[] array = {1,2,3};
            int i = array[10];
            return "try结束";
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("捕获到了异常");
            return "catch的返回值";
        } finally {
            System.out.println("finally块执行完毕了");
            return "finally的返回值";
        }
    }
}

以上3个方法的执行结果分别如下

try开始
捕获到了异常
finally块执行完毕了
finally的返回值
java.lang.ArrayIndexOutOfBoundsException: 10
	at exception.TryCatchTest.handleException3(TryCatchTest.java:42)
	at exception.TryCatchTest.main(TryCatchTest.java:7)

finally中的代码一定会执行吗

  • 如果程序能够正常运行结束,那finally中的代码一定会执行。
  • 如果在执行finally代码之前遇到一些异常情况,比如虚拟机被终止、程序所在的线程死亡,那finally中的代码就无法执行。

如何使用try-with-resource代替try-catch-finally

《Effective Java》中明确指出:

面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally则几乎做不到这点。

Java 中类似于InputStreamOutputStreamScannerPrintWriter等的资源都需要我们调用close()方法来手动关闭,一般情况下我们都是通过try-catch-finally语句来实现这个需求,如下:

//读取文本文件的内容
Scanner scanner = null;
try {
    scanner = new Scanner(new File("D://read.txt"));
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (scanner != null) {
        scanner.close();
    }
}

使用 Java 7 之后的 try-with-resources 语句改造上面的代码:

try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

当然多个资源需要关闭的时候,使用 try-with-resources 实现起来也非常简单,如果你还是用try-catch-finally可能会带来很多问题。

通过使用分号分隔,可以在try-with-resources块中声明多个资源。

try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
     BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
    int b;
    while ((b = bin.read()) != -1) {
        bout.write(b);
    }
}
catch (IOException e) {
    e.printStackTrace();
}

异常使用有哪些需要注意的地方

  • 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。

  • 抛出的异常信息一定要有意义。

  • 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException

  • 使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)。

参考文章

  1. try-catch-finally-return执行顺序
  2. JavaGuide面试题