面向正确性与健壮性的软件构造
什么是鲁棒性和准确性?
鲁棒性:系统在输入异常或外部环境异常的情况下仍能正常运行的程度。
处理意外行为和错误退出
即使执行终止,也能准确、清晰地向用户显示全面的错误信息。
错误消息对于调试很有用
稳健性原理(Postel 定律)
1. 偏执:总是假设用户是恶意的,并假设代码可能会失败。
2.白痴:将用户视为可以输入任何内容的白痴。
3、对别人宽容,对自己残酷。对代码必须保守,对用户行为必须开放
原则:封闭实现细节,限制恶意用户行为。让我们考虑一种极端情况。没有什么是不可能的
准确性:按照规范运行程序的能力是最重要的质量指标。
声音和准确:在天平的两端。
准确性:永远不会给用户错误的结果
鲁棒性:让软件尽可能长时间地运行,而不是总是终止它
正确性倾向于直接报告错误,稳健性倾向于容错。
稳健性:不要给用户施加太大的压力,这样他们就可以容忍一些问题。
稳健:用户易于使用。错误是可以容忍的,并且程序中存在容错机制。
准确性:让开发人员更容易。如果用户输入错误,则直接退出。 (不满足前提条件的调用)
鲁棒性和准确性比较
在安全关键型应用中,准确性往往优先于稳健性。
– 不返回结果比返回错误结果要好。
消费者应用程序往往强调稳健性和准确性。
– 通常比关闭软件会产生更好的结果。
可靠性。系统在指定条件下、无论何时需要、且平均故障间隔时间较长的能力。
可靠性=稳健性+ 准确性
提高稳健性和准确性的步骤
第0 步:使用断言、防御性编程、代码审查、形式验证等以健壮性和正确性为目标来编写代码。
步骤1:观察故障的症状(内存转储、堆栈跟踪、执行日志、测试)
步骤2:识别潜在故障(错误定位、调试)
第3 步:修复错误(代码修复)
如何衡量稳健性和准确性?
外部:平均故障间隔时间(MTBF) 是操作系统固有的故障之间的预期经过时间。
内部:根据KLOC 的说法,残余缺陷率是指“软件发布后留下的缺陷”,是每1,000 行代码留下的错误数量。
Java 错误和异常
内部错误:发生此错误时程序员通常无能为力。找到一种优雅退出程序的方法。
例外:由专有程序引起的问题。这可以被捕获并处理。
错误类型:用户输入错误、设备错误、物理限制
异常处理
什么是例外?
异常是指程序执行过程中发生的异常事件,中断了程序的正常运行。
程序执行期间发生异常事件,导致程序无法按预期运行。
异常是代码向调用它的代码传达错误或异常事件的特定方式。将错误信息传递给更高级别的调用者并报告“犯罪现场”信息。如果任务无法以正常方式完成,Java 允许每个方法有一个替代的退出路径。除了返回之外的第二种退出方式
– 该方法抛出一个封装错误信息的对象。
– 该方法立即退出并且不返回任何值。
此外,调用该方法时不会恢复执行。
相反,异常处理机制开始搜索可以处理此特定错误情况的异常处理程序。
如果没有找到异常处理程序,则整个系统完全退出。
异常情况分类
运行时异常:是由于程序员对代码处理不当引起的。如果事先在代码中执行验证,则可以避免这些错误。
其他异常:是由外部原因引起的。非运行时异常是由完全超出程序员控制范围的外部问题引起的。即使代码经过预先验证(文件存在与否),也无法完全避免失败。
检查和非检查异常
异常情况如何处理?
发生异常时检查
您必须捕获并处理异常,或者声明一个抛出异常的方法以告诉编译器它无法处理异常。
使用该方法的代码必须处理异常(或者如果不能的话,可以选择声明它以引发异常)。
– 编译器检查您是否完成了以下两件事之一:捕获或声明。编译器可以帮助您查看程序是否引发或处理了异常。
编译器不检查错误或运行时异常。错误表示应用程序外部发生的情况,例如系统崩溃。运行时异常通常是由应用程序逻辑错误引起的。在这种情况下,唯一的选择就是重写程序代码。所以编译器不会检查这些。这些运行时异常是在开发和测试期间发现的。然后您需要重构代码以消除这些错误。
未经检查的异常
编译程序不需要任何操作,但如果不捕获异常,程序将失败。编译时不需要使用try.catch或者其他机制。但是,如果在执行过程中发生这种情况,程序就会失败,这表明程序中存在潜在的错误。
检查异常
您必须捕获并指定错误处理程序。否则编译不会通过。
您还可以使用throws 语句或try/catch 来捕获异常,但在大多数情况下这是不必要的,也不应该这样做。 —— 捂住耳朵、偷铃并忽略发现的编程错误。
在决定使用检查异常还是非检查异常时,请询问以下问题:“如果抛出此异常,客户端将采取什么补救措施?”
当客户端可以通过其他方式从异常中恢复时,请使用已检查异常。
当客户端无法对异常执行任何操作时,请使用未经检查的异常。
如果发生异常,请采取一些操作来尝试恢复,而不仅仅是打印该信息。
不要创建无意义的异常。客户端从检查的异常中获取更有价值的信息(犯罪现场到底是什么样子),并使用异常返回的信息来找出操作失败的原因。 如果客户端只想查看异常信息,则可以直接抛出未经检查的异常。检查异常应该为客户端提供丰富的信息。 为了使代码更易于阅读,我们倾向于使用未经检查的异常来处理程序中的错误。
错误是可以预料到的,但无法避免,但有一种方法可以使用错误预检查来恢复。
如果不可能,请使用未经检查的异常
错误是可以预测的,但无法预防
如果读取时发现该文件不存在,用户可以选择另一个文件,但如果调用该方法时传递了错误的参数,则在不中止执行的情况下无法进行恢复。
异常设计注意事项
对特殊结果(即预期情况)使用检查异常。
使用未经检查的异常的信号错误(意外失败)
使用throw 声明已检查的错误
通过throws 声明受检查的异常允许Java 方法在遇到无法处理的情况时抛出异常。
方法不仅告诉Java编译器可以返回什么值,还告诉Java编译器什么可能会出错。 “例外”也是方法和客户端的条件
程序员必须在方法规范中清楚地记录此方法抛出的任何已检查异常,以便调用该方法的客户端可以处理它。
如果一个方法可以抛出多个已检查的异常类型,则所有异常类必须在标头中列出。
不要抛出错误或未经检查的异常
不需要暴露继承自Error 的内部Java 错误异常。
任何代码都可能引发这些异常,并且完全超出您的控制范围。
不要公开从RuntimeException 继承的未检查异常。
考虑亚型多态性
如果子类型重写了父类型的函数,则子类型的方法抛出的异常类型不能比父类型抛出的异常类型宽。子类型方法可以抛出更具体的异常或根本不抛出异常。如果超类型的方法不抛出异常,那么子类型的方法也不能抛出异常。
请参阅LSP 原则。目标是亚型多态性。客户端可以以统一的方式处理不同类型的对象,子类型可以替换父类型。
里氏替换原则
LSP 是子类型关系的特殊定义,称为(强)行为子类型。 强行为亚型
在编程语言中,LSP 依赖于以下约束:
1. 不能对子类型强加前提条件。
2. 子类型不能削弱后置条件。
3. 超类型不变量必须保留在子类型中。
4. 子类型方法参数的反面子类型方法参数:逆变
5. 子类型内返回类型的协方差。子类型方法返回值:协方差
6. 子类型的方法不得引发新的异常,除非它们本身是其超类型的方法引发的异常的子类型。
如何抛出异常
使用Exception构造函数将现场错误信息完整传递给客户端。
找到一个可以表达错误的异常类,或者构造一个新的异常类。
构造Exception 类的实例,写入并引发错误消息。
当抛出异常时,该方法不会将控制权返回给调用客户端,因此无需考虑返回错误代码。
创建异常类
如果JDK 提供的异常类不能充分描述程序中发生的错误,您可以创建自己的异常类。
只需派生自Exception 或Exception 的子类即可,例如IOException。
通常会提供默认构造函数和带有详细消息的构造函数。
Throwable 超类的toString 方法返回包含详细消息的字符串。这对于调试很有用。
异常信息还包括“犯罪现场信息”的异常类定义和辅助函数
当抛出异常时,本地信息会记录在异常中。
使用这些信息可以为用户在处理异常时提供更有帮助的帮助。
捕获异常
如果发生异常后找不到处理器,则执行程序将终止并将堆栈跟踪打印到控制台。
如果try 块中的代码引发catch 子句中指定的类的异常,
– 程序将跳过try 块中的其余代码。
– 程序执行catch 子句中的处理程序代码。
– 如果try 块中的代码未引发异常,则程序将跳过catch 子句。
– 如果方法内的代码抛出除catch 子句中指定的异常类型之外的异常类型,则该方法立即退出。
– 优选地,调用者之一提供该类型的catch 子句。
处理异常的另一种选择是不执行任何操作并将异常传递给调用者。
如果您尝试自行处理无效,——负责上传。
然而,有时你不知道如何处理它,所以要小心你的主机,让你的客户自己处理。
消息:
如果超类型的方法不抛出异常,则子类型的方法必须捕获所有已检查的异常。
子类型上的方法不能抛出比其父类型上的方法更多的异常。
重新抛出异常并连接
Catch 语句主要用于异常处理,但也可以在catch 语句内引发异常。
目的是改变异常的类型,使客户端更容易检索和处理错误信息。
最后条款
当您的代码引发异常时,该方法中的其余代码将停止处理,并且该方法中正常运行的任何代码(文件、数据库连接等)都会终止。由于该方法知道这些资源,因此当需要清理这些资源时就会出现问题。如果异常发生前应用了某些资源,则异常发生后必须妥善清理这些资源。
一种解决方案是捕获并重新抛出所有异常。然而,这个解决方案很麻烦,因为它需要在两个地方进行资源分配清理:在常规代码中和在异常代码中。
Java有一个更好的解决方案:finally子句。
分析堆栈跟踪元素
异常调用栈
当Java 方法中发生异常时,该方法会创建一个异常对象并将其传递给JVM(即该方法“抛出”异常)。 Exception对象包含异常的类型以及异常发生时程序的状态。
JVM 负责寻找异常处理程序来处理异常对象。在调用堆栈中向后搜索,直到找到与特定异常对象类匹配的异常处理程序(在Java 术语中,这称为“捕获”异常)。
如果JVM 在调用堆栈中找不到任何方法的匹配异常处理程序,它将终止程序。
分析堆栈跟踪元素
堆栈跟踪是程序执行中特定点的所有挂起方法调用的列表。
每当您的Java 程序因未捕获的异常而退出时,您几乎肯定会看到堆栈跟踪列表。
您可以通过调用Throwable 类的printStackTrace 方法来访问堆栈跟踪的文本描述。
更灵活的方法是getStackTrace 方法。
可以通过编程方式分析的StackTraceElement 对象的数组。
分析框架
StackTraceElement 类具有检索文件名和行号以及执行该代码行的类名和方法名的方法。
toString 方法生成包含所有这些信息的格式化字符串。
断言
第一道防线:杜绝bug
防止错误的最佳方法是通过设计使它们不可能出现。最好的防御是不引入错误
– 静态检查:通过在编译时检测来消除许多错误。
– 动态检查:Java动态检测数组溢出错误并使其不可能发生。如果您尝试在数组或列表中使用越界索引,Java 会自动生成错误。 —— 未经检查的异常/运行时错误
-Immutable:不可变类型是指一旦创建其值就不会改变的类型。
– 不可变值:final 导致它们仅被分配一次,但永远不会重新分配。
– 不可变引用:最终,引用变得不可分配,但它指向的对象可能是可变的或不可变的。
第二道防线:本地化错误
如果您无法防止错误,您可以尝试将其限制在程序的一小部分,这样您就不必费力寻找错误的原因。如果无法避免,尽量将程序误差限制在尽可能小的范围内。
– 当本地化到单个方法或小模块时,您可以通过检查程序文本来发现错误。限制在方法内且不扩散
– 更快的故障:越早发现问题(越接近原因),就越容易修复。发生故障的时间越早,就越容易检测到,修复的速度也就越快。
断言:如果不满足先决条件,此代码将引发AssertionError 异常并终止程序。防止调用者错误传播。很快就会崩溃
前提条件检查是防御性编程的一个例子
前提条件检查是防御性编程的经典形式
– 真正的程序很少是没有错误的。
– 防御性编程提供了一种减少错误影响的方法,即使您不知道错误在哪里。
你主张什么以及为什么主张它?
断言:在开发过程中嵌入代码中,以测试某些“假设”是否正确。如果为true 则表示程序运行成功,否则表示发生错误。
每个断言都包含一个布尔表达式,程序运行时该表达式预计为true。否则,JVM 会抛出断言错误。
此错误意味着存在需要纠正的无效假设。断言失败意味着内部假设被违反
此断言证实了您对程序行为的假设,并增加了程序没有错误的信心。
程序员对其代码的质量有很高的信心: 对代码所做的假设仍然是正确的。
断言记录了程序员在代码中所做的假设,并且不会影响运行时性能(在实际使用中,断言没有任何作用)。
断言通常包括两个参数,它们描述一个布尔表达式,假设该表达式为真,如果不为真则显示一条消息。
Java语言有关键字Assert,它有两种形式:
– 断言条件;
-断言条件:消息;
– 如果布尔表达式的计算结果为false,则两个语句都会计算条件并抛出AssertionError。
– 在第二个语句中,表达式被传递到AssertionError 对象的构造函数并转换为消息字符串。当断言失败时,错误消息中会打印描述,该描述可用于向程序员提供有关失败原因的其他详细信息。
当发生错误时,创建的消息会显示给用户,以便于查找错误所在。
什么是被主张的,什么是未被主张的?
#以上关于软件构建类的注释:第12章—— 软件构建相关内容,强调准确性和鲁棒性来源网络,仅供参考。相关信息请参见官方公告。
原创文章,作者:CSDN,如若转载,请注明出处:https://www.sudun.com/ask/91426.html