软件构造复习——面向正确性与健壮性的软件构造(PPT12)


一、什么是健壮性和正确性

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
对外的接口,倾向于健壮;对内的实现,倾向于正确

二、如何衡量稳健性和正确性

在这里插入图片描述

三、Java 中的错误和异常

在这里插入图片描述

3.1 Error

Error 类描述了 Java 运行时系统内部很少发生的内部系统错误和资源耗尽情况(例如 VirtualMachineError、LinkageError)。
– 你不应该抛出这种类型的对象。
– 如果发生此类内部错误,除了通知用户并尝试正常终止程序之外,您几乎无能为力。
例如:用户输入错误用户输入错误
– 除了不可避免的错别字外,一些用户喜欢开辟自己的道路而不是遵循指示。
– 例如 用户要求连接到语法错误的 URL,网络层会抱怨。
§ 设备错误设备错误
– 硬件并不总是按照您的意愿行事。
– 打印机可能已关闭。
– 网页可能暂时不可用。
– 设备在执行任务时经常会出现故障。
§ 物理限制物理限制
– 磁盘可以填满
– 您可能会耗尽可用内存

3.2 Exception

在这里插入图片描述

Exception 类描述了由您的程序引起的错误(例如 FileNotFoundException、IOException)。
– 这些错误可以由您的程序捕获和处理(例如,执行替代操作或通过关闭所有文件、网络和数据库连接来正常退出)。

在这里插入图片描述

在这里插入图片描述

异常是在程序执行过程中出现的一种异常事件,它扰乱了程序的正常流程。
异常:程序执行中的非正常事件,程序无法再按预想的流程执行
§ 异常是代码可以将错误或异常事件传递给调用它的代码的特定方式。
将错误信息传递给上层调用者,并报告“案发现场”的信息
§ 如果每个方法无法以正常方式完成其任务,Java 允许它有一个替代的退出路径。 回归之外的另一种退出途径
– 该方法抛出一个封装了错误信息的对象。
– 方法立即退出,不返回任何值。
– 此外,不会在调用该方法的代码处恢复执行;
– 相反,异常处理机制开始寻找
可以处理此特定错误情况的异常处理程序。
如果缺少异常处理程序,整个系统完全退出

在这里插入图片描述
在这里插入图片描述
异常分类
在 Java 编程语言中,异常对象始终是从 Throwable 派生的类的实例。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从 RuntimeException 继承的异常包括以下问题:
– 糟糕的演员阵容
– 越界数组访问
– 空指针访问
——……
§ 不从 untimeException 继承的异常包括
– 尝试读取文件末尾
– 试图打开一个不存在的文件
– 尝试为不表示现有类的字符串查找 Class 对象

在这里插入图片描述

您可以通过针对数组边界测试数组索引来避免 ArrayIndexOutOfBoundsException。
– 如果您在使用之前检查变量是否为空,则不会发生 NullPointerException。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

$发生异常时
– 您必须捕获并处理异常,或者通过声明您的方法抛出该异常来告诉编译器您无法处理它,
– 然后使用您的方法的代码将不得不处理该异常(如果无法处理,可以选择声明它抛出异常)。
– 编译器将检查我们是否完成了两件事之一(catch 或declare)。 编译器可帮助检查你的程序是否已抛出或处理了可能的异常

§ 编译器不检查错误和运行时异常
– 错误代表发生在应用程序之外的情况,例如系统崩溃。 运行时异常通常是由应用程序逻辑中的错误引起的。
– 在这些情况下您不能做任何事情,但必须重新编写您的程序代码。 所以编译器不会检查这些。
– 这些运行时异常将在开发和测试期间发现。 然后我们必须重构我们的代码以消除这些错误

在这里插入图片描述

未经检查的异常:编程错误,其他不可恢复的失败(Error + RuntimeException)
– 程序编译不需要任何动作,但未捕获的异常会导致程序失败
–不需要在编译的时候用try…catch等机制处理

