您的位置:首页 > 编程语言 > Java开发

编写java程序151条建议读书笔记(14)

2017-05-26 21:35 204 查看
建议110:提倡异常封装

Java语言的异常处理机制可以确保程序的健壮性,提高系统的可用率,但是Java API提供的异常都是比较低级的(这里的低级是指 " 低级别的 " 异常),只有开发人员才能看的懂,而对于终端用户来说与业务无关,是纯计算机语言的描述,那该怎么办?这就需要对异常进行封装。异常封装有三方面的优点:(1)提高系统的友好性

public static void doStuff() throws FileNotFoundException {
InputStream is = new FileInputStream("无效文件.txt");     /* 文件操作 */  }
此时doStuff的友好性极差,出现异常时(如果文件不存在),该方法直接把FileNotFoundException异常抛到上层应用中(或者是最终用户),而上层应用(或用户要么自己处理)要么接着抛,最终的结果就是用户不知道这是什么问题,只是知道系统告诉他"  ,解决办法就是封装异常,可以把异常的阅读者分为两类:开发人员和用户。开发人员查找问题,需要打印出堆栈信息,而用户则需要了解具体的业务原因,比如文件太大、不能同时编写文件等,代码如下:
public static void doStuff2() throws MyBussinessException{
try {
InputStream is = new FileInputStream("无效文件.txt");
} catch (FileNotFoundException e) {
//方便开发人员和维护人员而设置的异常信息
e.printStackTrace();
//抛出业务异常
throw new MyBussinessException();
}      /* 文件操作 */
}
(2)提高系统的可维护性,对异常进行分类处理,并进行封装输出不是捕捉直接输出。包装后,维护人员看到这样的异常就有了初步的判断,或者检查配置,或者初始化环境,不需要直接到代码层级去分析了。
public  void doStuff3(){
try{
//doSomething
}catch(FileNotFoundException e){
log.info("文件未找到,使用默认配置文件....");
e.printStackTrace();
}catch(SecurityException e1){
log.info(" 无权访问,可能原因是......");
e1.printStackTrace();
} }
(3)解决Java异常机制自身的缺陷。Java中的异常一次只能抛出一个,比如doStuff方法有两个逻辑代码片段,如果在第一个逻辑片段中抛出异常,则第二个逻辑片段就不再执行了,也就无法抛出第二个异常了,现在的问题是:如何才能一次抛出两个(或多个)异常呢?使用自行封装的异常可以解决该问题
class MyException extends Exception {
// 容纳所有的异常
private List<Throwable> causes = new ArrayList<Throwable>();
// 构造函数,传递一个异常列表
public MyException(List<? extends Throwable> _causes) {
causes.addAll(_causes);
}   // 读取所有的异常
public List<Throwable> getExceptions() {
return causes;
}
}
MyException异常只是一个异常容器,可以容纳多个异常,但它本身并不代表任何异常含义,它所解决的是一次抛出多个异常的问题,具体调用如下:
public void doStuff() throws MyException {
List<Throwable> list = new ArrayList<Throwable>();
// 第一个逻辑片段
try {
// Do Something
} catch (Exception e) {
list.add(e);
}
// 第二个逻辑片段
try {
// Do Something
} catch (Exception e) {
list.add(e);
}
// 检查是否有必要抛出异常
if (list.size() > 0) {
throw new MyException(list);
}   }


DoStuff方法的调用者就可以一次获得多个异常了,也能够为用户提供完整的例外情况说明。如Web界面注册时,展现层依次把User对象传递到逻辑层,Register方法需要对各个Field进行校验并注册,例如用户名不能重复,密码必须符合密码策略等,不要出现用户第一次提交时系统显示" 用户名重复 ",在用户修改用户名再次提交后,系统又提示" 密码长度小于6位 " 的情况,这种操作模式下的用户体验非常糟糕,最好的解决办法就是异常封装,建立异常容器,一次性地对User对象进行校验,然后返回所有的异常。

建议111:采用异常链传递异常

设计模式中有一个模式叫做责任链模式(Chain of Responsibility) ,它的目的是将多个对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止,异常的传递处理也应该采用责任链模式。异常需要封装,但仅仅封装还是不够的,还需要传递异常。一个系统友好性的标志是用户对该系统的" 粘性",粘性越高,系统越友好,粘性越低系统友好性越差。比如JavaEE项目一般都有三层结构:持久层,逻辑层,展现层,持久层负责与数据库交互,逻辑层负责业务逻辑的实现,展现层负责UI数据库的处理,有这样一个模块:用户第一次访问的时候,需要从持久层user.xml中读取信息,如果该文件不存在则提示用户创建之,如果我们直接把持久层的异常FileNotFoundException抛弃掉,逻辑层根本无法得知发生了何事,也就不能为展现层提供一个友好的处理结果了,最终倒霉的就是发展层:没有办法提供异常信息,只能告诉用户说“出错了,我也不知道出什么错了”毫无友好性可言。正确的做法是先封装,然后传递,过程如下:(1)把FIleNotFoundException封装为MyException。

