软件构造课程总结(十二)——面向正确性与健壮性的软件构造

什么是健壮性和正确性

健壮性

健壮性:系统在不正常输入或不正常外部环境下仍能够表现正常的程度

面向健壮性的编程

  • 处理未期望的行为和错误终止
  • 即使终止执行,也要准确/无歧义的向用户展示全面的错误信息
  • 错误信息有助于进行debug

健壮性原则(Postel定律)

  1. 总是假定用户恶意、假定自己的代码可能失败。

  2. 把用户想象成白痴,可能输入任何东西。

    因此,程序员返回给用户一个明确的、直观的错误消息,不需要查找错误代码。错误消息应尽量尽可能准确,而不误导用户,从而使问题能够轻松解决。

  3. 对别人宽容点,对自己狠一点(Postel定律)

    在你的工作上要保守;在接受别人的东西时要自由。

做的保守,接受别人的自由 —— Postel

  1. 封闭实现细节,限定用户的恶意行为。

    用户不应该访问库、数据结构或指向数据结构的指针。这些信息应该对用户隐藏,这样用户就不会意外地修改它们并在代码中引入错误。当这些接口构建正确时,用户使用它们而不会发现漏洞来修改接口。因此,用户只关注自己自己的代码。

  2. 考虑极端情况,没有“不可能”

    不可能的情况被认为是极不可能的情况。开发人员考虑如何处理极不太可能的情况,并实现相应的处理

Correctness 正确性

正确性:程序按照spec加以执行的能力,是最重要的质量指标!

  • 正确性:永不给用户错误的结果
  • 健壮性:尽可能保持软件运行而不是总是退出

正确性倾向于直接报错(error),健壮性则倾向于容错(fault-tolerance)。

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

健壮性与正确性比较:

健壮性:让用户变得更容易:出错也可以容忍,程序内部已有容错机制

正确性:让开发者变得更容易:用户输入错误,直接结束。(不满足precondition的调用)

对外的接口,倾向于健壮;对内的实现,倾向于正确。

安全关键应用程序倾向于正确性。不返回任何结果总比返回一个错误的结果要好。

消费者应用程序倾向于健壮性。任何结果通常都比软件关闭要好。

可靠性。系统在规定条件下在任何需要下执行其所需功能的能力——具有很长的平均故障间隔时间。

可靠性 = 健壮性 + 正确性

提高健壮性和正确性的步骤:

  • 步骤0:使用断言、健壮性和正确性目标的编程代码、防御性编程、代码审查、正式验证等
  • 步骤1:观察故障症状(内存转储、堆栈跟踪、执行日志、测试)
  • 步骤2:识别潜在的错误(错误本地化、调试)
  • 步骤3:修复错误(代码修订)

如何衡量健壮性和正确性

外部观察角度:平均失效间隔时间

平均失效间隔时间(MTBF)是系统在运行过程中发生固有故障之间的预测运行时间。它是计算为一个系统故障之间的算术平均时间。

MTBF的定义取决于对系统故障的定义。

对于复杂的、可修复的系统,故障被认为是那些超出设计条件,使系统停止服务并进入维修状态的故障。

故障可以保持或保持在未修复的状态下,并且不会使系统停止服务,在此定义下不被视为故障。

Mean time between failures(MTBF)描述了一个可修复系统的两次故障之间的预期时间,而mean time to failure(MTTF)表示一个不可修复系统的预期故障间隔时间。

内部观察角度:残余缺陷率

每千行代码中遗留的bug的数量。

  • 1 - 10个缺陷/kloc:典型的行业软件。
  • 0.1 - 1个缺陷/kloc:高质量的验证。Java库可能会达到这种级别的正确性。
  • 0.01 - 0.1个缺陷/kloc:最佳的安全关键验证。NASA和像Praxis这样的公司可以达到这个水平。

Java中的错误和异常

Java中的“异常”

所有异常对象的基类都是 java.lang.Throwable,连同它的两个子类 java.lang.Exception 和 java.lang.Error。

请添加图片描述

错误和异常

Error类描述了Java运行时系统内部很少发生的内部系统错误和资源耗尽情况(例如,虚拟机器错误、链接错误)。

  • 您不应该抛出这种类型的对象。如果发生这样的内部错误,除了通知用户和试图优雅地终止程序之外,你几乎做不到什么。

内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束

