每个软件都可能遇到异常,所以从设计阶段就要考虑异常处理的问题,纳为业务流程的一部分。
异常是需要妥善处理的,但是处理的前提是发现异常,而发现异常的前提的对异常有清楚的认识,我们要先认识到程序中都有什么样的异常(定义异常),然后在程序结构中检测和抛出异常(捕获异常),最后用恰当的业务流程去分别处理(处理异常)
所以,发现和处理异常的过程可以简单归纳为定义->发现->处理的过程,也就是定义异常-->捕捉异常->处理异常。
一、定义异常
要定义异常,就要看看程序都可能有哪些异常,如何去为这些异常分类。
异常当前不只一种,只有一种异常是不能满足业务流程需要的。
例如在线登录失败这种异常,只抛出一个“登录失败”是无法准确处理的,失败是因为网络连接失败?还是用户名密码错误?这两种错误类型,要分别去走不同的业务流程,一个要检查网络,一个要检查用户名密码,只有定义成不同的异常,才能根据实际情况去引导用户分别操作。
1.Throwable
在Java中,异常的基类是Throwable,它只引用了Serializable这个可序列化接口,基于Throwable,还有Exception、RuntimeException和Error这三个类,这几个类之间的关系如下:
我们看到从大的分支上来看,分为Error和Exception两大类,我们先看看这两类有什么区别。
2.Error和Exception
我们看一组对比:
Error Exception
check uncheck(运行时)
主要在编译时提示 运行时提示
不建议捕获 建议捕获
Error错误,主要在开发时起检查作用(check),编译器在编译时,根据已知可能存在的异常,提示开发者,例如,把一个String值直接赋给int对象,编译器就会提示Error了。
Exception异常,是编译器检查不出来的(uncheck),是在软件运行时才会发现的异常,也就是运行时异常(RuntimeException),例如,做一个3/0的运算,这是个算术错误,但是编译器在编译阶段看不出错误,只有在运行阶段才能发现异常。
Error和Exception这两种异常,其实都是可以用try catch捕获的,写catch(Error e)/catch(Exception e)就可以捕获,但是一般不建议捕获Error错误,这是为什么呢?
因为分工不同,先看Exception,这是运行阶段可能出现的异常,一般在某些特殊逻辑分支或参数下,才可能出现,开发者也应该对这种逻辑进行处理,所以建议捕获异常进行处理;
但是Error是不建议捕获的,前面说了,Error不是运行阶段可能出现的错误,他本身就代表程序逻辑有硬伤,或者运行环境不正确,在这种情况下,即便是捕获了异常,程序也没有办法继续执行,所以建议不捕获。
我们找两个例子,ClassNotFoundException和NoClassDefFoundError,这两个看起来都是找不到类导致的异常,但是一个是Exceptioin异常,一个是Error错误,我们对比一下,就能理解Error和Exception的区别了。
ClassNotFoundException,是个Exception异常,一般在反射时遇到,是动态加载时报错的,动态加载是开发者故意设计的业务逻辑,本身就有失败的可能,所有建议捕获异常。
NoClassDefFoundError,是个Error错误,这个错误发生时,在编译时都没有问题,但是运行时,JVM或者ClassLoader去加载某个类,发现这个类找不到了,就会报这个错误。这一般是运行环境的问题,例如缺少库文件什么的,这个错误与业务逻辑无关,是必须解决掉的错误,否则软件无法继续运行,所以不建议捕获异常。
3.异常子类
异常子类一般都是Exception的子类,Java提供了丰富的异常类,开发者也可以自定义异常类,异常类的作用就是描述什么出了错,和为什么出错,例如:IllegalArgumentException("filepath is null"),就抛出了一个参数错误的异常,而且说明了出错的原因是"filepath is null"。
在实际开发中,我们需要根据自己的业务场景,去选用或自定义异常类型,根据实际情况去抛出异常。
二、捕获异常
前面一直在说定义异常的问题,接下来我们要捕获到这些异常。在出现异常时,我们需要立即知道哪里出了异常,为什么会出异常,具体来说,就是定位到异常代码,并为异常分类,去定义这个异常的消息内容。
1.定位
Java可以比较容易地定位到异常代码,异常堆栈提供了导致异常的方法调用链,能精确定位到类名,方法,代码行。
2.分类
分类就需要一定的设计经验了,一方面要提前做好异常定义,另一方面要在代码中准确抛出异常。
例如:在读取文件时,把文件地址作为参数,如果输入一个空的文件地址,Java默认只会报一个NullPointerException空指针异常,也没有异常消息,这种宽泛的分类下,我们只知道这里出现了异常,却不知道为什么会异常,后面的异常处理就没办法做。
这种情况下,我们为了能精准地处理空文件地址的问题,就需要自己去判断文件名是否为空,如果为空,则抛出一个IllegalArgumentException,并自定义一个"filepath is null"的异常消息传出来,这样后面处理时,就可以在这种情况下提示用户“请输入文件地址”,而不是简单粗暴地报一句“出错啦”了事。
3.捕获
异常捕获是需要融入到代码逻辑中的,首先要预见可能的异常,然后定义异常及其异常消息,最后才能在代码段中捕获到相应的异常。
4.抛出
有时候,我们需要主动抛出异常,比如我们不希望用户调用某些函数,或在某些逻辑分支中提前判断并抛出异常,我们可以主动在代码里throw一些异常,比如throw methodErr("visiting this method is not allowed");
三、处理异常
我们捕获异常,最终都是为了走恰当的业务流程,去处理异常。
Java有两种处理异常的方式,一是自己捕获,用try catch去捕捉异常,在catch代码里处理;另一种是让调用者捕获,用throw抛出异常,通知调用者去处理。
这两种处理方式不是随便选择的,要看具体的业务,异常应该马上捕获,但不一定要马上处理。
例如:服务器查询数据库时,业务层查询数据,会调用数据访问层,这时如果数据库连接失败就会出现异常,这个异常如果在数据层自己捕获到,就仅限于数据层知道了,业务层根本不知道出了异常,会误以为数据库中没有这种数据。这时正确的做法应该是抛出异常,用throw向业务层抛出异常,让业务层自己去处理,决定是重试连接,还是告知用户。
异常的提示,是与业务相关的,如果需要用户走不同的逻辑分支,就需要设计相关的界面和提示;如果需要反馈给开发者,就需要记录日志并上传到服务器。