Java异常捕获与处理,防患于未然!
前言
要想创建健壮的系统,它的每一个构件都必须是健壮的。
应用程序能在正常情况下正确地运行,这是程序的基本要求。但一个健壮的程序,还需要考虑很多会使程序失效的因素,即它要在非正常的情况下,也能进行必要的处理。
程序是由程序员编写的,而程序员是存在思维盲点的,一个合格的程序员能保证Java程序不会出现编译错误,但却无法“考虑完备”,确保程序在运行时一定不会发生错误,而这些运行时发生的错误,对Java而言就是一种“异常”。
异常处理的目的在于通过使用少于目前数量的代码来简化大型、可靠的程序的生成,并且通过这种方式可以使你更加确信:你的应用中没有未处理的错误。异常的相关知识学起来并非艰涩难懂,并且它属于那种可以使你的项目受益明显、立竿见影的特性之一。
异常概念
正如“天有不测风云,人有旦夕祸福”。在特定环境下Java程序代码也会发生某些不测情况,即使安排了专业的软件测试人员,这仅仅能减少错误,而非避免错误,也就是说,在理论上,在软件使用过程中,发生不可预测的异常在所难免。因此,在代码编写过程中,程序员要做到两点:第一,尽自己所能,减少错误。第二,发挥主观能动性,考虑在发生异常后,该如何处理,防患于未然。前者依赖于程序员的日积月累,后者则是一种较为成熟的“灾后处理”范式。
所谓异常(exception),是指所有可能造成计算机无法正常处理的情况,如果事先没有做出妥善安排,严重的话会使计算机宕机。
异常分类
在Java中,异常可分为两大类:java.lang.Exception类与java.lang.Error类。这两个类均继承自java.lang.Throwable类。下图为Throwable类的继承关系图。
【Java异常捕获与处理,防患于未然!】
文章图片
将Error类与Exception类统称为异常类,但二者在本质上还是有不同的。Error类通常指的是Java虚拟机(JVM)出错了,用户在程序里无法处理这种错误。如果程序在启动时出现Error,则启动失败。Exception类包含了一般性的异常,这些异常通常在捕捉到之后便可做妥善的处理,以确保程序继续运行。
如此多不同的异常类其实并没有比Throwable这个基类多多少属性和方法,大部分类在继承父类后只是定义了几个构造方法,这些构造方法也只是调用了父类的构造方法,并没有额外的操作。
那为什么定义这么多不同的类呢?主要是为了名字不同。异常类的名字本身就代表了异常的关键信息,无论是抛出还是捕获异常,使用合适的名字都有助于代码的可读性和可维护性。
异常处理
在程序编制过程中,有一个80/20原则, 即80%的精力花费在20%的事情上,而这20%的事情,就是要处理各种可能出现的错误或异常。异常处理方式有抛出异常和使用try catch语句块捕获并处理异常这两种方式。
抛出异常
遇到异常时不进行具体处理,而是将异常抛给调用者,由调用者根据情况处理。有可能是直接捕获并处理,也有可能是继续向上层抛出异常。抛出异常有三种形式: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()); }
位置不同:throws作用在方法上,后面跟着的是异常的类;而throw作用在方法内,后面跟着的是异常的对象。
功能不同:throws用来声明方法在运行过程中可能出现的异常,以便调用者根据不同的异常类型预先定义不同的处理方式;throw用来抛出封装了异常信息的对象,程序在执行到throw时后续的代码将不再执行,而是跳转到调用者,并将异常信息抛给调用者。也就是说,throw后面的语句块将无法被执行(finally语句块除外)。
捕获异常 使用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内的代码执行结束后执行。
- 如果有异常发生但没被捕获,则在异常被抛给上层之前执行。
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的。
处理逻辑 如果自己知道怎么处理异常,就进行处理;如果可以通过程序自动解决,就自动解决;如果异常可以被自己解决,就不需要再向上报告。
如果自己不能完全解决,就应该向上报告。如果自己有额外信息可以提供,有助于分析和解决问题,就应该提供,可以以原异常为cause重新抛出一个异常。
总有一层代码需要为异常负责,可能是知道如何处理该异常的代码,可能是面对用户的代码,也可能是主程序。如果异常不能自动解决,对于用户,应该根据异常信息提供用户能理解和对用户有帮助的信息;对运维和开发人员,则应该输出详细的异常链和异常栈到日志。
这个逻辑与在公司中处理问题的逻辑是类似的,每个级别都有自己应该解决的问题,自己能处理的自己处理,不能处理的就应该报告上级,把下级告诉他的和他自己知道的一并告诉上级,最终,公司老板必须要为所有问题负责。每个级别既不应该掩盖问题,也不应该逃避责任。
异常指南 应该在下列情况下使用异常:
- 尽可能使用 try-with-resource。
- 在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常。)
- 解决问题并且重新调用产生异常的方法。
- 进行少许修补,然后绕过异常发生的地方继续执行。
- 用别的数据进行计算,以代替方法预计会返回的值。
- 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
- 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
- 终止程序。
- 进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人。)
- 让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资。)
最后的最后 为初学者提供学习指南,为从业者提供参考价值。我坚信码农也具有产生洞见的能力。关注【码农洞见】,一起学习和交流吧!
推荐阅读
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 事件代理
- Java|Java OpenCV图像处理之SIFT角点检测详解
- java中如何实现重建二叉树
- 数组常用方法一
- 【Hadoop踩雷】Mac下安装Hadoop3以及Java版本问题
- Java|Java基础——数组
- RxJava|RxJava 在Android项目中的使用(一)
- java之static、static|java之static、static final、final的区别与应用
- Java基础-高级特性-枚举实现状态机