异常类描述程序引起的错误(例如,文件查找异常,IO异常)。

  • 这些错误可以被您的程序捕获和处理(例如,执行一个替代操作或通过关闭所有文件、网络和数据库连接来进行优雅的退出)。

异常:自己程序导致的问题,可以捕获、可以处理

错误的种类:

  • 用户输入错误
    • 除了不可避免的拼写错误外,一些用户还喜欢开辟自己的路线,而不是遵循方向。
  • 设备错误
    • 硬件并不总是能做你想做的事情。
    • 打印机可能已关闭。
    • 一个网页可能暂时不可用。
    • 设备通常会在任务执行过程中失败。
  • 物理限制
    • 磁盘空间不足
    • 耗尽可用的内存

在大多数时候, 程序员不需要实例化Error。

一些典型的错误:

  • 虚拟机器错误:抛出以表示Java虚拟机已经损坏或耗尽了继续运行所需的资源。
    • 内存错误:当Java虚拟机无法分配对象,因为内存不足,垃圾收集器无法提供其他内存时抛出。
    • 堆栈溢出错误:当由于应用程序递归得太深而发生堆栈溢出时抛出。
    • 内部错误:抛出以指示在JVM中发生了一些意外的内部错误。
  • 链接错误:一个类对另一个类有一些依赖性;但是,后一个类在编译后发生了不兼容的更改。
    • 类定义未发现的错误:如果JVM或类加载器实例试图在类的定义中加载,但没有找到定义,则抛出。

异常情况处理

什么是异常

异常是程序执行中的非正常事件,程序无法再按预想的流程执行。

将错误信息传递给上层 调用者,并报告"案发现场"的信息

Java允许如果不能以正常方式完成其任务,则允许每个方法提供一个替代的退出路径。(return之外的第二种退出途径)

  • 该方法抛出一个封装错误信息的对象。
  • 该方法会立即退出,并且不返回任何值。
  • 此外,不会在调用方法的代码处恢复执行;相反,异常处理机制开始搜索可以处理此特定错误条件的异常处理程序。若找不 到异常处理程序,整个系统完全退出。
异常分类

在Java编程语言中,异常对象总是从可抛出对象中派生的类的实例。

在执行Java编程时,请重点关注异常层次结构。

异常层次结构还分为两个分支:

  • 运行时异常,由程序员在代码里处理不当造成
  • 其他异常,由外部原因造成

从运行时异常继承的异常例如:一个糟糕的强制转换,一个越界的数组访问,一个空指针访问…

不从运行时异常继承的异常包括:试图读取超过一个文件的末尾,试图打开一个不存在的文件,试图为一个不表示现有类的字符串找到一个类对象…

运行时异常,是程序源代码中引入的故障所造成的,如果在代码中提前进行验证,这些故障就可以避免。

非运行时异常,是程序员无法完全控制的外在问题所导致的。即使在代码中提前加以验证,也无法完全避免失效发生。

Checked 和 unchecked 的异常

发生异常时,您必须捕获并处理异常,或者通过声明您的方法抛出异常来告诉编译器您无法处理异常。然后,客户端将必须处理这个异常(或者继续抛出)。编译器可帮助检查你的程序是否已抛出或处理了可能的异常。

**编译器不能检查到错误和运行时异常。**错误表示发生在应用程序之外的条件,如系统崩溃。运行时异常通常由应用程序逻辑中的故障发生。在这些情况下,你不能做任何事情,但必须重写你的程序代码。所以这些不会被编译器检查。这些运行时异常将在开发和测试过程中被发现。然后,我们必须重新配置我们的代码,以删除这些错误。

Unchecked exception:编程错误,其他不可恢复的失败(错误+运行时异常)

从RuntimeException派生出子类型。

程序编译不需要任何操作,但未捕获的异常将导致程序失败。Unchecked exception不需要在编译的时候用try…catch等机制处理。

Checked exception:每个调用者都应该知道和处理的错误

从Exception派生出子类型。

必须捕获或传播,否则程序将无法编译(编译器检查是否为所有检查的异常提供异常处理程序)

常见的未经检查的异常类

ArrayIndexOutOfBoundsException

当代码使用在数组边界之外的数组索引时,由JVM抛出。

NullPointerException

当代码尝试使用需要对象引用的空引用时,JVM抛出。

NumberFormatException

