Java的异常

Java的异常

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

异常应该只用于异常的情况下:它们永远不应该用于正常的控制流。

下面的代码是**错误​**用法:

    /**
     * 异常的错误用法
     * @param range
     */
    private static void loopByExceptionTest(String[] range){
        try{
            int i=0;
            while (true){
                System.out.println(range[i++]);
            }
        }catch (ArrayIndexOutOfBoundsException e){

        }
    }

基于异常的循环模式不仅模糊了代码的意图,降低了它的性能,而且它还不能保证正确工作!

一般地,应该优先使用标准的、容易理解的模式,而不是那些声称可以提供更好性能的、弄巧成拙的方法。

设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常。

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

Java程序设计语言提供了三种可抛结构(throwable):受检异常(checked exception)、运行时异常(run-time exception)、错误(error)。

2.1 使用受检异常还是未受检异常

使用受检异常还是未受检异常时,主要原则是:如果期望调用者能够适当地恢复,对于这种情况就应该使用受检异常。

通过抛出受检的异常,强迫调用者在一个catch子句中处理该异常,或者将它传播出去。

因此,方法中声明要抛出的每个受检异常,都是对API用户对一种潜在指示:与异常相关联的条件时调用这个方法的一种可能的结果。

2.2 有两种未受检的可抛出结构:运行时异常和错误

在行为上两者是等同的:它们都是不需要也不应该被捕获的可抛出结构。

如果程序抛出未受检的异常或者错误,往往就属于不可恢复的情形,继续执行下去有害无益。

如果程序没有捕捉到这样的可抛结构,将会导致当前线程中断,并出现适当的错误消息。

2.3 运行时异常

用运行异常来表示编程错误。大多数的运行时异常都表示前提违例

所谓前提违例是指API的客户没有遵守API规范建立的约定。例如,违反了数组下标值的约定,就会出现ArrayIndexOutOfBoundsException异常。

对于要处理可恢复的条件,还是处理编程错误,情况并非总是那么黑白分明。

如果你相信一种情况可能允许恢复,就使用受检异常;如果不是,则使用运行时异常。如果不清楚是否有可能恢复,最好使用未受检的异常。

2.4 错误

**错误**往往被JVM保留下来使用,以表明资源不足、约束失败,或者其他使程序无法继续执行的条件。

由于这已经是个几乎被普遍接受的惯例,因此最好不要再实现任何新的Error子类。

你实现的所有未受检的抛出结构都应该是RuntimeException的子类(直接或间接)。不仅不应该定义Error子类,甚至也不应该抛出AssertionError异常。

2.5 为异常提供方法

异常也是个完全意义上的对象,可以在它上面定义任意的方法。

这些方法的主要用途是为了捕获异常的代码而提供额外的信息,特别是关于引发这个异常条件的信息。

对于受检异常往往指明了可恢复的条件。所以,对于这样的异常,提供一些辅助方法尤其重要,通过这些方法,调用者可以获得一些有助于恢复的信息。

三、避免不必要地使用受检异常

受检异常,它们强迫程序员处理异常的条件,大大增强了可靠性。

如果方法抛出受检异常,调用该方法的代码就必须在一个或多个catch块中处理这些异常,或者它必须声明抛出受检异常,调用该方法的代码就必须在一个或者多个catch快中处理这些异常,或者它必须声明抛出这些异常,并让它们传播出去。

无论哪种处理受检异常的方法,都给程序员增添了不可忽视的负担。这种负担在Java8中更重了,因为抛出受检异常的方法不能直接在Stream中使用

石蕊测试:是指简单而具有决定性的测试。

3.1 程序员如何处理受检异常

方案一:

        try {
        ...
        }catch (TheCheckedException e){
            throw new AssertionError();// Can't happen!
        }

方案二:

        try {
            
        }catch (TheCheckedException e){
            e.printStackTrace();  // Oh well, we lose
            System.exit(1);
        }

如果使用API的程序员无法做得比这更好,那么未受检的异常可能更为合适。

3.2 消除受检异常的方法

(1)返回所要的结果类型的一个optional

这是最简单的一种方法。这种方法不抛出受检异常,而只是返回一个零长度的optional

这种方法的缺点是,方法无法返回任何额外的信息,来详细说明它无法执行你想要的计算。

相反,异常则具有描述性的类型,并且能够导出方法,以提供额外的信息。

(2)把抛出异常的方法分成两个方法,其中第一个方法返回一个boolean值,表明是否应改抛出异常

把下面的调用序列:

        try {
            obj.action(args);
        }catch (TheCheckedException e){
            ... // Handle exceptional condition
        }

重构成:

        if (obj.actionPermitted(args)){
            obj.action(args);
        }else {
            ... // handle exception condition
        }

四、优先使用标准的异常

代码重用是值得提倡的,这是一条通用的规则,异常也不例外。

专家追求并且通常也能勾实现高度的代码重用。

Java平台类库提供了一组基本的未受检异常,它们满足了绝大多数API的异常抛出需求。

重用标准异常的好处:

  • 使API更易于学习和使用;
  • 对于使用这些API的程序而言,它们的可读性更好;
  • 异常越少,意味着内存占用就越小,装载这些类的时间开销就越少;

4.1 经常被重用的异常

(1)IllegalArgumentException

当调用传递的参数值不合适的时候,往往就会抛出这个异常。

(2)IllegalStateException

如果因为接收对象的状态而调用非法,通常抛出这个异常。

