如何优雅地处理异常
作者:互联网
目录
问题
1、什么时候才需要抛异常?
如果你觉得某些“问题”解决不了了,那么你就可以抛出异常了。
问题:对于异常情形,已经无法继续下去了,因为在当前环境下无法获得必要的信息来解决问题,你所能做的就是从当前环境中跳出,并把问题提交给上一级环境。
2、抛出什么类型的异常?
非受检异常和受检异常之间的区别就是:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常。
一般来讲,如果没有特殊的要求,建议使用RuntimeException异常。
3、异常如何处理,处理异常还是抛出异常?
通常应该捕获那些知道如何处理的异常,而将那些不知道如何处理的异常进行传递。
Java异常层次结构
所有的异常都是由Throwable继承而来,又分为Error和Exception。
Error描述了Java运行时系统的内部错误和资源耗尽错误。例如,JVM 内存溢出。一般地,程序不会从错误中恢复。
要重点关注Exception异常,由编程错误导致的异常属于RuntimeExcepiton;如果程序本身没问题,但由于像I/O错误这类问题导致的异常属于其他异常。
非受检异常和受检异常
非受检(unchecked)异常:Java语言规范将继承自Error或RuntimeException的所有异常称为非受检(unchecked)异常,所有其他异常称为受检(checked)异常。常见的非受检异常有NullPointerException、IndexOutOfBoundsException、ClassCastException等。常见的Error有OutOfMemoryError、StackOverflowError、NoSuchMethodError等。
受检异常:Java语言规定必须对受检异常进行处理,编译器将检查是否为所有的受检异常提供了异常处理器或者是否在函数声明中声明抛出,否则无法编译通过。常见的受检异常有FileNotFoundException、SocketTimeoutException等。
异常处理
1、使用异常替代错误返回码
public void test() {
if (find() == OK) {
if (update() == OK) {
log.info("update");
} else {
log.info("update failed");
}
} else {
log.info("find failed");
}
}
返回错误码的问题:
- 搞乱了调用者代码,调用者必须在调用之后即刻检查错误。不幸的是,这个步骤很容易被遗忘。
- 导致了更深层次的嵌套结构。
【推荐】使用异常替代返回错误码,错误处理代码就能从主路径代码中抽离出来,得到简化。
public void update() {
try {
find();
updateExist();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
Try/Catch代码块搞乱了代码结构,把异常处理和流程处理混为一谈。最好把try和catch代码的主体部分抽离出来,另外形成函数。
【推荐】抽离Try/Catch代码块,有了这样美妙的区隔,代码就更易于理解和修改了。
public void update() {
try {
findAndUpdate();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
public void findAndUpdate() throws Exception {
find();
updateExist();
}
函数应该只做一件事,错误处理就是一件事。如果关键字try在某个函数中存在,它就该是函数中的第一个单词,而且在catch/finally代码块后也不该有其他内容。
2、先写Try-Catch-Finally语句
异常的妙处之一是,他们在程序中定义了一个范围。执行try-catch-finally语句中try部分的代码时,你是在表明可以可随时取消执行,并在catch语句中接续。
在编写可能抛出异常的代码时,最好先写try-catch-finally语句。这能帮助你定义代码的用户应该期待什么,无论try代码中执行的代码出什么错都一样。
3、使用非受检异常
受检异常违反了开闭原则,如果你在方法中抛出可控异常,而catch语句在三个层级之上,你就得在catch语句和抛出异常处之间的每个方法签名中声明该异常。这意味着对软件中较低级别的修改,都将波及到较高层级的签名。
4、给出异常发生的环境说明
抛出的异常都应当提供足够的环境说明,以便判断错误的来源的出处。在Java中,你可以从任意异常中得到堆栈踪迹,堆栈踪迹无法告诉你该操作失败的初衷。
应创建信息充分的错误消息,并和异常一起传递出去。
5、依调用者定义异常类
异常分类的方式:
- 来源:组件、其他地方。
- 类型:设备、网络、编程错误。
当我们在程序中定义异常类时,最重要的考虑应该是它们如何被捕获。
先看一个不太好的异常分类例子,ACMEPort为第三方代码库:
ACMEPort port = new ACMEPort(12);
try {
port.open();
} catch (DeviceResponseException e) {
reportPortError(e);
logger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {
reportPortError(e);
logger.log("Unlock exception", e);
} catch (GMXError e) {
reportPortError(e);
logger.log("Device response exception");
} finally {
...
}
语句中包含了一大堆重复的代码,这并不出奇。在大多数异常处理中,不管真实原因如何,我们总是做相对标准的处理。记录错误,确保正常工作。
可以通过打包调用的API,确保它返回通用异常类型,从而简化代码。
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
...
}
LocalPort类是个简单的打包类,捕获并翻译由ACMEPort抛出的异常。
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber) {
innerPort = new ACMEPort(portNumber);
}
public void open() {
try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e);
} catch (ATM1212UnlockedException e) {
throw new PortDeviceFailure(e);
} catch (GMXError e) {
throw new PortDeviceFailure(e);
}
}
...
}
类似于为ACMEPort定义的打包类非常有用,将第三方代码库打包是个良好的实践手段。
当你打包一个第三方API,就降低了对它的依赖,未来可以不太痛苦地改用其他代码库。
打包的好处还在于不必绑死在某个特定的API设计上,可以定义自己感觉舒服的API。
6、定义常规流程
业务逻辑和错误处理代码之间有良好的间隔。
打包外部API以抛出自己的异常,在代码中定义了一个处理器来应付任何失败了的运算。
7、别返回NULL值
返回null值,基本上是在给自己增加工作量,也是在给调用者添乱。只要有一处没有检查null值,应用程序就会失控。
如果你打算在方法中返回null值,不如抛出异常,或是返回特例对象。如果你在调用第三方API中可能返回null值的方法,可以考虑用新方法打包这个方法,在新方法中抛出异常或返回特例对象。
public List<String> findListById(Integer id){
if(Objects.isNull(id)){
return Collections.emptyList();
}
}
8、别传递NULL值
在大多数编程语言中,没有良好的方法能对付由调用者意外传入的null值,恰当的做法就是禁止传入null值。
标签:log,受检,处理,代码,优雅,try,catch,异常 来源: https://blog.csdn.net/kl_Dreaming/article/details/118997264