当试图将一个字符串转换为数字类型,但该字符串没有适当的格式时,以编程方式抛出(例如,通过Integer.parseInt())。

ClassCastException

当试图强制转换对象引用失败时,JVM抛出。

在异常处理中使用了5个关键字

  • try
  • catch
  • finally
  • throws
  • throw

Java的异常处理包括三个操作:

  • 声明“本方法可能会发生XX异常”
  • 抛出XX异常
  • 捕获并处理XX异常

Unchecked异常也可以使用throws声明或try/catch进行捕获,但大多数时候是不需要的,也不应该这么做——掩耳盗铃, 对发现的编程错误充耳不闻

当要决定是采用checked exception还是unchecked exception的时候,问一个问题:“如果这种异常一旦抛出,client会做怎样的补救?

当异常发生时客户端的反应异常类型
客户端代码不能做任何事情使其成为一个 unchecked exception
客户端代码将根据异常中的信息采取一些有用的恢复操作使其成为一个checked exception

异常出现的时候,要做一些试图恢复它的动作而不要仅仅的打印它的信息。

**尽量使用unchecked exception来处理编程错误:**因为unchecked exception不用使客户端代码显式的处理它们,它们自己会在出现的地方挂起程序并打印出异常信息。

充分利用Java API中提供的丰富unchecked exception,如NullPointerException , IllegalArgumentException和IllegalStateException等,使用这些标准的异常类而不需亲自创建新的异常类,使代码易于理解并避免过多消耗内存。

如果client端对某种异常无能为力,可以使用unchecked exception,程序被挂起并返回客户端异常信息。

如果它们没有关于客户端代码的有用信息,请尽量不要创建新的自定义异常。client应该从checked exception中获取更有价值的信息(案发现场具体是什么样子),利用异常返回的信息来明确操作失败的原因。如果client仅仅想看到异常信息,可以简单抛出一个unchecked exception:

throw new RuntimeException("Username already taken");

错误可预料,但无法预防,但可以有手段从中恢复,此时使用 checked exception。

如果做不到这一点,则使用unchecked exception。

不可预防:脱离了你的程序的控制范围

可合理的恢复

如果读文件的时候发现文件不存在了,可以让用户选择其他文件;但是如果调用某方法时传入了错误的参数,则无论如何都无法在不中止执行的前提下进行恢复。

除非您抛出的异常满足上述所有条件,否则它应该使用Unchecked Exception。

您应该使用一个unchecked exception来表示一个意外的失败(即bug),或者如果您期望客户端通常会编写代码来确保异常不会发生,因为有一种方便和廉价的方法来避免异常;否则您应该使用checked exception。

请添加图片描述

通过抛出声明已检查的异常

Java方法在遇到它无法处理的情况时可以抛出异常。“异常”也是方法和client端之间spec的一部分,在post-condition中刻画

您声明方法可以抛出异常的地方是方法的头;头会更改以反映方法可以抛出的已检查的异常。

public FileInputStream(String name) throws FileNotFoundException

如何在规范中声明异常

异常总是用Javadoc @throws 子句记录,指定该特殊结果发生的条件。

Java还可能要求使用抛出声明在方法签名中包含异常

程序员必 须在方法的spec中明确写清本方法会抛出的所有checked exception, 以便于调用该方法的client加以处理

用于表示意外失败的Unchecked exceptions——客户端或实现中的错误不是方法的后置条件的一部分,因此它们不应该出现在@throws 或 throws中。

如果一个方法可能抛出多个已选中的异常类型,则必须在标头中列出所有异常类。

public Image loadImage(String s) throws FileNotFoundException, EOFException
{
    ...
}

你的方法应该throws什么异常?

  • 你所调用的其他函数抛出了一个checked exception——从其他函数传来的异常
  • 当前方法检测到错误并使用throws抛出了一个checked exception——你自己造出的异常

此时需要告知你的client需要处理这些异常,如果没有handler来处理被抛出的checked exception,程序就终止执行。

不需要发布内部Java错误-从错误继承的异常。

您不应该发布从运行时异常继承的未经检查的异常。(修复而不是抛出)

考虑子类型多态

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

LSP:目标是子类型多态:客户端可用统一的方式处理不同类型的对象,子类型可替代父类型

LSP是一种子类型关系的一种特殊定义,称为强行为子类型化。

