EffectiveJava-8-异常

作者: 今阳说 | 来源:发表于2021-01-19 18:13 被阅读0次

    只针对异常的情况才使用异常

    上面代码有什么问题呢?

    - 试图通过抛出异常并忽略的方式终止无限循环;意图避免for循环的越界检查;

    - 然而:

    1. 异常机制的设计初衷适用于不正常的情形,so,很少有JVM对其进行优化;

    2. 把代码放在try-cache中阻止了现代JVM实现本来可能要执行的某些特定优化;

    3. for遍历并不会导致冗余的检查,有些现代的JVM会将它们优化掉;

    - 上面的代码模糊了代码意图,降低了性能,还不能保证正常工作, 例如循环体中的计算过程调用了一个方法,这个方法执行了对某个不想关的数组的越界访问, 出发异常,而该try语句块会忽略掉这个bug,增加了调试过程的复杂性;

    对api设计的启发:设计良好的api不应该强迫它的客户端为了正常的控制流而使用异常; 例如Iterator有个hasNext方法可以判断是否终止循环,如果没有提供,可能用户就会不得不使用上面的代码实现了;

    总之,异常就是为了异常情况下使用而设计,不要用于普通的控制流,也不要编写迫使别人这样做的API;

    对可恢复的情况使用受检异常,对编程错误使用运行时异常

    java的三种可抛出结构(throwable):

    - 受检异常(checked exception)

    - 运行时异常(run-time exception)

    - 错误(error)

    如果期望调用者能够适当的恢复,则通过抛出受检异常,强迫调用者在一个catch子句中处理改异常或者将其传播出去;

    api的设计者让api用户面对受检的异常,以此强制用户从这个异常条件中回复; 用户可以忽略这样的强制要求,只需要捕获并忽略即可,但这往往不是一个好办法; 如File,io流操作是常见的IOException,FileNotFoundException;

    另外两种则是不需要,也不应该被捕获的;

    用运行时异常表明编程错误;大多数表示前提违例( precondition violation);前提违例是指api用户没有遵守api规范建立的约定,如ArrayIndexOutOfBoundsException,NullPointerException;

    错误被jvm保留用于表示资源不足,约束失败,或其他使程序无法继续执行的条件;因此最好不要实现任何新的Error子类;

    api设计者往往会忘记,异常也是个完全意义上的对象,可以在它上面定义任意方法, 这些方法主要用于为捕获异常的代码提供额外的信息,受检的异常往往指明了可恢复的条件, 所以对于这样的异常,提供一些辅助方法尤为重要,可以帮助调用者获得一些有助于恢复的信息;

    避免不必要的使用受检异常

    受检异常强迫程序员处理,大大增强了可靠性;

    过分的使用受检的异常会使api使用起来非常不方便(要做try-catch处理或抛出);

    所以设计api时要谨慎使用受检异常,如果使用api的程序员无法做的比下面的更好,那么使用未受检的异常更为合适;

    其实仔细回想一下,之前没少写上面这种代码,应该问问自己,是否有别的途径来避免使用受检异常;

    一种解决方法是:把抛出异常的方法分成两个方法,其中一个返回boolean,即使用if判断来处理是否抛出异常的两种情况,就如之前提过的Iterator有个hasNext方法;

    (但是这样可能会失去强制约束,api使用者不一定会调用如Iterator.hasNext()这种方法,这就需要完善的api说明文档来规范使用者的调用了)

    优先使用标准的异常

    专家与菜鸟的一个主要区别: 高度的代码重用,这是一条通用规则,异常也不例外, 本条目将讨论常见的可重用异常(未受检);

    重用现有异常的好处:

    1. 使api更加易于学习和使用(通用,习惯用法);

    2. 对用到这些api的程序而言,可读性会更好(不会出现很多不熟悉的异常);

    3. 异常类越少,内存印记(footprint)就越小,装载这些类的时间开销就越少;

    如果希望增加更多的失败-捕获信息,可以把现有的异常进行子类化;

    常用异常:

    - IllegalArgumentException: 非法参数(调用者传参不合适时);

    - IllegalStateException:非法状态(被调用的程序中某个对象的状态不满足程序运行需求);

    - NullPointerException: 这个就很常用了, 某个不允许为空的对象或参数为空时;

    - IndexOutOfBoundsException: 这个也很熟悉了,操作数组时经常会遇到他的子类;

    - ConcurrentModificationException: 如果一个对象被设计为专用于单线程或与外部同步机制配合使用,一旦发现它正在或已经被并发的修改,就应该抛出这个异常;

    - UnsupportedOperationException: 对象不支持用户请求的方法;

    抛出与抽象相对应的异常

    如果方法抛出的异常与它所执行的任务没有明显的联系,将会使人不知所措;当放过传递由低层抽象抛出的异常时,往往会发生这种情况,这也让实现细节污染了更高层的api;

    异常转译:更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常;

    异常链:如果低层异常对于调试导致高层异常的问题非常有帮助,可以将低层异常传到高层异常,高层异常提供访问方法(Throwable.getCause())来获取低层异常:

    处理来自低层的异常最好的做法是:在调用低层方法之前确保他们会执行成功,从而避免他们抛出异常,如检查参数的有效性;

    如果无法避免,次选方案是,让高层来悄悄绕开这些异常,从而将高层方法的调用者与低层问题隔离,可以使用适当的记录机制将异常记录下来,有助于管理员调查问题;

    每个方法抛出的异常都要有文档

    始终要单独的声明受检的异常,并利用javadoc的@throws标记,准确的记录下抛出每个异常的条件;

    一个方法需要抛出多个异常类时,不要用这些异常的超类或Exception,Throwable,代替,这样不仅没 有提供"这个方法能够抛出哪些异常"的指导信息,而且大大妨碍了该方法的使用,因为它实际上掩盖了该方法在同样的执行环境下可能抛出的任何其他异常;

    未受检异常最好也使用javadoc的@throws标签记录,但不要使用throws关键字将未受检的异常包含在方法的声明中;

    如果一个类中的许多方法处于同样的原因而抛出同一个异常,该类的文档注释中对这个异常建立文档也是可以的;如"All methods in this class throw a NullPointerException if a null object reference is passed in any parameter";

    在细节消息中包含能捕获失败的信息

    为了捕获失败,异常的细节信息应该包含所有"对该异常有贡献"的参数和域的值; 当程序由于未被捕获的异常而失败的时候,系统会自动的打印出该异常的堆栈轨迹, 在堆栈轨迹中包含该异常的字符串表示法,即toString方法的调用结果;

    因为异常可能不易复现,所以toString(即细节信息)中应该尽可能多的返回有关失败的原因信息; (为了捕获失败,异常的细节信息应该包含所有"对该异常有贡献"的参数和域的值) (堆栈轨迹的用途是与源文件结合起来进行分析,它通常包含抛出该异常的确切文件和行数,以及堆栈中所有其他方法调用所在的文件和行数)

    异常的细节消息不应该与“用户层次的错误消息”混为一谈,后者对于最终用户必须是可理解的,前者则用来让程序员分析失败原因,信息内容比可理解性要重要的多; 为了确保足够的信息,一种拜访是在异常的构造器中引入这些信息 如:

    努力使失败保持原子性

    定义:失败的方法调用应该使对象保持在被调用之前的状态,具有这种属性的方法被称为具有失败原子性;

    最简单的办法:设计一个不可变的对象;

    对于可变对象的常见办法:

    1. 在执行操作之前检查参数的有效性,这可以使得对象的状态被修改之前, 先抛出适当的异常,如我之前讲栈的文章中ArrayStack的pop方法,如果取消对初始大小的检查, 当这个方法企图从一个空栈中弹出元素时,它仍然会抛出异常, 而且,这将会导致将来对该对象的任何方法调用都会失败;

    2. 调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生;例如TreeMap的put方法, 向其中添加元素,该元素的类型就必须是可以利用TreeMap的排序准则与其他元素进行比较的, 如果企图添加类型不正确的元素,在tree以任何方式被修改之前,自然会导致ClassCastException异常;

    3. 编写一段恢复代码,由它来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上,这种办法主要用于永久性的(基于磁盘的disk-based)数据结构;

    4. 在对象的一份临时拷贝上执行操作,当操作完成后,再用临时拷贝中的结果替代对象的内容,计算过程会更加迅速;

    不要忽略异常

    当API的设计者生命一个方法将抛出某个异常的时候,他们等于正在试图说明某些事情,所以请不要忽略它;

    要忽略一个异常很容易,只需要如下这样:

    至少catch块也应该包含一条说明,解释为什么可以忽略这个异常;

    用空的catch块忽略它,将会导致程序在遇到错误的情况下悄然的执行下去,然后在将来的某个点上, 当程序不能再容忍与错误源明显相关的问题时,他就会失败;

    只要将异常传播给外界,至少可以使程序迅速的失败,从而保留了有助于调试该失败条件的信息;

    我是今阳,如果想要进阶和了解更多的干货,欢迎关注公众号”今阳说“接收我的最新文章

    相关文章

      网友评论

        本文标题:EffectiveJava-8-异常

        本文链接:https://www.haomeiwen.com/subject/zkdczktx.html