12 面向正确性与健壮性的软件构造

面向正确性与健壮性的软件构造

健壮性

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

面向健壮性的编程:处理未期望的行为和错误终止,即使终止执行,也 要准确/无歧义的向用户展示全面的错误信息。错误信息有助于进行debug。总是假定用户恶意、假定自己的代码可能失败,把用户想象成白痴,可能输入任何东西。封闭实现细节,限定用户的恶意行为。考虑极端情况,没有“不可能”。

正确性

程序按照spec加以执行的能力,是最重要的质量指标。

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

正确性倾向于直接报错,健壮性则倾向于容错。
在这里插入图片描述
安全软件和用户软件不同
在这里插入图片描述

Error类

在这里插入图片描述

描述Java运行时系统内部很少发生的系统错误和资源耗尽情况(例如VirtualMachineError、LinkageError)。

注意:不应该抛出这种类型的对象。

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

错误的种类:用户输入错误,设备错误,物理限制。无法解决!

一些典型错误

  1. VirtualMachineError:抛出表示Java虚拟机已损坏或已耗尽继续运行所需的资源。
  2. OutOfMemoryError:当Java虚拟机由于内存不足而无法分配对象,并且垃圾收集器无法提供更多内存时抛出。
  3. StackOverflowerError:由于应用程序递归太深而发生堆栈溢出时抛出。
  4. InternalError:抛出以指示JVM中发生了一些意外的内部错误。
  5. LinkageError:一个类对另一个类有某种依赖性;然而,后一类在前一类的编译之后发生了不可比拟的变化。
  6. NoClassDefFoundError:如果JVM或类加载器实例试图加载类的定义,但找不到定义,则引发

Exception类

在这里插入图片描述

描述由程序引起的错误(例如FileNotFoundException、IOException)。

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

异常:程序执行中的非正常事件,程序无法再按预想的流程执行。将错误信息传递给上层调用者,并报告“案发现场”的信息。是return之外的第二种退出途径。

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

Runtime异常

运行时异常:由程序员在代码里处理不当造成

从RuntimeException继承的异常包括以下问题:

-杂乱排绕

–越界数组访问

–空指针访问

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

其他异常

其他异常:由外部原因造成

–试图读取超过文件结尾的内容

–试图打开一个不存在的文件

–尝试为不表示现有类的字符串查找类对象

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

Checked异常:

必须捕获并处理异常,或者通过声明方法抛出异常来告诉编译器您无法处理它,然后使用方法的代码将必须处理该异常(如果无法处理该异常,则可以选择声明它抛出该异常)。编译器将检查我们是否完成了两件事情之一(catch或declare)。

必须捕获并指定错误处理器handler,否则编译无法通过。类似于编程语言中的static type checking

五个关键字 – try – catch – finally – throws – throw

throws:声明“本方法可能会发生XX异常

throw:抛出XX异常

try、catch、finally:捕获并处理XX异常

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

Unchecked异常:

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

Unchecked异常:(Error和Runtime异常)不需要在编译的时候用try…catch等机制处理。

注:可以不处理,编译没问题,但执行时出现就导致程序失败,代表程序中的潜在bug。类似于编程语言中的动态检查。

总结

  1. 如果客户端可以通过其他的方法恢复异常,那么采用checked exception;如果客户端对出现的这种异常无能为力,那么采用unchecked exception;异常出现的时候,要做一些试图恢复它的动作而不要仅仅的打印它的信息。

  2. 尽量使用unchecked exception来处理编程错误,如果client端对某种异常无能为力,可以把它转变 为一个unchecked exception,程序被挂起并返回客户端异常信息。Checked exception应该让客户端从中得到丰富的信息。 错误可预料,但无法预防,但可以有手段从中恢复,此时使用checked exception。要想让代码更加易读,倾向于用unchecked exception来处理程序中的错误。可预料但不可预防,脱离了你的程序的控制范围

  3. 对特殊结果(即预期情况)使用Checked exception,使用unchecked exception来发出错误信号(意外故障)。应该仅使用unchecked exception来表示意外故障(即bug),或者如果您希望客户机通常会编写代码来确保异常不会发生,因为有一种方便且廉价的方法来避免异常;否则应使用checked exception。

Checked异常的处理机制

(1)声明:throws抛出异常

“异常”也是方法和client端之间spec的一部分,在post-condition中刻画。标头更改以反映方法可以抛出的checked异常。

一个方法也可以抛出多个异常。Error 和 unchecked exceptions无法抛出。

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

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

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

(2)抛出:throw异常

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

throw异常的方法:

找到一个能表达错误的Exception类/或者构造一个新的Exception类,构造Exception类的实例,将错误信息写入,抛出它。一旦抛出异常,方法不会再将控制权返回给调用它的client,因此也无需考虑返回错误代码。

(3)捕获与处理