LSP依赖于以下限制:

  • 先决条件不能在一个子类型中得到加强。
  • 后置条件不能在一个子类型中被减弱。
  • 超类型的不变量必须保同样在一个子类型中符合。
  • 子类型方法参数:逆变
  • 子类型方法的返回值:协变
  • 子类型的方法不应该抛出新的异常,除非这些异常本身是超类型的方法抛出的异常的子类型。
如何抛出一个异常

假设您有一个方法,读取Data,它正在读取一个文件。在你的代码中发生了一些错误。您可能会认为这种情况非常异常,因此您想要抛出一个异常EOF异常,并述“在输入期间意外到达EOF的信号”。

throw new EOFException();
EOFException e = new EOFException();
throw e;

EOFException有第二个构造函数接受字符串参数,可以利用Exception的构造函数,将发生错误的现场信息充分的传递给client。

String gripe = "Content-length: " + len + ", Received: " + n;
throw new EOFException(gripe);

如果现有的异常类适合您,则抛出异常很容易:

  • 找到一个能表达错误的Exception类/或者构造一个新的Exception类
  • 构造Exception类的实例,将错误信息写入
  • 抛出它

一旦抛出异常,方法不会再将控制权返回给调用它的client,因此也无需考虑返回错误代码。

创建异常类

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

只需从Exception派生它,或者从Exception的子类派生。

通常同时给出默认构造函数和包含详细消息的构造函数。

Throwable超类的toString方法返回一个包含该详细消息的字符串,这对调试很方便。

要定义 checked exception,您可以创建java.lang.Exception的子类(或子类的层次结构):

public class FooException extends Exception {
	public FooException() { super(); }
	public FooException(String message) { super(message); }
	public FooException(String message, Throwable cause) { 
	super(message, cause); 
	}
	public FooException(Throwable cause) { super(cause); }
}

可能抛出或传播此异常的方法必须声明它:

public void calculate(int i) throws FooException, IOException;

有时会出现以下情况,即您不想强制每个方法在其抛出子句中声明异常实现。在这种情况下,您可以创建一个派生自 java.lang.RuntimeException 的异常。

方法可以抛出或传播脚运行时异常异常,而不声明它。

异常类的内部可以加入更多逻辑存储“现场信息”。抛出异常的时候,将现场信息记入异常在异常处理时,利用这些信息给用户更有价值的帮助。

捕获异常

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

要捕获异常,请设置一个try/catch块:

try {
	...
} 
catch (ExceptionType e) {
	...//handler for this type
}

如果try块中的任何代码抛出了catch子句中指定的类的异常,那么

  • 该程序将跳过try块中的其余代码
  • 该程序在catch子句中执行处理程序代码。

如果try块中的任何代码都没有抛出异常,则程序将跳过catch子句。

如果方法中的任何代码抛出catch子句中命名的类型以外的类型的异常,此方法将立即退出。

所以需要为该类型提供所有可能异常的catch子句

处理异常的另一个选择是:什么都不做,然后简单地将异常传递给调用者。

如果我们采用这种方法,那么我们就必须声明,该方法可能会抛出一个异常。

编译器严格地强制执行抛出说明符。如果调用的方法会抛出checked exception,则必须处理它或将其传递。

一般来说,您应该捕获那些您知道如何处理的异常,并传播那些您不知道如何处理的异常。在传播异常时,必须添加抛出说明符,以提醒调用者可能会抛出异常。

如果父类型中的方法没有抛出异常,那么子类型中的方法必须捕获所有的checked exception( 子类型方法中不能抛出比父类型方法更多的异常)

异常对象可能包含有关异常性质的信息。要了解更多关于该对象的信息,请尝试e.getMessage()来获取详细的错误消息(如果有的话)

使用e.getClass().getName()来获取异常对象的实际类型。

您可以在一个尝试块中捕获多个异常类型,并以不同的方式处理每种类型。为每种类型使用一个单独的catct子句,如下示例所示:

try {
	...//code that might throw exceptions
}
catch (FileNotFoundException e) {
	...//emergency action for missing files
}
catch (UnknownHostException e) {
	...//emergency action for unknown hosts
}
catch (IOException e) {
	...//emergency action for all other I/O problems
}
重新抛出和链接异常

本来catch语句下面是用来做异常处理的,但也可以在catch里抛出异常。

通常,您会在要更改异常类型时执行此操作。如果您构建了一个其他程序员使用的子系统,那么使用一种表示子系统发生故障的异常类型是很有意义的。