在这里插入图片描述
检查异常:每个调用者都应该知道并处理的错误
§ 必须被捕获或传播,否则程序将无法编译(编译器检查您是否为所有已检查的异常提供了异常处理程序)

在这里插入图片描述
在这里插入图片描述
翻译:
ArrayIndexOutOfBoundsException:当您的代码使用超出数组边界的数组索引时由 JVM 抛出。
NullPointerException:当您的代码尝试使用需要对象引用的空引用时由 JVM 抛出
以上两个异常(数组越界、空指针)在编程和编译的时候,IDE与编译器均不会给出任何错误提示

在这里插入图片描述

在这里插入图片描述

常见的未检查异常类
§ NumberFormatException:当试图将字符串转换为数字类型时,以编程方式抛出(例如,由 Integer.parseInt()),但该字符串没有适当的格式。
Integer.parseInt(“abc”);

线程“main”中的异常
java.lang.NumberFormatException:对于输入字符串:
“ABC”

§ ClassCastException:当尝试强制转换对象引用失败时由 JVM 抛出。
Object o = new Object();
整数 i = (整数)o;
线程“main”中的异常
java.lang.ClassCastException: java.lang.Object 不能

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

通过抛出声明检查异常

在这里插入图片描述

如果遇到无法处理的情况,Java 方法可能会抛出异常。
– 方法不仅会告诉 Java 编译器它可以返回哪些值,还会告诉编译器可能出错的地方。
“异常”也是方法和客户端之间的特殊部分,在后处理中刻画
– 例如 尝试从文件中读取的代码知道该文件可能不存在或可能为空。 因此,尝试处理文件中信息的代码需要通知编译器它可以抛出某种IOException。
§ 你宣传你的方法可以抛出异常的地方是方法的头部; 标头更改以反映该方法可以抛出的已检查异常。
公共 FileInputStream(String name) 抛出 FileNotFoundException

在这里插入图片描述
规约里也要写异常!

在这里插入图片描述

试试
unchecked异常不应该出现

如何在规范中声明异常
§ 用于表示意外失败的未经检查的异常——客户端或实现中的错误——不是方法后置条件的一部分,因此它们不应出现在@throws 或 throws 中。
§ 例如,NullPointerException 永远不需要在规范中提及

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
不应该抛出Error和uncheked异常

在这里插入图片描述
可以把不抛出异常理解为抛出0异常,是最具体的那一个
在这里插入图片描述

LSP 是一种子类型关系的特殊定义,称为(强)行为子类型化。
§ 在编程语言中,LSP 依赖于以下内容
限制:
– 不能在子类型中加强先决条件。
– 不能在子类型中削弱后置条件。
– 超类型的不变量必须保留在子类型中。
– 子类型中方法参数的逆变
子类型方法参数:
– 子类型中返回类型的协方差。 子类型方法的返回值:协变
– 子类型的方法不应抛出新的异常,除非这些异常本身是超类型方法抛出的异常的子类型

如何抛出异常?
在这里插入图片描述

如何抛出异常
§ 假设您有一个方法 readData,它正在读取文件。 您的代码中发生了一些可怕的事情。
§ 您可能认为这种情况非常不正常,以至于您想抛出一个异常 EOFException,描述为“在输入期间意外到达 EOF 的信号。

在这里插入图片描述

EOFException 有第二个构造函数,它接受一个字符串参数。
§ 您可以通过更仔细地描述异常情况来充分利用这一点。
在这里插入图片描述

如果现有异常类之一适合您,则抛出异常很容易:
– 找到一个合适的异常类 找到一个能表达错误的异常类/
或者构造一个新的Exception类
– 创建该类的对象构造Exception类的实例,将错误信息写入
– 扔掉它
§ 一旦一个方法抛出异常,它就不会返回给它的调用者。
这意味着您不必担心编写默认返回值或错误代码
抛出异常,方法不会再将控制权返回给调用它的客户,因此也不需要考虑返回错误代码
在这里插入图片描述

