您的位置 首页 java

Java基础:异常捕获与处理,防患于未然

1 前言

应用程序能在正常情况下正确地运行,这是程序的基本要求。但一个健壮的程序,还需要考虑很多会使程序失效的因素,即它要在非正常的情况下,也能进行必要的处理。

程序是由程序员编写的,而程序员是存在思维盲点的,一个合格的程序员能保证 java 程序不会出现编译错误,但却无法“考虑完备”,确保程序在运行时一定不会发生错误,而这些运行时发生的错误,对 Java 而言就是一种“异常”。

异常处理的目的在于通过使用少于目前数量的代码来简化大型、可靠的程序的生成,并且通过这种方式可以使你更加确信:你的应用中没有未处理的错误。异常的相关知识学起来并非艰涩难懂,并且它属于那种可以使你的项目受益明显、立竿见影的特性之一。

2 异常概念

正如“天有不测风云,人有旦夕祸福”。在特定环境下Java程序代码也会发生某些不测情况,即使安排了专业的 软件测试 人员,这仅仅能减少错误,而非避免错误,也就是说,在理论上,在软件使用过程中,发生不可预测的异常在所难免。因此,在代码编写过程中,程序员要做到两点:第一,尽自己所能,减少错误。第二,发挥主观能动性,考虑在发生异常后,该如何处理,防患于未然。前者依赖于程序员的日积月累,后者则是一种较为成熟的“灾后处理”方式。

所谓异常(exception),是指所有可能造成计算机无法正常处理的情况,如果事先没有做出妥善安排,严重的话会使计算机宕机。

3 异常分类

在Java中,异常可分为两大类:java.lang.Exception类与java.lang.Error类。这两个类均继承自java.lang.Throwable类。下图为Throwable类的继承关系图。

将Error类与 Exception 类统称为异常类,但二者在本质上还是有不同的。Error类通常指的是Java虚拟机(JVM)出错了,用户在程序里无法处理这种错误。如果程序在启动时出现Error,则启动失败。Exception类包含了一般性的异常,这些异常通常在捕捉到之后便可做妥善的处理,以确保程序继续运行。

如此多不同的异常类其实并没有比Throwable这个基类多多少属性和方法,大部分类在继承父类后只是定义了几个构造方法,这些构造方法也只是调用了父类的构造方法,并没有额外的操作。

那为什么定义这么多不同的类呢?主要是为了名字不同。异常类的名字本身就代表了异常的关键信息,无论是抛出还是捕获异常,使用合适的名字都有助于代码的可读性和可维护性。

4 异常处理

在程序编制过程中,有一个80/20原则, 即80%的精力花费在20%的事情上,而这20%的事情,就是要处理各种可能出现的错误或异常。异常处理方式有抛出异常和使用try catch语句块捕获并处理异常这两种方式。

4.1 抛出异常

遇到异常时不进行具体处理,而是将异常抛给调用者,由调用者根据情况处理。有可能是直接捕获并处理,也有可能是继续向上层抛出异常。抛出异常有三种形式:throws、throw、系统自动抛出异常。其中, throws 作用在方法上,用于定义方法可能抛出的异常;throw作用在方法内,表示明确抛出一个异常。具体的使用方法如下:

  • throw方法内抛出异常代码示例
     private static int base64toInt(char c,  byte [] alphaToInt) {
        int result = alphaToInt[c];
        if (result < 0) {
            throw new IllegalArgumentException("Illegal character " + c);
        }
        return result;
    }  
  • throws方法上抛出异常代码示例
     public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.headers().frameOptions().disable();
        authorizeConfigManager.config(http.authorizeRequests());
    }  

throw和throws的区别如下。

位置不同:throws作用在方法上,后面跟着的是异常的类;而throw作用在方法内,后面跟着的是异常的对象。

功能不同:throws用来声明方法在运行过程中可能出现的异常,以便调用者根据不同的异常类型预先定义不同的处理方式;throw用来抛出封装了异常信息的对象,程序在执行到throw时后续的代码将不再执行,而是跳转到调用者,并将异常信息抛给调用者。也就是说,throw后面的语句块将无法被执行(finally语句块除外)。

4.2 捕获异常

使用 try catch 捕获并处理异常:使用try catch 捕获异常能够有针对性地处理每种可能出现的异常,并在捕获到异常后根据不同的情况做不同的处理。其使用过程比较简单:用try catch语句块将可能出现异常的代码包起来即可。其中catch只有一条,其实,catch还可以有多条,每条对应一种异常类型。具体的使用方法如下:

 try{
    //可能触发异常的代码
    }catch(NumberFormatException e){
        System.out.println("not valid number");
    }catch(RuntimeException e){
        System.out.println("runtime exception "+e.getMessage());
    }catch(Exception e){
        e.printStackTrace();
    }
}  

