第十二章 面向正确性与健壮性的软件构造

第十二章 面向正确性与健壮性的软件构造

健壮性(Robustness)和正确性(correctness)

【健壮性】

  • 定义:系统在不正常输入不正常外部环境下仍能够表现正常的程度
  • 面向健壮性编程:
    • 处理未期望的行为和错误终止
    • 即使终止执行,也要准确/无歧义地向用户展示全面的错误信息
    • 错误信息有助于进行debug
  • 健壮性原则:
    • Paranoia (偏执狂):总是假定用户恶意、假定自己的代码可能失败
    • 把用户想象成白痴,可能输入任何东西(返回给用户的错误提示信息要详细、准确、无歧义)
    • 对别人宽容点,对自己狠一点(对自己的代码要保守,对用户的行为要开放)
  • 面向健壮性编程的原则:
    • 封闭实现细节,限定用户的恶意行为
      • 这些信息应该对用户隐藏,这样用户就不会不小心修改它们并在代码中引入bug。
      • 当这些接口被正确构建后,用户使用它们时不会发现漏洞来修改接口。
      • 用户因此只关注自己的代码。
    • 考虑极端情况,没有“不可能”
      • 开发人员考虑如何处理不太可能发生的情况,并相应地实现处理。

【正确性】

  • 含义:程序按照spec加以执行的能力,是最重要的质量指标!

【对比健壮性和正确性】

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

  • 健壮性:尽可能保持软件运行而不是总是退出; 让用户变得更容易:出错也可以容忍,程序内部已有容错机制。

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

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

  • Reliability(可靠性) = Robustness + correctness

  • 健壮性使用户和第三方开发人员更加轻松:通过构建一些经过深思熟虑的灵活性,它将给客户端不太兼容的用户第二次机会,而不是把他们踢出局。

  • 正确性使开发人员更容易:

    • 他们可以专注于所有假设都得到保证的单一模型,而不是埋头检查/修正参数和处理奇怪的边缘情况。
    • 因此,主成功路径之外的任何状态都可以被忽略——产生更简短、更容易理解和更容易维护的代码。
  • 外部内部:

    • 外部接口(UI、输入文件、配置、API等)主要是为用户和第三方服务的。使它们健壮,并尽可能地适应,并期望人们会输入垃圾。
    • 应用程序的内部模型(即域模型)应该尽可能简单,并且始终处于100%有效的状态。使用不变量和断言来做出安全的假设,只要遇到任何不对的地方就抛出异常。
    • 在内外部之间做好隔离,防止“错误”扩散。
    • 对外的接口,倾向于健壮;对内的实现,倾向于正确。
  • 安全关键应用程序倾向于正确性而非健壮性。与其返回错误的结果,还不如不返回结果

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

  • 可靠性(Reliability):系统在规定的条件下无论何时需要都能执行其所需功能的能力,即故障间隔时间较长。

    Reliability = Robustness + Correctness

Problem健壮性正确性
浏览器发出包含空格的URL剥离空白,正常处理请求。将HTTP 400错误请求错误状态返回给客户端。
视频文件有坏帧跳过损坏区域到下一个可播放部分。停止播放,引发“损坏的视频文件”错误
配置文件使用了非法字符在内部识别最常见的评论前缀,忽略它们。终止启动时出现“配置错误”错误
奇怪格式的日期输入尝试针对多种不同的日期格式解析字符串。将正确的格式呈现给用户。日期错误无效

如何测量健壮性和正确性

  • 外部观察角度:

    • 平均故障间隔时间(MTBF,平均失效间隔时间)之间的运行时间预测系统在操作过程中固有的失败。

    • MTBF是指系统发生故障之间的平均时间。

    • MTBF描述了可修复系统两次故障之间的预期时间,而MTTF(平均故障间隔时间)表示不可修复系统的预期故障发生时间。

  • 内部观察角度:(间接)

    • 残余缺陷率:每千行代码中遗留的bug的数量。
      • 1-10个缺陷/kloc:典型的工业软件。
      • 0.1-1缺陷/kloc:高质量的验证。Java库可能达到这种级别的正确性。
      • 0.01-0.1缺陷/kloc:最好的安全关键验证。NASA和像Praxis这样的公司可以达到这个水平。

Java中的错误和异常

  • 所有Exception对象的基类都是java.lang.Throwable,以及它的两个子类java.lang.Exceptionjava.lang.Error

    java

【Error】

  • Error类描述很少发生的Java运行时系统内部的系统错误和资源耗尽情况。(例如VirtualMachineError,LinkageError)。
    • 你不应该抛出这种类型的对象。
  • 对于内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束
  • Error的类型:
    • 用户输入错误
      • 除了不可避免的拼写错误之外,一些用户喜欢开辟自己的道路,而不是遵循方向。
      • 例如:用户要求连接到语法错误的URL,网络层会投诉。
    • 设备错误
      • 硬件并不总是做你想做的。
      • 输出器被关闭
      • 某个网页可能暂时不可用。
      • 设备经常会在任务进行到一半的时候发生故障。
    • 物理限制
      • 磁盘可以填满
      • 可能耗尽了可用内存
  • 典型错误:
    • VirtualMachineErrorOutOfMemoryError StackOverflowError InternalError
    • LinkageErrorNoClassDefFoundError

【异常】Exception

  • 异常:程序执行中的非正常事件,程序无法再按预想的流程执行。
  • 异常处理:
    • 将错误信息传递给上层调用者,并报告“案发现场”的信息。
    • return之外的第二种退出途径:若找不到异常处理程序,整个系统完全退出
  • Exception类描述由程序引起的错误。(如:FileNotFoundException,IOException
    • 这些错误可以被你的程序捕获和处理(例如,执行一个替代操作或通过关闭所有文件、网络和数据库连接优雅地退出)。
  • 你自己程序导致的问题,可以捕获、可以处理。
  • 在Java编程语言中,异常对象始终是派生自Throwable的类的实例。
  • 异常是针对不满足precondition的情况的处理——增强健壮性。

【异常的分类】(按结构层次)

  • 运行时异常:由程序员处理不当造成,如空指针、数组越界、类型转换
  • 其他异常:程序员无法完全控制的外在问题所导致的,通常为IO异常,即找不到文件路径等,如:
    • Trying to find a Class object for a string that does not denote an existing class e.g., Class cl = Class.forName(“java.util.fooxxx”)
    • Trying to read past the end of a file

【异常的分类】(按处理机制角度)

  • 当异常发生时,(Checked Exceptions)

    • 你必须捕捉并处理异常,或者通过声明你的方法抛出异常来告诉编译器你不能处理它,
    • 然后使用你的方法的代码将不得不处理那个异常(如果不能处理异常,可以选择声明抛出异常)。
    • 编译器可帮助检查你的程序是否已抛出或处理了可能的异常。
  • 编译器不会检查ErrorsRuntime Exceptions(Unchecked Exceptions)

    • 错误Errors表示发生在应用程序之外的情况,例如系统崩溃。运行时异常 Runtime exceptions 通常是由于应用程序逻辑中的错误而发生的。
    • 在这些情况下你什么也做不了,只能重写程序代码。编译器不会检查这些。
    • 这些运行时异常将在开发和测试期间发现。然后我们必须重构代码以删除这些错误。
  • Unchecked exceptions

    • Unchecked exceptions:编程错误,其他不可恢复的故障(Error + RuntimeException)
    • 程序编译不需要任何操作,但未捕获的异常会导致程序失败。
    • 不需要在编译的时候用try…catch等机制处理。
    • 从RuntimeException派生出子类型
    • 可以不处理,编译没问题,但执行时出现就导致程序失败,代表程序中的潜在bug类似于编程语言中的dynamic type checking
  • Checked exceptions:

    • 已检查异常Checked exceptions:每个调用者都应该知道并处理的错误。

    • 必须被捕获或继续向上抛,否则程序将无法编译(编译器检查你是否为所有被检查的异常提供了异常处理程序)

    • 需要从Exception派生出子类型。

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

    • 例子:

      public class NullPointerExceptionExample {
      	public static void main(String args[]){
      		String str=null;
      		System.out.println(str.trim());
      	}
      }
      
      //Exception in thread "main" java.lang.NullPointerException
      

【Checked Exception 处理操作】

  • 异常处理五个关键词:try catch finally throws throw

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

    • 利用throws进行声明“本方法可能会发生XX异常”

      • 使用throws声明异常:此时需要告知你的client需要处理这些异常,如果client没有handler来处理被抛出的checked exception,程序就终止执行。
      • 程序员必须在方法的spec中明确写清本方法会抛出的所有checked exception,以便于调用该方法的client加以处理
      • 在使用throws时,方法要在定义和spec中明确声明所抛出的全部checked exception,没有抛出checked异常,编译出错,Unchecked异常和Error可以不用处理。
    • 利用throw抛出XX异常(详见下文)

      • 步骤:
        • 找到一个能表达错误的Exception类/或者构造一个新的Exception类
        • 构造Exception类的实例,将错误信息写入
        • 抛出它
      • 一旦抛出异常,方法不会再将控制权返回给调用它的client,因此也无需考虑返回错误代码
    • (try, catch, finally) 捕获并处理XX异常

      • 使用 try 和 catch 关键字可以捕获异常。try/catch 代码块放在异常可能发生的地方。
      • try/catch代码块中的代码称为保护代码,
      • catch 语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try 后面的 catch 块就会被检查。
      • 如果发生的异常包含在 catch 块中,异常会被传递到该 catch 块,这和传递一个参数到方法是一样。
      • 多个catch语句时,按照异常类从小到大的顺序写(如catche IOException再catch Exception)
      • 按照老师所说,catch是同时执行的,选最合适的那个。
      • catch可以有多个,运行时会选择最合适的那个进行异常处理,而不是顺序执行
    • finally语句

      • 场景:当异常抛出时,方法中正常执行的代码被终止;但如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理,所以需要用finally语句。(如文件close)
      • finally 关键字用来创建在 try 代码块后面执行的代码块。
      • 无论是否发生异常,finally 代码块中的代码总会被执行。
      • 在 finally 代码块中,可以运行清理类型等收尾善后性质的语句。
      • finally 代码块出现在 catch 代码块最后。
      • 注意下面事项:
        • catch 不能独立于 try 存在。
        • 在 try/catch 后面添加 finally 块并非强制性要求的。
        • try 代码后不能既没 catch 块也没 finally 块。
        • try, catch, finally 块之间不能添加任何代码。
  • Unchecked异常也可以使用throws声明或try/catch进行捕获,但大多数时候是不需要的,也不应该这么做——掩耳盗铃,对发现的编程错误充耳不闻。

    public class ArrayIndexOutOfBoundExceptionExample { //属于Unchecked Exceptions
    	public static void main(String args[]){
    		try {
    			String strArray[]={"Arpit","John","Martin"};
    			System.out.println(strArray[4]);  //编写程序时就应该避免溢出的,抛出无意义(直接改正即可)
    		} catch(ArrayIndexOutOfBoundsException e) {...}
    	}
    }
    
    

【checked和unchecked总结】

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

    • 如果客户端可以通过其他的方法恢复异常,那么采用checked exception;
    • 如果客户端对出现的这种异常无能为力,那么采用unchecked exception;
    • 异常出现的时候,要做一些试图恢复它的动作而不要仅仅的打印它的信息。
  • 尽量使用 unchecked exception 来处理编程错误:因为 unchecked exception 不用使客户端代码显式的处理它们,它们自己会在出现的地方挂起程序并打印出异常信息。

    • 充分利用Java API中提供的丰富unchecked exception,如NullPointerException , IllegalArgumentException和IllegalStateException等,使用这些标准的异常类而不需亲自创建新的异常类,使代码易于理解并避免过多消耗内存。
  • 如果client端对某种异常无能为力,可以把它转变为一个unchecked exception,程序被挂起并返回客户端异常信息,如:

    try{
    	...some code that throws SQLException
    }catch(SQLException ex){
    	throw new RuntimeException(ex);
    }
    
  • Checked exception应该让客户端从中得到丰富的信息。

  • 要想让代码更加易读,倾向于用unchecked exception来处理程序中的错误

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

  • 如果错误可预料但不可预防,则使用unchecked exception

    • 调用者尽其所能来验证输入参数,但是一些超出其控制范围的条件导致操作失败。
    • 例如,你试图读取一个文件,但在你检查它是否存在和读取操作开始之间,有人删除了它。
    • 通过声明一个检查异常,你告诉调用者预测这个失败。
  • 当异常没有处理时,它会依次向上传递,直到传到控制台,控制台结束进程。

  • 对于特殊结果(例如,预期情况)使用checked异常;使用unchecked的异常来显示错误(意外失败)

Checked exceptionUnchecked exception
Basic必须被显式地捕获或者传递
(try-catch-finally-throw),否则编
译器无法通过。
异常可以不必捕获或抛出,编译器不检查
Class of Exception
Handling从异常发生的现场获取详细的信息,利用
异常返回的信息来明确操作失败的原因,
并加以合理的恢复处理
简单打印异常信息,无法再继续处理

Appearance代码看起来复杂,正常逻辑代码和异常处
理代码混在一起
清晰,简单

【通过throws声明Checked Exception】

  • “异常”也是方法和client端之间spec的一部分,在post-condition中刻画。

    • 例如,试图读取文件的代码知道该文件可能不存在或它可能是空的。因此,试图处理文件中的信息的代码需要通知编译器,它可以抛出某种类型的IOException。
  • 方法可以抛出异常的地方是方法的头部;头部更改以反映该方法可能抛出的已检查异常。如:

    public FileInputStream(String name) throws FileNotFoundException
    
  • 在规约中声明异常的方法:

    • 发出特殊结果信号的已检查异常总是用Javadoc @throws子句记录,指定发生特殊结果的条件。

    • Java还可能要求在方法签名中包含异常,使用throws声明。

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

    • 用于指示意外失败的Unchecked Exceptions(客户机或实现中的错误)不是方法的后置条件的一部分,因此它们不应该出现在任何方法中

      @throws或抛出。(例如:NullPointerException不会出现在spec中)

    • 如果一个方法可能抛出一个以上被检查的异常类型,则必须在头文件中列出所有异常类。

  • 你的方法应该throws什么异常:

    • 你所调用的其他函数抛出了一个checked exception——从其他函数传来的异常;
    • 当前方法检测到错误并使用throws抛出了一个checked exception——你自己造出的异常。
    • 此时需要告知你的 client 需要处理这些异常,如果没有 handler 来处理被抛出的 Checked Exception ,程序就终止执行。
  • 不应该声明Error和Runtime Exceptions

【子类型多态】

  • 如果子类型中override了父类型中的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型更宽泛。

  • 子类型方法可以抛出更具体的异常,也可以不抛出任何异常。

  • 如果父类型的方法未抛出异常,那么子类型的方法也不能抛出异常。

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

throw异常的方法】

  • 两种方式:

    throw new EOFException();
    //或者
    EOFException e = new EOFException();
    throw e; //指明异常发生的位置
    
    • 用throws在方法处声明方法要抛出的异常,用throw声明异常发生的位置。
  • 利用Exception的构造函数,将发生错误的现场信息充分的传递给client。如:

    String gripe = "Content-length: " + len + ", Received: " + n;
    throw new EOFException(gripe);
    
    • 通常不需要包含方法,包含属性,输出提示信息即可。
  • 如果现有的异常类中有一个对你有效,抛出异常就很容易:

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

【创建异常类】

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

  • 从Exception或者从Exception的子类(如IOException)派生。

  • 习惯上,提供一个默认构造函数和一个包含详细消息的构造函数。

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

  • checked异常的例子:(注意创建异常类的写法,考试可能考)

    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;
      

      调用此方法的代码必须处理或传播此异常(或两者兼有):

      try {
      	...
      } catch(FooException ex) {
      	ex.printStackTrace();
      	System.exit(1);
      } catch(IOException ex) {
      	throw new FooException(ex); //注意写法
      }
      
  • 在某些情况下,您不希望强制每个方法在其throws子句中声明异常实现。在这种情况下,您可以创建一个继承RuntimeException的unchecked异常。

  • Unchecked异常的例子:

    public class FooRuntimeException extends RuntimeException {
    	...
    }
    
    public void calculate(int i) { //unchecked异常不需要在方法后throws
    	if (i < 0) {
    		throw new FooRuntimeException("i < 0: " + i);
    	}
    }
    

【捕获异常】

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

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

    • 程序跳过try块中的其余代码。
    • 程序执行catch子句内的处理程序代码。
    • 如果try块内的代码都不抛出异常,那么程序将跳过catch子句。
    • 如果一个方法中的任何代码抛出一个异常,而不是catch子句中指定的异常类型,这个方法立即退出。
    • 希望它的调用者已经为该类型提供了一个catch子句。
  • 异常处理的另一个选择:也可以不在本方法内处理, 而是传递给调用方,由client处理(“推卸责任”)

    • 如果我们采用这种方法,那么我们必须声明这个方法可能会抛出IOException
    • 编译器严格执行throws说明符。如果你调用一个抛出checked异常的方法,则必须处理它或将其传递下去。
  • 尽量在自己这里处理,实在不行就往上传——要承担责任;但有些时候自己不知道如何处理,那么提醒上家,由client自己处理。

  • 注意:

    • 如果父类型中的方法没有抛出异常,那么子类型中的方法必须捕获所有的checked exception
    • 子类型方法中不能抛出比父类型方法更多的异常。
  • getMessage()来获取详细的错误消息(如果有);使用getClass() getName()获取异常对象的实际类型

  • 捕获多个异常

    • 您可以在一个try块中捕获多个异常类型,并以不同的方式处理每种类型。
    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块捕获Exception,来保证一定被捕获
    

【Rethrowing and Chaining Exceptions】

  • 本来catch语句下面是用来做exception handling的,但也可以在catch里抛出异常

  • 通常,当您想要更改异常类型时,如果您构建了一个其他程序员使用的子系统,那么使用指示子系统故障的异常类型是很有意义的。

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

  • 下面是捕获异常并重新抛出它的方法:

    try {
    	access the database
    }
    catch (SQLException e) {
    	throw new ServletException("database error: " + e.getMessage());
    }
    //在这里,ServletException是用异常的消息文本构造的。
    
    • 但这么做的时候最好保留“根原因”

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

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

      Throwable e = se.getCause();
      

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

final语句】

  • 场景:当异常抛出时,方法中正常执行的代码被终止;但如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理,所以需要用finally语句。
  • finally 关键字用来创建在 try 代码块后面执行的代码块。
  • 无论是否发生异常,finally 代码块中的代码总会被执行。
  • 程序执行finally子句的三种可能情况:
    • 情况一:代码不抛出异常:
      • 程序首先执行try块中的所有代码。
      • 然后,执行finally子句中的代码。
      • 之后,继续执行finally子句后的第一条语句。
      • 换句话说,执行通过点1、2、5和6。
    • 情况二:代码抛出catch子句捕获的异常:
      • 程序执行try块中的所有代码,直到抛出异常为止。跳过try块中的其余代码。
      • 然后,程序执行匹配的catch子句中的代码,然后执行finally子句中的代码。
      • 如果catch子句不抛出异常,程序执行finally子句后的第一行。执行通过点1,3 4 5 6。
      • 如果catch子句抛出一个异常,那么该异常被抛出给调用者,并且执行通过点只有1、3、5。(仍要执行finally语句)
    • 情况三:代码抛出一个在任何catch子句中都没有捕获到的异常:
      • 在这里,程序执行try块中的所有代码,直到抛出异常。
      • 跳过try块中剩余的代码。
      • 然后,执行finally子句中的代码,并将异常返回给该方法的调用者。
      • 执行通过点只有1个和5个。
  • 可以只用finally语句不用catch语句(finally紧跟在try后)
  • 有finally时return失效(必须执行完finally)(无catch一定要finally,return在finally之后)
  • 任何情况finally必须被执行
  • 注意下面事项:
    • catch 不能独立于 try 存在。
    • 在 try/catch 后面添加 finally 块并非强制性要求的。
    • try 代码后不能既没 catch 块也没 finally 块。
    • try, catch, finally 块之间不能添加任何代码。
    • finally 代码块出现在 catch 代码块最后。

【分析栈跟踪元素】

  • 方法执行调用栈(后进先出)

  • 异常和调用栈:(PPT89页)

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

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

      它发现methodA()有一个XxxException处理程序,并将异常对象传递给处理程序。

      注意,需要使用methodC()和methodB()throws声明抛出的异常以编译程序。

  • 分析栈跟踪元素

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

Assertions

【断言】

  • 用assert断言保证程序的正确性。(异常保证健壮性)

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

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

    动态检查:Java通过动态捕获数组溢出bug,使其不可能发生。如果您试图在数组或List的边界之外使用索引,那么Java将自动产生一个错误。——未检查的异常/运行时错误。

  • 如果无法避免bugs,尝试着将bug限制在最小的范围内。(限定在一个方法内部,不扩散)

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

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

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

    • 当断言为真时,意味着一切都按预期运行。
    • 当它为假时,意味着它在代码中检测到意外错误。
  • 每个断言包含一个您认为在程序执行时为真的布尔表达式。如果不是真的,JVM抛出AssertionError。出现AssertionError,意味着内部某些假设被违反了。

  • 断言即是对代码中程序员所做假设的文档化,也不会影响运行时性能(在实际使用时,assertion都会被disabled(关闭))

  • 断言通常有两个参数:一个布尔表达式,描述假定为真的假设;如果不是,则显示一条消息。

  • Java的关键词assert有两个形式:assert condition;assert condition : message;(所构造的message在发生错误时显示给用户,便于快速发现错误所在)

  • assert针对的是方法内的参数以及要返回的值(而非传给方法的参数)。

  • 断言可用于验证:

    • 内部不变量

    • 表示不变量(RI),如:checkRep()

    • 控制流不变量:断言无法到达某个位置。例如,switch-case语句的default子句。

      • switch (vowel) {
        	case 'a':
        	case 'e':
        	case 'i':
        	case 'o':
        	case 'u': return "A";
        	default: assert false ;
        }
        
        void foo() {
        	for (...) {
        		if (...)
        			return;
        	}
        	assert false;
        	// Execution should never reach this point!
        }
        //等价于:
        default: throw new AssertionError ("must be a vowel, but was: " + vowel);
        
      • 但是不要在这里使用assert语句,因为它可以被关闭。相反,在不合法的情况下抛出异常,以便检查总是会发生;???

    • 方法的前置条件:当一个方法被调用时,什么必须是真的?

    • 方法的后置条件:方法成功完成后,什么必须为真?

  • 断言主要用于开发阶段,避免引入和帮助发现bug。断言主要用于开发阶段,避免引入和帮助发现bug,避免降低性能。

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

  • 因为可以禁用断言,所以程序的正确性永远不应该取决于是否执行断言表达式。特别地,断言表达式不应该有副作用。

    //错误写法:
    assert list.remove(x);
    //正确写法:
    boolean found = list.remove(x); 
    assert found;
    //关键:asser关闭以后会改变代码的功能
    
  • assert 还可以判断:

    • 输入变量值没有被方法改变
    • 指针是否为空
    • 传入方法的数组或其他容器至少可以包含 X 个数据元素
    • 一个表被初始化为实数值
    • 当方法开始执行(或完成)时,容器是空的(或满的)
    • 从一个高度优化的、复杂的方法得到的结果与从一个较慢但清晰编写的例程得到的结果相匹配。实际上可以对程序执行时的某些条件进行验证。
  • 程序之外的事,不受你控制,不要乱断言,注意:

    • 如:文件/网络/用户输入等不要检测。这是因为检测了也没办法
    • 断言只是检查程序的内部状态是否符合规约
    • 断言一旦 false ,程序就停止执行
    • 你的代码无法保证不出现此类外部错误。
    • 外部错误要使用 Exception 机制去处理。而 assert 一般检测程序内部的错误(开发阶段)
      • 这里指的程序内部可能是一个类内的方法。
  • Java缺省关闭断言,要记得打开(-ea)

  • 断言非常影响运行时的性能

    • 如二分法排序正常是对数级别的,但用assert判断以后就是线性时间了。

【断言使用指南】

  • 断言——正确性;异常——健壮性

  • 使用异常来处理你“预料到可以发生”的不正常情况;

    使用断言处理 “绝不应该发生”的情况。

  • 在其他一些开发者眼里,不应该针对参数的合法性使用断言。原因:

    • 参数检查通常是方法规约的一部分,无论启用或禁用断言,都必须遵守这些规范。
    • 即使spec被违反,也不应通过assert直接fail,而是应抛出具体的runtime异常。
  • 如果参数来自于外部(不受自己控制),使用异常处理;

    如果来自于自己所写的其他代码,可以使用断言来帮助发现错误(例如post-condition就需要)

  • 断言和异常处理都可以处理同样的错误

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

防御式编程

【防御式编程】

  • 防御式编程:防御性编程是防御性设计的一种形式,旨在确保软件在不可预见的情况下继续发挥功能。
  • 灵感来自于防御式驾驶:眼观六路,耳听八方,一旦 其他车辆有对你产生危险的症状,马上采取防御式行动。
  • 防御式编程技术:
    • 保护程序免受无效输入
      • “Garbage in, nothing out”;“Garbage in, error message out”;“No garbage allowed in”
      • 对来自外部的数 据源要仔细检查,例如:文件、网络数据、用户输入等。
      • 对每个函数的输入参数合法性要做仔细检查,并决定如何处理非法输入
    • 断言
    • 异常
    • 特定的错误处理技术
    • 设置路障
      • 防御编程的一种方法是将某些接口指定为“安全”区域的边界。检查跨安全区域边界的数据的有效性,如果数据是无效的,及时响应。
      • 定义干净区域(用assert保证正确性)和脏区域(用异常)
      • 类的public方法接收到的外部数据都应被认为是dirty的,需要处理干净再传递到 private方法——隔离舱。
        • “隔离舱”外部的函数应使用异常处理,“隔离舱”内的函数应使用断言。
      • Proxy设计模式——隔离。
    • 调试病毒

【SpotBugs 工具】

  • FindBugs:Java静态代码分析工具
  • SpotBugs是一个使用静态分析来查找bug的程序Java代码。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值