异常发生后,如果找不到处理器, 就终止执行程序,在控制台打印出stack trace。

也可以不在本方法内处理, 而是传递给调用方,由client处理。

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

尽量在自己这里处理,实在不行就往上传——要承担责任!但有些时候自己不知道如何处理,那么提醒上家,由client自己处理。

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

(4)清理现场、释放资源

当代码抛出异常时,它将停止处理方法中的剩余代码并退出该方法。如果该方法获取了一些只有该方法知道的资源(文件、数据库连接等),并且该资源必须被清除,则这是一个问题。如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理。

一种解决方案是捕获并重新引用所有异常。但是这个解决方案很乏味,因为您需要清理正常代码和异常代码中两个位置的资源分配。

使用finally解决此问题。

Try-Catch-Finally:无论是否捕获到异常,finally子句中的代码都会执行。

自定义异常类

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

要定义checked异常,创建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); }
}
有时会出现不希望强制每个方法在其throws子句中声明异常实现的情况。在这种情况下,您可以创建一个扩展java.lang.RuntimeException的unchecked异常。方法可以抛出或传播fooRuntimeException异常而不声明它。

public class FooRuntimeException extends RuntimeException {
    ...
}
 
public void calculate(int i) {
    if (i < 0) {
        throw new FooRuntimeException("i < 0: " + i);
    }
}

断言的作用、应用场合

最好的防御:不要引入bug

如果无法避免,尝试着将bug限制在最小的范围内,限定在一个方法内部,不扩散。尽快失败,就容易发现、越早修复

断言:当不满足前提条件时,此代码通过抛出AssertionError异常终止程序。

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

–真正的程序很少没有bug。

–防御性编程提供了一种减轻bug影响的方法,即使不知道它们在哪里。

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

每个断言都包含一个布尔表达式,该表达式在程序执行时为真。

如果不是真的,JVM将抛出AssertionError。此错误表示您有一个需要修复的无效假设。增强程序员对代码质量的信心:对代码所做的假设都保持正确。断言即是对代码中程序员所做假设的文档化,也不会影响运行时性能。

使用场景:

  1. 内部不变量: 断言某个值在某个约束内,例如,断言x>0。

  2. 表示不变量: 断言对象的状态在约束内。在方法执行之前或之后,类的每个实例必须是什么样的?类不变量通常通过私有布尔方法进行验证,例如checkRep()。

  3. 控制流不变量: 断言不会到达某个位置。例如,switch case语句的default子句。

  4. 方法的前置条件: 调用方法时什么必须为真?通常用方法的参数或对象的状态来表示。

  5. 方法的后置条件: 在一个方法成功完成后,什么是真的?

  6. 仅输入变量的值不会被方法更改

  7. 指针不为空

  8. 传入方法的数组或其他容器可以包含至少X个数据元素

  9. 表已初始化为包含实值

  10. 当方法开始执行(或完成)时,容器为空(或满)

  11. 高度优化、复杂方法的结果与较慢但编写清晰的例程的结果相匹配

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

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

比如:文件/网络/用户输入等

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

许多断言机制的设计使断言仅在测试和调试期间执行,并在程序发布给用户时关闭。断言是一个很好的工具,可以保护您的代码不受bug的影响,但是Java在默认情况下关闭了断言!要记得打开(-ea)

断言通常包括程序的正确性问题。使用断言处理 “绝不应该发生”的情况

–如果针对异常情况触发断言,纠正措施不仅仅是优雅地处理错误,而是更改程序的源代码、重新编译并发布新版本的软件。

Exception通常包括程序的健壮性问题。使用异常来处理你“预料到可以发生”的不正常情况

–如果错误处理代码用于处理异常情况,则错误处理将使程序能够优雅地响应错误。

防御式编程的基本思路

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

防御式编程技术:

  1. 保护程序不受无效输入的影响
  2. 断言
  3. 异常
  4. 特定错误处理技术
  5. 路障
  6. 调试辅助工具

防御式编码的最佳形式不是首先插入错误,可以将防御编程与其他技术结合使用。

保护程序不受无效输入的影响:对来自外部的数据源要仔细检查,例如:文件、网络数据、用户输入等;对每个函数的输入参数合法性要做仔细检查,并决定如何处理非法输入

设置路障:

  1. 类的public方法接收到的外部数据都应被认为是dirty的,需要处理干净再传递到private方法——隔离舱。“隔离舱”外部的函数应使用异常处理,“隔离舱”内的函数应使用 断言。

  2. 操作间技术。数据在进入操作间之前要消毒。操作间里的任何东西都是安全的。关键的设计决策是决定在错左键放什么,不放什么,门放在哪里,哪些例行程序被认为在安全区内,哪些在外面,哪些对数据进行了消毒。最简单的方法通常是在外部数据到达时对其进行消毒,但数据通常需要在多个级别进行消毒,因此有时需要进行多个级别的消毒。
    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值