这么做的目的是:更改exception的类型,更方便client端获取错误信息并处理

try {
	...//access the database
}
catch (SQLException e) {
	throw new ServletException("database error: " + e.getMessage());
}

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

try {
	...//access the database
}
catch (SQLException e) {
	Throwable se = new ServletException("database error");
	se.initCause(e);
	throw se;
}

当捕获异常时,可以检索原始异常

Throwable e = se.getCause();

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

finally 语句块

当异常抛出时,方法中正常执行的代码被终止。如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理。

finally子句中的代码执行无论是否捕获了异常。在下面的示例中,该程序将在所有情况下都关闭该文件:

InputStream in = new FileInputStream(. . .);
try {
	...//code that might throw exceptions
}
catch (IOException e) {
	...//show error message
} 
finally {
	in.close();
}

您可以使用finally子句而没有catch子句。

分析堆栈跟踪

一个典型的应用程序涉及到许多级别的方法调用,它由一个所谓的方法调用堆栈来管理。

堆栈是一个最后一进到先出的队列。

假设方法C()执行了一个“除以0”的操作,这会触发一个算术异常。

异常消息清楚地显示了具有相关语句行号的方法调用堆栈跟踪:

请添加图片描述

过程:

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

当在Java方法中发生异常时,该方法将创建一个异常对象,并将该异常对象传递给JVM(即,该方法“抛出”一个异常)。

异常对象包含异常的类型,以及发生异常时的程序的状态。

JVM负责查找要处理异常对象的异常处理程序。

假设methodD()遇到一个异常情况,并向JVM抛出一个XxxException。

JVM在调用堆栈中向后搜索匹配的异常处理程序。

它找到具有XxxException异常处理程序的方法 methodA(),并将异常对象传递给处理程序。

  • 请注意,为了编译程序,需要methodC()methodB() 在其方法签名中声明“throws XxxException”才能编译程序。

分析堆栈跟踪

堆栈跟踪是在程序执行中特定点上所有挂起的方法调用的列表。您肯定见到过堆栈跟踪列表——当Java程序以未捕获的异常终止时,就会显示这些列表。您可以通过调用 Throwable类的printStackTrace方法来访问堆栈跟踪的文本描述。

Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();

一种更灵活的方法是getStackTrace方法,该方法生成一个StackTraceElement数组,您可以在程序中进行分析。

Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames)
	analyze frame

StackTraceElement类有一些方法来获取代码的执行行的文件名和行号,以及类和方法名。toString方法将生成一个包含所有这些信息的格式化字符串。

断言

  • 最好的防御就是不要引入bug

    • 静态检查:通过在编译时捕获许多bug来消除它们。

    • 动态检查: Java通过动态捕获数组溢出错误,使它们不可能实现。如果您尝试在数组或列表的边界之外使用索引,那么Java会自动产生一个错误。

      unchecked exception / runtime error

    • 不变性:不可变类型是其值在创建后永远不会改变的类型。

    • 不可变值:通过final,可以分配一次,但永远不会重新分配。

    • 不可变引用:通过final,这使引用不可重新分配,但引用指向的对象可能是可变的或不可变的。

  • 如果无法避免,尝试着将bug限制在最小的范围内

  • 限定在一个方法内部,不扩散,易于发现错误

  • 尽快失败,就容易发现、越早修复

Assertions断言:当前提条件不满足时,此代码通过抛出一个断言错误异常来终止程序。调用者的错误的影响无法传播。快速故障,避免扩散。

检查前置条件是防御式编程的一种典型形式

  • 真正的程序很少是没有漏洞的。
  • 防御性编程提供了一种方法来减轻bug的影响,即使你不知道它们在哪里。
什么是断言和为什么使用断言

断言:在开发阶段的代码中嵌入,检验某些“假设”是否成立。若成立,表明程序运行正常,否则表明存在错误。

每个断言都包含一个布尔表达式,您认为在程序执行时为成立。

  • 如果它不是真的,JVM将抛出一个断言错误。
  • 出现AssertionError,意味着内部某些假设被违反了
  • 该断言确认了您对程序行为的假设,增加了您对程序没有错误的信心。

断言比使用if-else语句要好得多,因为它可以作为关于您的假设的适当文档,并且它在生产环境中不承担性能责任。(在实际使用时,assertion都会被disabled)