在这里插入图片描述

您的代码可能会遇到任何标准异常类都没有充分描述的问题。
§ 在这种情况下,创建自己的异常类很容易。
§如果JDK提供的异常类无法描述你的程序发生的错误,可以创建自己的异常类
§ 只需从 Exception 或从 Exception 的子类(例如 IOException)派生它。
§ 通常会同时提供默认构造函数和包含详细消息的构造函数。
– Throwable 超类的 toString 方法返回一个包含该详细消息的字符串,便于调试
在这里插入图片描述

可能抛出或传播此异常的方法必须声明它
要定义已检查的异常,请创建 java.lang.Exception 的子类(或子类的层次结构)

在这里插入图片描述

在这里插入图片描述

有时会出现您不想强制每个方法在其 throws 子句中声明您的异常实现的情况。 在这种情况下,您可以创建一个扩展 java.lang.RuntimeException 的未经检查的异常。

方法可以在不声明的情况下抛出或传播 FooRuntimeException 异常

在这里插入图片描述

在这里插入图片描述

如果发生在任何地方都没有捕获到的异常,程序将终止并向控制台打印一条消息,给出异常的类型和堆栈跟踪

捕捉异常
§ 如果try块内的任何代码抛出catch子句中指定的类的异常,那么
– 程序跳过 try 块中的其余代码。
– 程序执行 catch 子句中的处理程序代码。
– 如果 try 块中的任何代码都没有抛出异常,则程序跳过 catch 子句。
– 如果方法中的任何代码抛出了不同于 catch 子句中指定的类型的异常,则该方法立即退出。
– 希望它的一个调用者已经为该类型提供了一个 catch 子句。

§ 编译器严格执行 throws 说明符。 如果调用抛出已检查异常的方法,则必须处理它或传递它。

在这里插入图片描述

您可以在 try 块中捕获多种异常类型并以不同方式处理每种类型。
§ 对每种类型使用单独的 catch 子句,如下例所示
在这里插入图片描述

在这里插入图片描述

通常,当您想要更改异常类型时会执行此操作。
§ 如果你构建了一个其他程序员使用的子系统,那么使用指示子系统失败的异常类型是很有意义的。
– 例如,ServletException,执行 servlet 的代码可能不想详细知道出了什么问题,但它肯定想知道 servlet 有问题。
在这里插入图片描述

强烈推荐这种包装技术。 它允许您在子系统中抛出高级异常而不会丢失原始故障的详细信息。

在这里插入图片描述
在这里插入图片描述

程序将执行 finally 子句的三种可能情况。
§ 案例 1:代码没有抛出异常。
– 程序首先执行 try 块中的所有代码。
– 然后,它执行finally 子句中的代码。
– 之后,继续执行第一个
finally 子句之后的语句。
– 换句话说,执行通过点 1、2、5、

在这里插入图片描述

情况 2:代码抛出一个异常,该异常在 catch 子句中被捕获。
– 程序执行 try 块中的所有代码,直到抛出异常为止。 跳过 try 块中的其余代码。 然后程序执行匹配的 catch 子句中的代码,然后是 finally 子句中的代码。
– 如果catch 子句没有抛出异常,则程序执行finally 子句之后的第一行。
– 执行通过第 1、3、4、5 和 6 点。
– 如果 catch 子句抛出异常,则异常被抛出回调用者,并且执行仅通过点 1、3 和 5。

在这里插入图片描述

Try-Catch-Finally:案例 3
§ 案例 3:代码抛出了一个没有被任何 catch 子句捕获的异常。
– 在这里,程序执行 try 块中的所有代码,直到抛出异常。
– 跳过 try 块中的剩余代码。
– 然后,执行finally子句中的代码,将异常抛回给该方法的调用者。
– 执行仅通过第 1 点和第 5 点。
在这里插入图片描述