例如,如果在某个对象被正确地初始化之前,调用者就企图使用这个对象,就会抛出这个异常。

也可以这么说,所有错误的方法调用都可以被归结为非法参数或者非法状态。

(3)NullPointerException

如果调用者在某个不允许null值的参数中传递了null。

(4)IndexOutOfBoundsException

如果调用者在表示序列下表的参数中传递了越界的值。

(5)ConcurrentModificationException

如果监测到一个专门设计用于单线程的对象,或者与外部同步机制配合使用的对象正在(或已经)被并发修改,就应该抛出这个异常。

这个异常顶多就是一个提示,因为不可能可靠地侦测到并发到修改。

(6)UnsupportedOperationException

如果对象不支持请求的操作,就会抛出这个异常。

很少使用,应为绝大多数对象都会支持他们实现的所有方法。

如果类没有实现由它们实现的接口所定义的一个或者多个可选操作,它就可以使用这个异常。

例如,对于只支持追加操作的List实现,如果有人试图从列表中删除元素,它就会抛出这个异常。

**不要直接重用ExceptionRuntimeExceptionThrowableError。**对待这些类要像对待抽象类一样。

你无法可靠地测试这些异常,因为它们是一个方法可能抛出的其他异常的超类。

4.2 其他可被重用的异常

在条件许可的情况下,其他的异常也可以被重用。

ArithmeticExceptionNumberFormatException

这种重用必须建立在语义的基础上,而不是建立在名称的基础之上。

4.3 异常是可以序列化的

异常是可序列化的,这正是“如果没有非常正当的理由,千万不要自己编写异常类的原因。

五、抛出与抽象对应的异常

5.1 异常转译

更高层的实现应该捕捉低层的异常,同时抛出可以按照高层抽象进行解释的异常。

        try {

        }catch (LowerLevelException e){
            throw new HigherLevelException(...);
        }

例如:AbstractSequentialList

    public E get(int index) {
        try {
            return listIterator(index).next();
        } catch (NoSuchElementException exc) {
            throw new IndexOutOfBoundsException("Index: "+index);
        }
    }

5.2 异常链

异常链是一种特殊的异常转译形式。

如果底层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很适合。

底层的异常被传到高层的异常,高层的异常提供访问方法(Throwable的getCause方法)来获得底层的异常;

        try{
            ....
        }catch (LowerLevelException cause){
            throw new HigherLevelException(cause);
        }

高层异常的构造器将原因传到支持链的超级构造器,因此它最终将被传到Throwable的其中一个运行异常链的构造器,例如Throwable(Throwable):

    // Exception with chaining-aware constructor
    class HigherLevelException extends Exception{
        HigherLevelException(Throwable cause){
            super(cause);
        }
    }

对于没有支持链的异常,可以利用ThrowableinitCause方法设置原因。

尽管异常转译与不加选择地从低层传递异常的做法相比所有改进,但是也不能滥用它。

最好的最发是,在调用底层方法之前确保它们会成功执行,从而避免它们抛出异常。

六、每个方法抛出的所有异常都要建立文档

仔细为每个方法抛出的异常建立文档是特别重要的。

6.1 始终要单独地声明受检异常

利用Javadoc的@throws标签,准确记录下抛出每个异常的条件。

永远不要声明一个公有方法直接throws Exception,或者更糟糕的是声明它直接throws Throwable

这样的声明不仅没有为程序员提供关于"这个方法能够抛出哪些异常"的任何知道信息,而且大大地妨碍了该方法的使用。

有一个例外,就是main方法,它可以安全地声明抛出Exception,因为它只通过虚拟机调用。

6.2 如同受检异常一样,仔细地为未受检异常建立文档是非常明智的

未受检异常通常代表编程上的错误。

让程序员了解所有这些错误都有助于帮助他们避免同样的错误。

对于接口的方法,在文档中记录下它可能抛出的未受检异常显得尤为重要。

使用Javadoc的@throws标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中。

6.3 在类的文档注释上对(特殊)异常建立文档

如果一个类的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,而不是单独建立文档。

七、在细节信息中包含失败——捕获信息

异常类型的toString方法应该尽可能多地返回有关失败原因的信息。

  • 为了捕获失败,异常的细节信息应该包含“对该异常有贡献”的所有参数和域的值

    例如数组的角标

  • 千万不要在细节消息中包含密码、密钥以及类似的信息

  • 一种办法时在异常的构造器而不是字符串细节信息中引入这些信息

        // IndexOutOfBoundsException
        public IndexOutOfBoundsException(int index) {
            super("Index out of range: " + index);
        }
    

八、努力使失败保持原子性

一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性

(1)设计一个不可能的对象

(2)在执行操作之前检查参数的有效性;

    // Stack
    public Object pop(){
        if (size==0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

(3)调整计算机处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生

(4)在对象的一份拷贝上执行操作

(5)编写一段恢复代码,由它来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前到状态

这种方法主要用于永久性的(基于磁盘的)数据结构。

(6)对于某些操作,保持原子性会显著增加开销或复杂度

当方法抛出AssertionError时,不需要努力保持失败原子性。

九、不要忽略异常

空的catch块会使异常达不到应有的目的。

忽略异常就如同忽略火警信号一样——如果八火警信号器关掉了,当真正由火灾发生时,就没有人能看到火警信号了。

如果选择忽略异常,catch块中应该包含一条注释,说明为什么可以这么做,并且变量应该命名为ignored

本条目的建议同样适用于受检异常和未受检异常。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值