异常机制中还有一个重要的部分,就是finally。catch后面可以跟finally语句,语法如下所示:

 try{  
    //可能抛出异常
}catch(Exception e){    
    //捕获异常
}finally{    
    //不管有无异常都执行
}  

finally内的代码不管有无异常发生,都会执行,具体来说:

  • 如果没有异常发生,在try内的代码执行结束后执行。
  • 如果有异常发生且被catch捕获,在catch内的代码执行结束后执行。
  • 如果有异常发生但没被捕获,则在异常被抛给上层之前执行。

由于finally的这个特点,它一般用于释放资源,如数据库连接、文件流等。 try/catch/finally语法中,catch不是必需的,也就是可以只有try/finally,表示不捕获异常,异常自动向上传递,但finally中的代码在异常发生后也执行。 finally语句有一个执行细节,如果在try或者catch语句内有return语句,则return语句在finally语句执行结束后才执行,但finally并不能改变返回值。

对于一些使用资源的场景,比如文件和数据库连接,典型的使用流程是首先打开资源,最后在finally语句中调用资源的关闭方法,针对这种场景, Java 7 开始支持一种新的语法,称之为 try-with-resources ,这种语法针对实现了java.lang.AutoCloseable接口的对象,该接口的定义为:

 public interface AutoCloseable {
   void  close () throws Exception;
}  

没有try-with-resources时,使用形式如下:

 public static void useResource() throws Exception {
     AutoCloseable r = new FileInputStream("hello"); //创建资源     
    try {
        //使用资源     
    } finally {
        r.close();     
    }
}  

使用try-with-resources语法,形式如下:

 public static void useResource() throws Exception {
     try(AutoCloseable r = new FileInputStream("hello")) { //创建资源
         //使用资源     
     }
}  

资源r的声明和初始化放在try语句内,不用再调用finally,在语句执行完try语句后,会自动调用资源的close()方法。 资源可以定义多个,以分号分隔。在Java 9之前,资源必须声明和初始化在try语句块内,Java 9去除了这个限制,资源可以在try语句外被声明和初始化,但必须是final的或者是事实上final的。

4.3 处理逻辑

如果自己知道怎么处理异常,就进行处理;如果可以通过程序自动解决,就自动解决;如果异常可以被自己解决,就不需要再向上报告。 如果自己不能完全解决,就应该向上报告。如果自己有额外信息可以提供,有助于分析和解决问题,就应该提供,可以以原异常为 cause 重新抛出一个异常。 总有一层代码需要为异常负责,可能是知道如何处理该异常的代码,可能是面对用户的代码,也可能是主程序。如果异常不能自动解决,对于用户,应该根据异常信息提供用户能理解和对用户有帮助的信息;对运维和开发人员,则应该输出详细的异常链和异常栈到日志。 这个逻辑与在公司中处理问题的逻辑是类似的,每个级别都有自己应该解决的问题,自己能处理的自己处理,不能处理的就应该报告上级,把下级告诉他的和他自己知道的一并告诉上级,最终,公司老板必须要为所有问题负责。每个级别既不应该掩盖问题,也不应该逃避责任。

5 异常指南

应该在下列情况下使用异常:

  1. 尽可能使用 try-with-resource。
  2. 在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常。)
  3. 解决问题并且重新调用产生异常的方法。
  4. 进行少许修补,然后绕过异常发生的地方继续执行。
  5. 用别的数据进行计算,以代替方法预计会返回的值。
  6. 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
  7. 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
  8. 终止程序。
  9. 进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人。)
  10. 让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资。)

6 总结

异常是 Java 程序设计不可分割的一部分,如果不了解如何使用它们,那你只能完成很有限的工作。异常处理的优点之一就是它使得你可以在某处集中精力处理你要解决的问题,而在另一处处理你编写的这段代码中产生的错误。从而,处理异常情况的代码可以大大减少,代码的可读性、可靠性、可维护性也都可以得到提高。

7 最后的最后

为初学者提供学习指南,为从业者提供参考价值。我坚信 码农 也具有产生洞见的能力。关注【码农洞见】,一起学习和交流吧!

文章来源:智云一二三科技

文章标题:Java基础:异常捕获与处理,防患于未然

文章地址:https://www.zhihuclub.com/183003.shtml

关于作者: 智云科技

热门文章

网站地图