在没有 Catch 的情况下使用 finally
§ 您可以使用 finally 子句而不使用 catch 子句。
§ 例如,考虑以下 try 语句:
§ 无论在 try 块中是否遇到异常,finally 子句中的 in.close() 语句都会执行。
§ 如果遇到异常,它会被重新抛出,并且必须在另一个 catch 子句中捕获
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • MethodC() 触发 ArithmeticException。 由于它不处理这个异常,它立即从调用堆栈中弹出。
    – MethodB() 也不处理此异常并从调用堆栈中弹出。 methodA() 和 main() 方法也是如此。
    – main() 方法传回 JVM,JVM 突然终止程序并打印调用堆栈跟踪

异常和调用堆栈
§ 当 Java 方法内部发生异常时,该方法会创建一个 Exception 对象并将该 Exception 对象传递给 JVM(即该方法“抛出”一个异常)。
§ Exception 对象包含异常的类型,以及发生异常时程序的状态。
§ JVM 负责寻找异常处理程序来处理异常对象。
– 它在调用堆栈中向后搜索,直到找到与该特定类 Exception 对象匹配的异常处理程序(在 Java 术语中,它被称为“捕获”异常)。
– 如果 JVM 在调用堆栈中的所有方法中都找不到匹配的异常处理程序,它将终止程序。

在这里插入图片描述

假设 methodD() 遇到异常情况并向 JVM 抛出 XxxException。
§ JVM 在调用堆栈中向后搜索匹配的异常处理程序。
§ 它找到具有 XxxException 处理程序的 methodA() 并将异常对象传递给处理程序。
– 请注意,methodC() 和 methodB() 需要在它们的方法签名中声明“throws XxxException”才能编译程序

在这里插入图片描述

分析堆栈跟踪元素
§ 堆栈跟踪是在程序执行的特定点上所有挂起的方法调用的列表。
§ 您几乎肯定见过堆栈跟踪列表——每当 Java 程序因未捕获的异常而终止时,它们就会显示出来。
§ 您可以通过调用 Throwable 类的 printStackTrace 方法访问堆栈跟踪的文本描述
在这里插入图片描述

一种更灵活的方法是 getStackTrace 方法,它生成一组 StackTraceElement 对象,您可以在程序中对其进行分析。
§ StackTraceElement 类具有获取执行代码行的文件名和行号以及类和方法名的方法。
§ toString 方法产生一个包含所有这些信息的格式化字符串。

四、断言

在这里插入图片描述
对错误最好的防御是通过设计使它们成为不可能。 最好的防御就是不要引入错误
– 静态检查:通过在编译时捕获它们来消除许多错误。
– 动态检查:Java 通过动态捕获它们使数组溢出错误成为不可能。 如果您尝试使用数组或列表边界之外的索引,则 Java 会自动产生错误。 —
未经检查的异常/运行时错误
– 不变性:不可变类型是其值一旦创建就永远不会改变的类型。
– 不可变值:通过 final,可以分配一次但永远不会重新分配。
– 不可变引用:通过final,这使得引用不可重新分配,但引用指向的对象可能是可变的或不可变的
在这里插入图片描述
当不满足前提条件时,此代码通过抛出 AssertionError 异常终止程序。 调用者错误的影响被阻止传播

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
断言通常有两个参数
– 一个布尔表达式,描述应该为真的假设
– 如果不是,则显示一条消息。
§ Java 语言有一个关键字 assert 有两种形式:
– 断言条件;
– 断言条件:消息;
– 如果布尔表达式的计算结果为 false,两个语句都会评估条件并抛出 AssertionError。
– 在第二个语句中,表达式被传递给 AssertionError 对象的构造函数并转换为消息字符串。 当断言失败时,描述会打印在错误消息中,因此它可用于向程序员提供有关失败原因的其他详细信息。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