一个断言通常需要两个参数:

  • 一个描述了应该是正确的假设的布尔表达式
  • 如果它不是正确的,则会显示一条消息。

Java语言有一个具有两种形式的关键字断言:

  • assert [condition];

  • assert [condition] : [message];

这两个语句都计算条件,如果布尔表达式的计算结果为false,则抛出一个断言错误。

在第二个语句中,表达式被传递到断言错误对象的构造函数,并转换为消息字符串。当断言失败时,描述将打印在错误消息中,因此它可以用来向程序员提供有关故障原因的其他细节。

什么需要去断言,什么不需要

断言可用于验证:

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

前提条件:方法参数要求;后置条件:方法返回值要求。

控制流程:覆盖所有可能。如果条件语句或转换不涵盖所有可能的情况,那么最好使用断言来阻止非法情况。但是不要在这里使用断言语句,因为程序会被关闭。相反,在非法案例中抛出一个异常。

使用异常的更多场景:

  • 方法不会更改仅输入变量的值
  • 指针是非空的
  • 数组或其他容器传递到一个方法可以包含至少X数量的数据元素
  • 表已经初始化为包含真实值
  • 当方法开始执行时(或当方法完成时),容器为空(或满)
  • 来自高度优化、复杂的方法的结果与来自较慢但编写清晰的例程的结果相匹配

什么时候使用断言:

  • 断言主要用于开发阶段,避免引入和帮助发现bug
  • 实际运行阶段,不再使用断言,避免降低性能

使用断言的主要目的是为了在开发阶段调试程序,尽快避免错误

  • 当你编写代码时使用断言,而不是事后。当您在编写代码时,您已经记住了不变量。
  • 如果你推迟写断言,你就容易忘记或者忽略一些不变量。

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

  • 由于断言可能被禁用,所以程序的正确性不应该取决于是否执行断言表达式。

  • 特别是,断言的表达式不应该有副作用。例如,如果您想断言从列表中删除的元素实际上是在列表中找到的,请不要这样写:

     assert list.remove(x);
    
  • 如果禁用了断言,则将跳过整个表达式,并且x永远不会从列表中删除。请这样写:

    boolean found = list.remove(x); 
    assert found;
    

程序之外的事,不受你控制,不要乱断言

  • 文件/网络/用户输入等
  • 断言只是检查程序的内部状态是否符合规约
  • 断言一旦false,程序就停止执行
  • 你的代码无法保证不出现此类外部错误
  • 外部错误要使用Exception机制去处理

许多断言机制被设计为仅在测试和调试期间执行断言,并在程序发布给用户时关闭。

这种方法的优点是,您可以编写非常昂贵的断言,否则将严重降低程序的性能。

默认情况下,将禁用断言,所以需要启用断言(根据IDE)

使用断言的指南
  • Assertions通常关系到程序的正确性问题
    • 如果因异常情况而触发断言,纠正操作不仅仅是优雅地处理错误——纠正操作是更改程序的源代码、重新编译并发布软件的新版本。
  • Exceptions通常关系到程序的健壮性问题。
    • 如果错误处理代码用于处理异常情况,则错误处理将使程序能够优雅地响应该错误。
  • 断言在大型、复杂的程序和高可靠性程序中特别有用。
    • 它们使程序员能够更快地清除不匹配的接口假设、代码时出现的错误等等。

Assertion vs. Exception

  • 使用异常来处理你“预料到可以发生”的不正常情况
  • 使用断言处理“绝不应该发生”的情况

另一种观点是:不要在公共方法中使用断言来检查参数。

参数检查通常是方法的已发布规范(或契约)的一部分,无论是否启用或禁用断言,都必须遵守这些规范。

使用断言进行参数检查的另一个问题是,错误的参数应该会导致适当的运行时异常(如非法指针异常、索引超出边界异常或零指针异常)。使用断言如果失败将不会抛出适当的异常,因此应该在程序中抛出Runtime的异常。

如果参数来自于外部(不受自己控制),使用异常处理;如果来自于自己所写的其他代码,可以使用断言来帮助发现错误(例如postcondition就需要)。

您可以使用断言来测试非公共方法的前提条件,即您相信无论客户端如何处理该类,这些前提条件都将是正确的;您可以在公共方法和非公共方法中使用断言来测试后置条件。

断言和异常处理代码都可以用来处理相同的错误。

