很多程序员多年都没掌握的异常处理技巧和原则

Java中的异常机制是指:当程序在运行过程中遇到意外情况时会自动抛出一个Exception对象来通知程序,程序收到这个异常通知后可以采取各种处理措施,这种机制能使程序更加健壮,可读性更高。本文就来讲讲异常处理的相关知识。


异常分类

Java中的异常分为RuntimeException和CheckedException。

  • RuntimeException:程序运行过程中出现错误,才会被检查的异常。例如:类型错误转换,数组下标访问越界,空指针异常、找不到指定类等等。
  • CheckedException:来自于Exception且非运行时异常都是检查异常,编译器会强制检查并通过try-catch块来对其捕获,或者在方法头声明抛出该异常,交给调用者处理。常见的checked异常有FileNotFoundExceptionInterruptedException等。

很多程序员多年都没掌握的异常处理技巧和原则

Error和Exception的区别

在谈到Exception时,经常会涉及到Error。Error和Exception存在如下的区别:

Error是指系统中的错误,程序员是不能改变和处理的,是在程序运行时出现的错误,只能通过修改程序才能修正。Java中的Error一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止,调整代码或者虚拟机参数再重新启动程序;

Exception(异常)是程序可以处理的。遇到这类异常,程序员应该尽可能捕获处理异常,使程序恢复运行,而不应该随意终止异常。实在不知道如何处理就向上抛出该异常留给调用者处理。

总结下:异常(Exception)是一种非程序原因的操作失败(Failure),而错误(Error)则意味着程序有缺陷(Bug)。

异常处理的原则

1. 能处理的异常尽早处理
对于能明确知道要怎么处理的异常要第一时间处理掉。对于不知道要怎么处理的异常,要么直接向上抛出,要么转换成RuntimeException再向上抛出,让调用者处理。

2. 具体明确原则
尽量不要用Exception捕获抛出所有异常。抛出异常时需要针对具体问题来抛出异常,抛出的异常要足够具体详细;在捕获异常时需要对捕获的异常进行细分,这时会有多个catch语句块,这几个catch块中间泛化程度越低的异常需要越放在前面捕获,泛化程度高的异常捕获放在后面,这样的好处是如果出现异常可以近可能得明确异常的具体类型是什么。

抛出时:

public FileInputStream(File file) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name);
    }
    //根据不同的情况抛出不同的异常
    if (name == null) {
        throw new NullPointerException();
    }
    if (file.isInvalid()) {
        throw new FileNotFoundException("Invalid file path");
    }
    ...
}

捕获时:

public void foo1(String fileName){
    File file = new File(fileName);
    InputStream in = null;
    try {
        in = new FileInputStream(file);
        Integer num = in.read();
        in.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e){
        e.printStackTrace();
    } catch(Exception e){
        e.printStackTrace();
    }
}

3. 系统需要有统一异常处理机制
系统需要有自己的一套统一异常处理的机制,如果系统使用Spring等框架的话可以非常简单地引入统一异常处理框架。另外在统一异常处理时一定要打出异常堆栈,不然的话问题可能就无从查起了。

4. 一些其他注意点

  • try语句块内要分清稳定代码和非稳定代码,对于稳定的不会出现异常的代码不要放到try语句块中;
  • catch捕获的异常一定要处理,吃掉异常不处理的话将是灭顶之灾;
  • finally中不要使用return语句,因为finally语句块最后一定会执行,这里的return语句会覆盖之前的return语句。

如何自定义异常

在复杂业务环境下,java自带的异常可能满足不了我们业务的需求, 这个时候我们可以自定义异常来进行对业务异常的处理。

public class MyException extends RuntimeException {

    private static final long serialVersionUID = 6958499248468627021L;
   
    private String errorCode;
    
    private String errorMsg;