(2)抛出到逻辑层,逻辑层根据异常代码(或者自定义的异常类型)确定后续处理逻辑,然后抛出到展现层。

(3)展现层自行决定要展现什么,如果是管理员则可以展现低层级的异常,如果是普通用户则展示封装后的异常。

使用异常链进行异常的传递,以IOException为例来看看是如何传递的,代码如下:

public class IOException extends Exception {
public IOException() {
super();
}
//定义异常原因
public IOException(String message) {
super(message);
}
//定义异常原因,并携带原始异常
public IOException(String message, Throwable cause) {
super(message, cause);
}
//保留原始异常信息
public IOException(Throwable cause) {
super(cause);
}
}
在IOException的构造函数中,上一个层级的异常可以通过异常链进行传递,链中传递异常的代码如下所示:

 try{           //doSomething      }catch(Exception e){         throw new IOException(e);      }

捕捉到Exception异常,然后把它转化为IOException异常并抛出(此种方式也叫作异常转译),调用者获得该异常后再调用getCause方法即可获得Exception的异常信息,如此即可方便地查找到产生异常的基本信息,便于解决问题。异常需要封装和传递,在进行系统开发时不要" 吞噬 " 异常,也不要赤裸裸的抛出异常,封装后再抛出,或者通过异常链传递,可以达到系统更健壮,更友好的目的。

建议112:受检异常尽可能转化为非受检异常

之所以尽可能因为" 把所有的受检异常(Checked Exception)"都转化为非受检异常(Unchecked Exception)" 这一想法是不现实的:受检异常是正常逻辑的一种补偿手段,特别是对可靠性要求比较高的系统来说,在某些条件下必须抛出受检异常以便由程序进行补偿处理,也就是说受检异常有合理存在的理由,那为什么要把受检异常转化为非受检异常呢?受检异常有不足:(1)受检异常使接口声明脆弱:OOP(Object Oriented Programming,面向对象程序设计)
要求我们尽量多地面向接口编程,可以提高代码的扩展性、稳定性等,但是涉及异常问题就不一样了,例如系统初期是这样设计一个接口的:interface User{

    //修改用户密码,抛出安全异常

    public void changePassword() throws MySecurityException;

}随着开发接口实现着增加如果发现changePassword方法可能还需要抛出RejectChangeException(拒绝修改异常,如自动执行正在处理的任务时不能修改其代码),那就需要修改User接口了:changePassword方法增加抛出RejectChangeException异常,这会导致所有的User调用者都要追加了对RejectChangeException异常问题的处理。

这里产生了两个问题:一、异常是主逻辑的补充逻辑,修改一个补充逻辑,就会导致主逻辑也被修改,也就是出现了实现类 " 逆影响 " 接口的情景,我们知道实现类是不稳定的,而接口是稳定的,一旦定义了异常,则增加了接口的不稳定性,这是面向对象设计的严重亵渎;二、实现的变更最终会影响到调用者,破坏了封装性,这也是迪米特法则所不能容忍的。(2)受检异常使代码的可读性降低,一个方法增加可受检异常,则必须有一个调用者对异常进行处理代码膨胀许多,可读性也降低了,特别是在多个异常需要捕捉的情况下,多个catch块多个异常处理,而且还可能在catch块中再次抛出异常,这大大降低了代码的可读性。(3)受检异常增加了开发工作量,异常需要封装和传递,只有封装才能让异常更容易理解,上层模块才能更好的处理,可这会导致低层级的异常没完没了的封装,无端加重了开发的工作量。比如FileNotFoundException进行封装,并抛出到上一个层级,于是增加了开发工作量。

受检的缺点避免或减少:很简单的一个规则将受检异常转化为非受检异常即可,但是我们也不能把所有的受检异常转化为非受检异常,原因是在编码期上层模块不知道下层模块会抛出何种非受检异常,只有通过规则或文档来描述,可以这样说:1)受检异常提出的是" 法律下的自由 ",必须遵守异常的约定才能自由编写代码。2)非受检异常则是“ 协约性质的自由 ”,你必须告诉我你要抛什么异常,否则不会处理。

以User接口为例,我们在声明接口时不再声明异常,而是在具体实现时根据不同的情况产生不同的非受检异常,这样持久层和逻辑层抛出的异常将会由展现自行决定如何展示,不再受异常的规则约束了,大大简化开发工作,提高了代码的可读性。