断言可用于验证:
– Internal Invariants 内部不变量:断言某个值在某个约束范围内,例如断言 x > 0。
– Rep Invariants 表示不变量:断言对象的状态在约束范围内。 在方法执行之前或之后,类的每个实例必须为真吗? 类不变量通常通过私有布尔方法进行验证,例如 checkRep()。
– Control-Flow Invariants 控制流不变量:断言不会到达某个位置。 例如,switch-case 语句的 default 子句。
– 方法的前置条件方法的前置条件:当一个方法被调用时,什么必须为真? 通常用方法的参数或其对象的状态来表示。
– 方法的后置条件 方法的后置条件:一个方法成功完成后必须是什么?

在这里插入图片描述

如果条件语句或 switch 没有涵盖所有可能的情况,最好使用断言来阻止非法

但是这里不要使用 assert 语句,因为它可以关闭。 相反,在非法情况下抛出异常,以便始终进行检查:

在这里插入图片描述

断言内容:更多场景
§ 只输入变量的值不会被方法改变
§ 一个指针是非空的
§ 传递给方法的数组或其他容器可以包含至少 X 个数据元素
§ 一个表已被初始化为包含真实值
§ 当一个方法开始执行时(或当它完成时)一个容器是空的(或满的)
§ 高度优化的复杂方法的结果与较慢但编写清晰的程序的结果相匹配

在这里插入图片描述

避免将可执行代码放在断言中

在这里插入图片描述
在不同阶段关闭/打开断言

许多断言机制的设计使得断言只在测试和调试期间执行,并在程序发布给用户时关闭。
– 断言是一个很好的工具,可以保护您的代码免受错误的影响,但 Java 默认关闭它们!
Java关闭断断言,要记得打开(-ea)
§ 这种方法的优点是您可以编写非常昂贵的断言,否则会严重降低程序的性能。
断言非常影响运行时的性能
– 例如,使用二进制搜索搜索数组的过程要求对数组进行排序。
– 断言此要求需要扫描整个阵列,但是,将应该以对数时间运行的操作转换为需要线性时间的操作。
– 你应该愿意在测试期间支付这笔费用,因为它使调试更容易,但不会在程序发布给用户之后

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

您可以使用断言来测试非公共方法的前提条件,无论客户端对类做什么,您都认为该前提条件为真。

将断言用于前置/后置条件
§ 如果变量纬度、经度和海拔来自外部源,则应通过错误处理代码而不是断言来检查和处理无效值。
参数来自外部(不受自己控制),使用异常处理
§ 但是,如果变量来自可信的内部来源,并且例程的设计基于这些值将在其有效范围内的假设,则断言是合适的。
如果来自于自己所写的其他代码,可以使用断言来帮助发现错误(例如后置条件就需要)

在这里插入图片描述

是否应该断言前置/后置条件?
§ 您可以使用断言来测试非公共方法的前提条件,无论客户对类做什么,您都认为该前提条件为真

在这里插入图片描述

是否应该断言前置/后置条件?
§ 您可以在公共和非公共方法中使用断言测试后置条件。

在这里插入图片描述

五、防御性编程

防御性编程技术
§ 保护程序免受无效输入
§ 断言
§ 例外情况
§ 具体的错误处理技术
§ 路障
§ 调试辅助
§ 防御性编码的最佳形式首先不是插入错误。
§ 您可以将防御性编程与其他技术结合使用

在这里插入图片描述

路障和断言之间的关系
§ 路障的使用使断言和错误处理之间的区别一目了然。
– 路障外的例程应该使用错误处理,因为对数据做出任何假设是不安全的。
– 路障内的例程应该使用断言,因为传递给它们的数据应该在通过路障之前进行消毒。 如果路障内的程序之一检测到不良数据,
这是程序中的错误而不是数据中的错误。
§“隔离舱处理”外部的函数应使用异常,“隔离舱”内的函数应使用断言。
– 路障的使用还说明了在架构级别决定如何处理错误的价值。
– 决定哪些代码在路障内,哪些在路障外是架构级别的决定。
§ 代理设计模式?——隔离

六、SpotBug(静态分析)

Guess you like

Origin blog.csdn.net/m0_50906780/article/details/118465583