    public MyException(String errorCode,String errorMsg){
        super(errorMsg);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public MyException(String errorCode,String errorMsg,Throwable throwable){
        super(errorCode,throwable);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public String getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(String errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
}

在使用MyException时,最好定义一个枚举类用来枚举错误代码和错误详情。

return和finally的执行顺序

在Java中,异常处理主要由try、catch、throw、throws和finally5个关键字处理。Java程序中如果出现了异常,而且没有被try-catch块捕获的话,系统会把异常一直往上层抛,直到遇到处理代码。如果一直没有处理块,就抛到最上层,如果是多线程就由Thread.run()抛出,如果是单线程就被main()抛出。抛出之后,如果是子线程,这个子线程就退出了。如果是主程序抛出的异常,那么这整个程序也就退出了。

一个比较常见的异常处理流程如下:

try {
        //step1
        System.out.println("try...");
        throw new RuntimeException("异常1...");
    }catch (Exception e){
        //step2
        System.out.println("catch。。。");
    }finally {
        //step3
        System.out.println("finally。。。");
    }
    //step4
    System.out.println("end...");

上述的代码中由于抛出的异常被顺利catch住了,所以当前线程不会结束,程序会继续往下执行,step4这步代码会被打印出来。

try {
        System.out.println("try...");
        throw new RuntimeException("异常1...");
    }catch (Exception e){
        System.out.println("catch。。。");
        throw new RuntimeException("异常2...");
    }finally {
        System.out.println("finally。。。");
    }
    System.out.println("end...");

上面的代码中,由于catch块中又抛出了一个异常,而这个异常没有相应的catch块处理,所以系统会向上抛这个异常,最后的打印语句也就的不到执行。

try、catch、finally、throw和throws使用归纳

  • try、catch和finally都不能单独使用,只能是try-catch、try-finally或者try-catch-finally。
  • try语句块监控代码,出现异常就停止执行下面的代码,然后将异常移交给catch语句块来处理,catch块执行完之后代码还会继续往下执行
  • finally语句块中的代码一定会被执行,常用于回收资源 。
  • throws:声明一个异常,告知方法调用者。
  • throw :抛出一个异常,至于该异常被捕获还是继续抛出都与它无关。

还有一个比较重要的是return和finally的执行关系,可以参考下这篇博客

异常链

在平时的开发中,常常会在捕获一个异常后抛出另外一个自定义异常,并且希望把异常原始信息保存下来,这被称为异常链。我们在自定义异常时,只要提供一个接收throwable参数的构造函数即可:

public MyException(String errorCode,String errorMsg,Throwable cause){
    super(errorCode,cause);
    this.errorCode = errorCode;
    this.errorMsg = errorMsg;
}

try-with-resources

我们知道,在Java编程过程中,如果打开了外部资源(文件、数据库连接、网络连接等),我们必须在这些外部资源使用完毕后,手动关闭它们。因为外部资源不由JVM管理,无法享用JVM的垃圾回收机制,如果我们不在编程时确保在正确的时机关闭外部资源,就会导致外部资源泄露,紧接着就会出现文件被异常占用,数据库连接过多导致连接池溢出等诸多很严重的问题。

  1. 传统的资源关闭方式
//这种方式关闭资源,代码显得比较臃肿
public static void main(String[] args) {
    FileInputStream inputStream = null;
    try {
        inputStream = new FileInputStream(new File("test"));
        System.out.println(inputStream.read());
    } catch (IOException e) {
        throw new RuntimeException(e.getMessage(), e);
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
    }
}
  1. try-with-resources方式关闭资源
public static void main(String[] args) {
    try (FileInputStream inputStream = new FileInputStream(new File("test"))) {
        System.out.println(inputStream.read());
    } catch (IOException e) {
        throw new RuntimeException(e.getMessage(), e);
    }
}

将外部资源的句柄对象的创建放在try关键字后面的括号中,当这个try-catch代码块执行完毕后,Java会确保外部资源的close方法被调用。代码是不是瞬间简洁许多!当一个外部资源的句柄对象实现了AutoCloseable接口,JDK7中便可以利用try-with-resource语法更优雅的关闭资源,消除板式代码。

这种方式其实是一种语法糖,关于语法糖的详细介绍可以我的博客

参考

相关推荐