软件构造复习(五)
正确性与健壮性
健壮性
健壮性:系统在不正常输入或不正常外部环境下仍能够表现正常的程度
面向健壮性的编程:
处理未期望的行为和错误终止
即使终止执行,也要准确且无歧义地向用户展示全面的错误信息
错误信息有助于进行debug
健壮性准则:总是假定用户恶意、假定自己的程序可能失败;认为用户可能输入任何东西,返回给用户的错误提示信息要准确、详细、无歧义
正确性
正确性:程序按照Spec执行的能力,是最重要的指标
正确性:永不给用户错误的结果
健壮性:尽可能保持软件运行而不是总退出
正确性倾向于直接保持,健壮性则倾向于容错
正确性与健壮性的对比

健壮性:避免给用户太大压力,帮助用户承担一些麻烦

健壮性:让客户变得更容易,出错也可以容忍,程序内部已有容错机制
正确性:让开发者更容易,用户输入错误(不满足precondition),直接结束
内部倾向于正确,外部倾向于健壮
对外的接口,倾向于健壮;对内的实现,倾向于正确
在内外部之间做好隔离,防止错误扩散
可靠性=正确性+健壮性
提高正确性与健壮性
- 使用断言、防御式编程、正确性验证等方式
- 观察错误的症状(存储器信息、堆栈追踪、日志、测试)
- 识别潜在的错误
- 修复错误
衡量正确性与健壮性
外部观察角度
平均失效间隔时间(MTBF):相邻两次故障之间的平均工作时间

内部观察角度(间接)
残余缺陷率:每千行代码中遗留的bug数量

错误与异常
错误和异常都继承自Throwable,其有两个子类:Error和Exception

错误:程序员通常无能为力,一旦发生,想办法让程序优雅地结束
异常:自己的程序导致的问题,可以捕获、可以处理
错误类型
用户输入错误
设备错误
物理限制(内存空间不足等)
大多数时候,程序员不需要实例化Error
异常处理
异常
程序执行中的非正常事件,程序无法再按预想的流程执行
将错误信息传递给上层调用者,并报告“案发现场”的信息。是return之外的第二种退出途径
上层调用者如果没有异常处理程序将其捕获,则继续将异常上抛
若找不到异常处理程序,整个系统完全退出(上抛到最上层调用者后仍没有异常处理程序)
异常的分类
分为运行时异常(RuntimeException类)和其他异常
运行时异常是由程序员在代码里处理不当造成,其他异常是由外部原因造成(打开不存在的文件等)
运行时异常是程序源代码中引入的故障所造成的,如果在代码中提前验证(空指针、数组越界)就可以避免
非运行时异常是程序员无法完全控制的外在问题导致的,即使在代码中提前验证(文件是否存在),也无法完全避免失效发生
Checked异常与Unchecked异常
是从异常处理机制角度进行的分类
Checked异常可被编译器检查,类似于静态类型检查
对于Checked异常,要么方法中有对应的处理程序(将其捕获),要么在方法签名中声明
Error和RuntimeException不能被编译器检查,是Unchecked异常,类似于动态类型检查
Unchecked异常可以使用try...catch等机制处理,但不需要,也不应该这样处理
异常处理
throws:声明“本方法可能会发生某个(些)异常”
throw:抛出异常
try...catch...finally:捕获异常并处理
使用哪种异常?
如果客户端可以通过其他的方法恢复异常,那么采用checked exception;
如果客户端对出现的这种异常无能为力,那么采用unchecked exception;

异常出现的时候,要做一些试图恢复它的动作而不要仅仅的打印它的信息
尽量使用unchecked exception来处理编程错误:因为unchecked exception不用使客户端代码显式的处理它们,它们自己会在出现的地方挂起程序并打印出异常信息
如果client端对某种异常无能为力,可以把它转变为一个unchecked exception,程序被挂起并返回客户端异常信息
不要创建没有意义的异常,client应该从checked exception中获取更有价值的信息(案发现场具体是什么样子),利用异常返回的信息来明确操作失败的原因
如果client仅仅想看到异常信息,可以简单抛出一个unchecked exception:

总结:
Checked exception应该让客户端从中得到丰富的信息。
要想让代码更加易读,倾向于用unchecked exception来处理程序中的错误
错误可预料,但无法预防,但可以有手段从中恢复,此时使用checked
如果做不到这一点,则使用unchecked exception