" 尽可能 " 是以什么作为判断依据呢?受检异常转换为非受检异常是需要根据项目的场景来决定的,例如同样是刷卡,员工拿着自己的工卡到考勤机上打考勤,此时如果附近有磁性物质干扰,则考勤机可以把这种受检异常转化为非受检异常,黄灯闪烁后不做任何记录登记,因为考勤失败这种情景不是" 致命 "的业务逻辑,出错了,重新刷一下即可。但是到银行网点取钱就不一样了,拿着银行卡到银行取钱,同样有磁性物质干扰,刷不出来,那这种异常就必须登记处理,否则会成为威胁银行卡安全的事件。汇总成一句话:当受检异常威胁到了系统的安全性,稳定性,可靠性、正确性时,则必须处理,不能转化为非受检异常,其它情况则可以转化为非受检异常。

注意:受检异常威胁到系统的安全性,稳定性、可靠性、正确性时,不能转换为非受检异常。

建议113:不要在finally块中处理返回值

public class test113{
public static void main(String[] args) {
try {
System.out.println(doStuff(-1));
System.out.println(doStuff(100));
} catch (Exception e) {
System.out.println("这里是永远不会到达的");
}
}
//该方法抛出受检异常
public static int doStuff(int _p) throws Exception {
try {
if (_p < 0) {
throw new DataFormatException(" 数据格式错误 ");
} else {
return _p;
}

} catch (Exception e) {
// 异常处理
throw e;
} finally {
return -1;
}
}
}
main方法中的doStuff方法的返回值是什么?doStuff方法永远都不会抛出异常吗?答案是:doStuff(-1)的值是-1,doStuff(100)的值也是-1,调用doStuff方法永远都不会抛出异常,有这么神奇?原因就是我们在finally代码块中加入了return语句,而这会导致出现以下两个问题:(1)覆盖了try代码块中的return返回值。当执行doStuff(-1)时,doStuff方法产生了DataFormatException异常,catch块在捕捉此异常后直接抛出,之后代码执行到finally代码块,就会重置返回值,结果就是-1了。也就是出现先返回,再重置返回的情况,有人可能会思考,是不是可以定义变量,在finally中修改后return呢
public static int doStuff() {
int a = 1;
try {
return a;
} catch (Exception e) {

} finally {
// 重新修改一下返回值
a = -1;
}
return 0;
}
该方法的返回值永远是1,不会是-1或0(为什么不会执行到"
return 0 " 呢?原因是finally执行完毕后该方法已经有返回值了,后续代码就不会再执行了),这都是源于异常代码块的处理方式,在代码中try代码块就标志着运行时会有一个Throwale线程监视着该方法的运行,若出现异常,则交由异常逻辑处理。方法是在栈内存中运行的,并且会按照“ 先进后出 ”的原则执行,main方法调用了doStuff方法,则main方法在下层,doStuff方法在上层,当doStuff方法执行完" return a " 时,此方法的返回值已经确定int类型1(a变量的值,注意基本类型都是拷贝值,而不是引用),此时finally代码块再修改a的值已经与doStuff返回者没有任何关系了,因此该方法永远都会返回1。(2)屏蔽异常为什么明明把异常throw出去了,但main方法却捕捉不到呢?这是因为异常线程在监视到有异常发生时,就会登记当前的异常类型为DataFormatException,但是当执行器执行finally代码块时,则会重新为doStuff方法赋值,也就是告诉调用者"
该方法执行正确,没有产生异常,返回值为1 ",于是乎,异常神奇的消失了,其简化代码如下所示:
public static void doSomeThing(){
try{
//正常抛出异常
throw new RuntimeException();
}finally{
//告诉JVM:该方法正常返回
return;
}
}
public static void main(String[] args) {
try {
doSomeThing();
} catch (RuntimeException e) {
System.out.println("这里是永远不会到达的");
}
}
上面finally代码块中的return已经告诉JVM:doSomething方法正常执行结束,没有异常,所以main方法就不可能获得任何异常信息了。这样的代码会使可读性大大降低,读者很难理解作者的意图,增加了修改的难度。在finally中处理return返回值,代码看上去很完美,都符合逻辑,但是执行起来就会产生逻辑错误,最重要的一点是finally是用来做异常的收尾处理的,一旦加上了return语句就会让程序的复杂度徒然上升,而且会产生一些隐蔽性非常高的错误。与return语句相似,System.exit(0)或RunTime.getRunTime().exit(0)出现在异常代码块中也会产生非常多的错误假象,增加代码的复杂性。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: