什么是健壮性(Robustness)和正确性(Correctness)
健壮性
健壮性:系统在不 正常输入或不正常外部环境下仍能够表现正常的程度
在面向健壮性的编程要求:
- 处理未期望的行为和错误终止
- 即使终止执行,也 要准确/无歧义的向用户展示全面的错误信息
- 错误信息有助于进行debug
健壮性原则(Postel’s Law):
- 总是假定用户恶意、假定自己的代码可能失败
- 把用户想象成白痴,可能输入任何东西
- 因此,开发者需要使程序能够返回给用户的错误提示信息要详细、准确、无歧义,这样更容易解决问题
- 总的来说对别人宽容点,对自己狠一点,对自己代码保守,对用户行为开放
健壮性开发的原则:
- 封闭实现细节,限定用户的恶意行为
- 通过隐藏内部细节,使得用户无法因为修改内部细节而引发错误
- 考虑极端情况,没有“不可能”
正确性
正确性:程序按照spec加以执行的能力,是最重要的质量指标!
关于正确性和健壮性的比较:
- 正确性:永不给用户错误的结果
- 永不返回不准确的结果;没有结果比不准确的结果更好
- 健壮性:尽可能保持软件运行而不是总是退出
- 总是尝试做一些事情,让软件继续运行,即使有时会导致不准确的结果
- 总结:
- 健壮性增加了对常见和非关键错误的内置容忍度,而正确性则在遇到不完美的输入时抛出错误
- 正确性倾向于直接报错(error),健壮性则倾向于容错(fault-tolerance)
健壮性和正确性的作用和应用的比较
- 健壮性:让用户变得更容易:出错也可以容忍,程序内部已有容错机制
- 正确性:让开发者变得更容易:用户输入错误,直接结束。(不满足precondition的调用)
对外的接口,倾向于健壮;对内的实现,倾向于正确
- 对外的接口尽可能适应用户输入的垃圾
- 但是对内的模型要尽可能的简单,100%有效,通过invariant和assertion做出安全假设,在不正确的情况直接抛出异常
- 怎么理解?
- 如果对外接口的健壮性不够好,用户就不想用了
- 而对内的正确性不好,导致需要考虑各种边界情况等,导致内部结构变得非常复杂,程序员会不满意
对于特定领域的应用:
- 安全领域的应用倾向于更加满足正确性
- 面向消费者的应用倾向于满足健壮性
- 输出不准确的结果好过于关闭应用
Reliability (可靠性)的衡量:Robustness + Correctness
系统在规定条件下执行所需功能的能力,即在需要的时候有较长的平均故障间隔时间
怎么提高健壮性和正确性?
步骤
- 使用assertion,defensive programing, code review, formal validation来编程
- 观察出现的错误
- 识别潜在错误
- 修复
如何衡量正确性和健壮性
外部角度
Mean time between failures (MTBF):平均失效时间
这个时间的计算取决于怎么定义failures
对于复杂的、可修复的系统,故障被认为是那些超出设计条件,使系统停止服务并进入待修复状态的故障。
在本定义下,对于可以留在或保持在未修复的状态,并且不会使系统停止服务的故障,不被认为是故障
MTBF描述可修复系统两次故障之间的预期时间,注意是可修复系统
mean time to failure (MTTF) denotes the expected time to failure for a nonrepairable system.
内部
Residual defect rates 残余缺陷率,每千行代码中遗留的bug的数量
![[Pasted image 20230410150257.png]]
Error Exception
java.lang.Throwable
Error:程序员通常无能为力,一旦发生,想办法让程序优雅的结束
Exception:你自己程序导致的问题,可以捕获、可以处理
关于Error
分类
- 用户输入错误
- 设备错误
- 物理限制
一些常见的Error
- VirtualMachineError:JVM遇到内部错误或资源限制无法运行时
- OutOfMemoryError
- Java虚拟机由于内存不足而无法分配对象时抛出,并且垃圾回收器不能再提供内存
- StackOverflowError
- 递归太深而发生堆栈溢出时引发
- InternalError
- JVM中发生了一些意外的内部错误
- OutOfMemoryError
- LinkageError:一个类依赖于另一个类;但是,在编译前一个类之后,后一个类发生了不兼容的变化
- NoClassDefFoundError
- 如果JVM或ClassLoader实例试图装入类的定义,但找不到类的定义时抛出
- NoClassDefFoundError
处理Exception(Error无能为力)
什么是Exception
异常:程序执行中的非正常事件,程序无法再按预想的流程执行
将错误信息传递给上层 调用者,并报告“案发现场”的信息
当一个方法抛出了一个错误,该方法立刻退出,不返回任何值,不会在调用该方法的代码处恢复,相反,异常处理机制开始寻找可以处理这种特定错误条件的异常处理程序
理解:当一个错误发生时,会退出当前方法,把错误信息层层上报,直到寻找到可以处理这种特定错误的异常处理程序
Exception分类
Runtime Exception
运行时异常:由程序员在代码里处理不当造成,是程序源代码中引入的故障所造成的
-
A bad cast
-
An out-of-bounds array access
-
A null pointer access
-
您可以通过根据数组边界测试数组索引来避免ArrayIndexOutOfBoundsException
-
如果你在使用变量之前检查它是否为null,就不会发生NullPointerException
-
如果在代码中提前进行验证,这些故障就可以避免
Other Exceptions
其他异常:由外部原因造成,是程序员无法完全控制的外在问题所导致的,比如说,文件不存在
- Trying to read past the end of a file
- Trying to open a file that doesn’t exist
- Trying to find a Class object for a string that does not denote an existing class
关于文件是否存在
你不能先检查文件是否存在,然后再打开它吗?
实际上,文件可能会在你检查它是否存在后立即被删除。
因此,“存在”的概念取决于环境,而不仅仅是您的代码
即使在代码中提前加以验证(文件是否存在),也无法完全避免失效发生
Checked and unchecked exceptions
这是从异常处理机制的角度所做的分类 异常被谁check?——编译器、程序员
Checked Exceptions
当异常出现时
- 你要么捕获并处理异常,要么告诉编译器你不能通过声明你的方法抛出那个异常来处理它
- 然后使用你的方法的代码将不得不处理该异常 (如果不能处理异常,可以选择声明抛出异常)。
- 编译器会检查我们是否做了两件事中的一件(catch或declare)
- 必须捕获并指定错误 处理器handler,否则 编译无法通过 类似于编程语言中的 static type checking
Unchecked Exceptions
Errors and Runtime Exceptions are not checked by compiler
- 错误表示发生在应用程序之外的情况,例如系统崩溃。Runtime exceptions通常是由于应用程序逻辑中的错误而发生的
- 在这种情况下,您什么也做不了,只能重新编写程序代码。编译器不会检查这些。
- 这些运行时异常将在开发和测试期间发现。然后我们必须重构代码以消除这些错误。
- 不需要在编译的时候用try…catch等机制处理
- 可以不处理,编译没 问题,但执行时出现 就导致程序失败,代 表程序中的潜在bug 类似于编程语言中的 dynamic type checking
对于checked exception:
Five keywords are used in exception handling:
- try
- catch
- finally
- 以上三个都是捕获并处理XX异常
- throws
- 声明“本方法可能会发生XX异常”
- 作用于方法上
- throw
- 抛出XX异常
Unchecked异常也可以使用throws声明或try/catch进 行捕获,但大多数时候是不需要的,也不应该这么做——掩耳盗铃, 对发现的编程错误充耳不闻
当要决定是采用checked exception还是unchecked exception的时候,问一个问题:“如果这种异常一旦抛出,client会做 怎样的补救?”
- 如果客户端可以通过其他的方法恢复异常,那么采用checked exception;
- 如果客户端对出现的这种异常无能为力,那么采用unchecked exception;
- 异常出现的时候,要做一些试图恢复它的动作而不要仅仅的打印它的信息
尽量使用unchecked exception来处理编程错误:因为unchecked exception不用使客户端代码显式的处理它们,它们自己会在出现的地 方挂起程序并打印出异常信息。
- 充分利用Java API中提供的丰富unchecked exception,如 NullPointerException , IllegalArgumentException和 IllegalStateException等,使用这些标准的异常类而不需亲自创建新的 异常类,使代码易于理解并避免过多消耗内存
如果client端对某种异常无能为力,可以把它转变 为一个unchecked exception,程序被挂起并返回 客户端异常信息
-
不要创建没有意义的异常,client应该从checked exception中获取更有价值 的信息(案发现场具体是什么样子),利用异常返回的信息来明确操作失败的 原因。
-
如果client仅仅想看到异常信息,可以简单抛出一个unchecked exception:
-
错误可预 料,但无法预防,但可以有手段从中恢复,此时使用checked exception。
-
如果做 不到这一点,则使用unchecked exception
-
如果读文件的时候发现文件不存在了,可以让用户选择其他文件;但是如果 调用某方法时传入了错误的参数,则无论如何都无法在不中止执行的前提下 进行恢复。
抛出checked异常
“异常”也是方法和 client端之间spec的一部分,在post-condition中刻画
在Javadoc中@throws写明异常,并且在方法中throws
unchecked异常不用写出
你的方法应该throws什么异常?
- 你所调用的其他函数抛出了一个checked exception——从其他函数传来的异常
- 当前方法检测到错误并使用throws抛出了一个checked exception——你自己造出的异常
- 此时需要告知你的client需要 处理这些异常
- 如果没有handler来处理被抛出的checked exception,程序就终 止执行
对于Error和RuntimeException的子类,不需要抛出
LSP
- 如果子类型中override了父类型中 的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型 更宽泛
- 子类型方法可以抛出更具体的异 常,也可以不抛出任何异常
- 如果父类型的方法未抛出 异常,那么子类型的方法也不能抛出异常
怎么抛出异常
![[Pasted image 20230521204135.png]]
利用Exception的构造函数,将发生错误 的现场信息充分的传递给client。
总结:
- 找到一个能表达错误的Exception类/ 或者构造一个新的Exception类
- 构造Exception类的实例,将错误信息写入
- 抛出它
- 一旦抛出异常,方法不会再将控制权返 回给调用它的client,因此也无需考虑返回错误代码
创建异常类
前提:如果JDK提供的exception类无法充分描述你的程序发生的错误,可以 创建自己的异常类
只需从Exception或Exception的子类(如IOException)中继承即可
习惯上同时给出默认构造函数和包含详细消息的构造函数
Throwable超类的toString方法返回一个包含详细消息的字符串,这对调试很方便。
还可以抛出unchecked异常:需要继承java.lang.RuntimeException
异常类可以包含更多信息,在自定义的异常类里多加几个参数,修改构造器传入的参数数量
捕获异常
异常发生后,如果找不到处理器, 就终止执行程序,在控制台打印出stack trace。
有异常,则跳过try剩余部分,找到异常对于的catch,执行catch。如果出现的异常没有catch能对于,则方法立刻退出。
异常的处理:
- 尽量在自己这里处理,实在不行就往上传——要承担责任!
- 但有些 时候自己不知道如何处理,那么提醒上家,由client自己处理
注意:
- 如果父类型中的方法没有抛出异常,那么子类型中的方法必须捕获所有的checked exception
- 子类型方法中不能抛出比父类型 方法更多的异常!
Rethrowing and Chaining Exceptions
本来catch语句下面是 用来做exception handling的,但也可以在catch里抛出异常
这么做的目的是:更改exception的类型,更方便client端获取错误信 息并处理
但这么做的时候最好保留“根原因”
强烈推荐使用这种包装技术。它允许您在子系统中抛出高级异常,而不会丢失原始故障的细节
finally Clause
当异常抛出时,方法中正常执行的代码被终止
如果异常发生前曾申请过某 些资源,那么异常发生后这些资源要被恰当的清理
finally会在任何时候被执行,不管程序是否碰到异常,finally都会被执行
In the following example, the program will close the file under all circumstances:
InputStream in = new FileInputStream(. . .);
try {
// 1
code that might throw exceptions
// 2
}
catch (IOException e) {
// 3
show error message
// 4
}
finally {
// 5
in.close();
}
// 6
异常情况:
- 捕获异常,处理异常
- 1,3,4,5,6
- 捕获异常,抛出新异常
- 1,3,5
- 抛出一个未被任何catch子句捕获的异常
- 1,5,Go back to the client
Analyzing Stack Trace Elements
您可以通过调用Throwable类的printStackTrace方法来访问堆栈跟踪的文本描述
更灵活的方法是getStackTrace方法,它产生一个StackTraceElement对象数组,您可以在程序中对其进行分析。
StackTraceElement类具有获取执行代码行的文件名和行号以及类和方法名的方法
toString方法产生一个包含所有这些信息的格式化字符串。
Assertions
what
Fail fast避免扩散,检查前置条件是防御式编程的一种典型形式
断言:在开发阶段的代码中嵌入,检验某些“假设”是否 成立。若成立,表明程序运行正常,否则表明存在错误。
每个断言都包含一个布尔表达式,您认为该表达式在程序执行时为真
- If it is not true, the JVM will throw an AssertionError.
- 出现AssertionError,意味着内部某些假设被违反了
- 增 强程序员对代码质量的信心:对代码所做的假设都保持正确
断言即是对代码中程序员所做假设的文档化,也不会影响运行时性能 (在实际使用时,assertion都会被disabled)
一个断言通常有两个参数
- 描述假定为真的布尔表达式
- assert condition
- 如果不是,则显示一条消息
- assert condition : message
- 所构造的message在发生错误时显示给用户,便于快速发现错误所在
What to Assert and What not to?
- Pre-condition: method argument requirements
- Post-condition: Method return value requirements
- Control-flow: covering all
- 如果条件语句或开关不能涵盖所有可能的情况,那么最好使用断言来阻止非法情况。
但是不要在这里使用assert语句,因为它可以被关闭。相反,在非法的情况下抛出异常,这样检查就会一直发生
default: throw new AssertionError("must be a vowel, but was: " + vowel);
断言主要用于开发阶段,避免引入 和帮助发现bug
实际运行阶段, 不再使用断言,避免降低性能
使用断言的主要目的是 为了在开发阶段调试程序、尽快避免错误
- 由于断言可能被禁用,所以程序的正确性永远不应该依赖于断言表达式是否被执行
- 特别地,断言表达式不应该有副作用
程序之外的事,不受你控制,不要乱断言
- 文件/网络/用户输入等
- 外部错误要 使用Exception机制去处理
默认断言是关闭的
- Enable assertions
- -enableassertions or -ea
- Disable assertions
- disableassertions or -da
Guidelines for Using Assertions
断言→Correctness
如果针对异常情况触发了一个断言,那么纠正操作不仅仅是优雅地处理错误——纠正操作是更改程序的源代码、重新编译并发布软件的新版本
错误/异常处理→Robustness
如果错误处理代码用于处理异常情况,则错误处理将使程序能够优雅地响应错误
使用异常来处理你“预料到可以发生”的不正常情况
使用断言处理 “绝不应该发生”的情况
在其他一些开发者眼里,不应该针对参数的合法性使用断言
- 不管是否-ea, spec中的pre-/post-conditions都能够被保证
- 即使spec被违反,也不应通过assert直接fail,而是 应抛出具体的runtime异常
如果参数来 自于外部(不受自己控制),使用异常处理
如果来自于自己所写的其他代码,可以使用断言来帮助发现错误(例如postcondition就需要)
您可以使用断言来测试非公共方法的前提条件,您认为无论客户端对类做什么,该前提条件都将为真。
你可以在公共和非公共方法中使用断言测试后置条件。
开发阶段用断言尽可能消除bugs 在发行版本里用异常处理机制处理漏掉的错误
防御式编程
眼观六路,耳听八方,一旦 其他车辆有对你产生危险的 症状,马上采取防御式行动
Techniques for defensive programming
- Protecting programs from invalid inputs
- 对来自外部的数 据源要仔细检查,例如:文件、网络数据、用户输入等
- 对每个函数的输入 参数合法性要做仔细检查,并决定如何处理非法输入
- Assertions
- Exceptions
- Specific error handling techniques
- Barricade
- 类的public方法 接收到的外部数据都应被认为是dirty的,需要处理干净再传递到 private方法——隔离舱
- 一旦数据被类的公共方法接受,类的私有方法就可以假定数据是安全的。
- operating-room technique
- 数据在进入手术室之前是经过消毒的。手术室里的任何东西都被认为是安全的。
- 关键的设计决策是决定把什么放在手术室里,把什么放在外面,把门放在哪里-哪些程序被认为是在安全区内,哪些在外面,哪些是消毒数据
- 要做到这一点,最简单的方法通常是在外部数据到达时对其进行消毒,但是数据通常需要在多个级别上进行消毒,因此有时需要多个级别的消毒
- 障碍物的使用使断言和错误处理之间的区别变得清晰。
- “隔离舱”外部的函数应使用异常处理,“隔离舱”内的函数应使用 断言。
- 类的public方法 接收到的外部数据都应被认为是dirty的,需要处理干净再传递到 private方法——隔离舱
- Debugging aids