用throws声明抛出Unchecked exception
异常也是方法和client端之间spec的一部分,在post-condition中刻画

程序员必须在方法的spec中明确写清本方法会抛出的所有checked exception,以便于调用该方法的client加以处理
相反地,unchecked exception不是post-condition的一部分,所以不应该出现在spec和函数签名中!
如果一个方法可能抛出多种checked exception,必须把它们都列在函数签名中

方法throws的异常包括:调用的其他方法抛出的checked exception;本方法创建的checked exception
不要抛出Error和unchecked exception!

抛出异常与LSP原则
如果子类型中override了父类型中的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型更宽泛
子类型方法可以抛出更具体的异常,也可以不抛出任何异常
如果父类型的方法未抛出异常,那么子类型的方法也不能抛出异常

如何抛出异常
用“throw 异常对象”来抛出异常

利用Exception的构造函数,将发生错误的现场信息充分的传递给client

创建异常类
如果JDK提供的exception类无法充分描述你的程序发生的错误,可以创建自己的异常类

可以给异常类添加额外信息并在捕获时获取信息,以便于调试:

捕获异常
异常发生后,如果找不到处理器,就终止执行程序,在控制台打印出stack trace(最上一行是异常发生的“第一现场”,接下来每行为上一行被调用的位置)
使用try...catch捕获异常
也可以不在本方法内处理,而是传递给调用方,由client处理(“推卸责任”)
checked exception必须被处理或继续上抛
尽量在自己这里处理,实在不行就往上传——要承担责任
可使用异常对象的getMessage方法获取异常信息,用getClass().getName()获取异常的具体类型
可以用多个catch语句来捕获多种异常

多次抛出异常与异常链
本来catch语句下面是用来做exception handling的,但也可以在catch里抛出异常
这么做的目的是:更改exception的类型,更方便client端获取错误信息并处理

但这么做的时候最好保留“根原因”:

当异常被捕获时,原本的异常可以被取回:

finally语句
无论异常是否发生都要执行的代码写在finally中,在最后执行
如果try中的程序抛出了catch中没有捕获的异常,则运行至try中抛出异常的代码后,执行finally中的代码,然后将异常上抛
可以不使用catch而只使用finally:

无论try中的代码是否抛出异常,in.close()都会被执行
在有finally的情况下,try中的return会被屏蔽:

finally中的return语句会阻止异常的继续上抛:


分析堆栈追踪
函数的调用:

结果:

在上面代码的methodC中加入一个除0操作,则结果如下:

可见错误信息的第一行是异常产生的位置,此后的每一行都是其上一行的方法被调用的位置
断言
当前置条件不满足时,抛出AssertionError异常——尽可能早地指出用户的bug
检查前置条件是防御式编程的一种典型形式
断言:在开发阶段的代码中嵌入,检验某些“假设”是否成立。若成立,表明程序运行正常,否则表明存在错误

断言即是对代码中程序员所做假设的文档化,也不会影响运行时性能(在实际使用时,assertion都会被disabled)
断言的形式:

断言什么
应断言:
- 内部不变量
- 表示不变量
- 控制流不变量(断言程序的某个位置不会到达,如switch的defalt)
- 方法的前置条件
- 方法的后置条件
控制流不变量:

但不要使用assert,而是要直接抛出AssertionError
其他的断言情形:不应被修改的量、空指针等
断言主要用于开发阶段,避免引入和帮助发现bug。实际运行阶段,不再使用断言,避免影响性能
所以程序的正确性不能依赖于断言!

若采用前一种,则由于实际运行阶段不使用断言,所以会跳过list.remove(x)
不要随意断言程序外部的事情!外部错误要使用Exception机制去处理
由于断言非常影响性能,所以java缺省关闭断言,要记得打开
断言与异常
断言与正确性相关,异常与健壮性相关
使用异常来处理你“预料到可以发生”的不正常情况
使用断言处理“绝不应该发生”的情况
对于传入参数的检查:
如果参数来自于外部(不受自己控制),使用异常处理
如果来自于自己所写的其他代码,可以使用断言来帮助发现错误
浙公网安备 33010602011771号