开发阶段用断言尽可能消除bugs,在发行版本里用异常处理机制处理漏掉的错误

防御性编程

防御编程是一种防御设计形式,旨在确保一个软件在不可预见的情况下的持续功能。防御性编程实践通常在需要高可用性、安全性或安全性的地方使用。

这个想法可以被看作是减少或消除了墨菲定律发挥作用的前景。

这个想法是基于防御性驾驶的

  • 你采用的思维模式是,你永远不会确定其他司机会做什么。
  • 这样,你要确保如果他们做了危险的事情,你不会受伤。
  • 你要负责保护自己,即使这可能是其他司机的错。

用来进行防御性编程的技术:

  • 保护程序免受无效的输入
  • Assertions
  • Exceptions
  • 特定的错误处理技术
  • Barricade 设置路障
  • 调试辅助工具

防御性编码的最好形式是首先就不插入错误。你可以使用防御性编程与其他技术相结合。

保护程序不受无效输入的影响

Garbage in, garbage out

这个表达式本质上是软件开发版本的警告购买者:让用户小心。

对于生产软件,垃圾输入,垃圾输出还不够好。

一个好的程序从不输出垃圾,不管它接受了什么。

  • “垃圾输入,没有输出”
  • “垃圾输入,错误消息输出”
  • “不允许垃圾输入”

对来自外部的数据源要仔细检查,例如:文件、网络数据、用户输入等

当从文件、用户、网络或其他外部接口获取数据时,请检查以确保数据是否在允许的范围内。

对每个函数的输入参数合法性要做仔细检查,并决定如何处理非法输入

检查输入参数的值本质上与检查来自外部源的数据相同,只是数据来自另一个代码,而不是来自外部接口。

Barricade 设置路障

路障是一种损害控制策略。

  • 这样做的原因类似于船体上的隔离隔间和建筑物里的防火墙。
  • 用于防御性编程的一种方法是指定某些接口作为“安全”区域的边界。
  • 检查跨越安全区域边界的数据的有效性,并在数据无效时做出明智的响应。

定义软件中一些使用脏数据的部分和一些使用干净数据的部分是一种有效的方法,可以减轻大多数代码检查坏数据的责任。

**类的公共方法假设数据是不安全的,它们负责检查数据并对其进行清理。**类的public方法接收到的外部数据都应被认为是dirty的,需要处理干净再传递到private方法——隔离舱

一旦数据被类的公共方法所接受,类的私有方法就可以假设数据是安全的。

另一种方法是作为一个操作间的技术。

  • 数据在获准进入操作间之前要进行消毒。任何放在操作间里的东西都被认为是安全的。

  • 关键的设计决策是决定在操作间放什么,放什么,把门放在哪里;哪些常规被认为是在安全区内,哪些在外面,哪些消毒数据。

  • 最简单的方法通常是在外部数据到达时进行消毒,但数据通常需要在多个级别上进行消毒,因此有时需要多个级别的灭菌

路障与断言之间的关系

路障的使用区分了断言和错误处理。

  • 在路障之外的:例程应该使用错误处理,因为对数据做出任何假设都是不安全的。
  • 在路障内的:例程应该使用断言,因为传递给它们的数据应该在通过路障之前被清理。如果路障内的一个例程检测到坏数据,这是程序中的一个错误,而不是数据中的一个错误。

“隔离舱”外部的函数应使用异常处理,“隔离舱”内的函数应使用断言。

  • 路障的使用还说明了在架构级别上决定如何处理错误的价值。

  • 决定哪些代码在内部,哪些代码在路障之外是一个架构级别的决策

代理设计模式?——隔离思想

SpotBugs工具

早期版本:FindBugs

FindBugs是一个使用静态分析来查找Java代码中的bug的程序。它使用Java字节码进行操作。

潜在的错误被分为四类: scariest, scary, troubling 和 of concern.这是对开发人员关于它们可能产生的影响或严重性的一个提示。

它是作为一个独立的GUI应用程序发布的,但也有可用于Eclipse、Gradle、Maven和Jenkins的插件。

SpotBugs是一个使用静态分析在Java代码中查找bug的程序。它是FindBugs的精神继承者,从它离开的社区的支持继续下去。

它检查了400多个bug模式。

它可以独立使用,并通过几个集成来使用,包括: Ant、Maven、Gradle和Eclipse。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梚辰

感谢支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值