HIT软件构造经验漫谈(六)

本博客主要总结我个人对错误、异常和断言的理解

其中,1是初步总结、2、3是对1的修正,4是对修正后总结的一些说明

另外,本博客不代表最终观点,写这篇博客的时候还忙着其他各种大作业和竞赛的DDL,所以还没来得及把课件再回顾一遍,只是想赶紧先记录我一下我的思考,省得之后忘了,之后会再补充我的新的思考。


1. 初步的总结

HIT软件构造经验漫谈(六)_第1张图片
  图源自Java核心技术卷I,显然throwable可以分为两类:错误Error和异常Exception,而异常又分为IOException和RuntimeException,再加上断言assert即组成了本篇博客要总结的4个基本内容。
在这里插入图片描述
  在书中对Error类进行了这样的说明,显然Error一般不需要程序员去考虑,如果真的不幸发生,我们能做的也只有让程序优雅地结束了。因此我们事实上需要考虑的其实只有3个要素,即IOException、RuntimeException以及断言assert
  在上周末和小伙伴就何时使用错误、异常和断言进行了一次讨论,这里首先列出在讨论前,我对这三者的理解。
其中黄色部分是讨论时,争议比较大的一个内容。

  • 首先是一组大原则:
    • 我们通过异常和断言来检查程序在运行时,是否发生了期望之外的变故。
    • (受查)异常机制的目的是程序的健壮性,即对于一部分意料之外的变故,有自我修复的能力,而不是因为一点小差错就崩溃。但是对于某些变故,不存在程序自行修复的可能性(比如硬件错误),那么此时不应该去绞尽脑汁思考异常捕获程序怎么写,而是应该赶紧让他崩溃,省得之后debug的时候连哪里发生了问题都找不到。
    • 对于这个变故,如果可以修复,就通过IOException及其子类进行异常抛出。
    • 对于这个变故,如果不可能修复,唯一的修复方案是重写源代码,那么就通过RuntimeException及其子类进行异常抛出,或者通过断言来进行检查。
    • 对于RuntimeException和断言,通过断言在编写代码时进行我们能够想到的所需要的检查,通过RuntimeException在运行代码时,由虚拟机进行异常抛出。
      • 即,自己写的代码只assert,不抛出RuntimeException
  • 对于checkRep,显然应该通过assert,因为checkRep检查的是当前类的RI,如果断言不满足,必然是代码问题,老老实实回去改代码吧
  • 对于前置条件的检查,通过IOException和断言来检查。
    • 对于可以让用户重新输入参数来解决的异常,比如我需要一个航班编号满足/B\d{4}/的格式,但是用户给我输入了一个A123,此时抛出一个IOException或者其子类,然后在客户端捕获异常,并让用户重新输入,以此来修复异常
    • 对于和用户输入无关的参数错误,比如传进来了一个空引用null,那么通过断言assert来引用。毕竟用户再怎么输入,充其量就是传个空字符,如果传了个空引用,一定是因为客户端代码错了。(这里的编程场景是Lab3/4中我们写了若干个类,然后在App中使用这些类,对于某些情况,可能用户输入确实会导致传null进来,那么仍然用IOException来检查)
  • 对于后置条件的检查,用断言assert检查。因为后置条件不满足,毫无疑问就是代码写的有问题。

个人习惯用IOException和RuntimeException来分别指代受查异常和非受查异常,但是后者的称呼可能会更规范一些。

2. 讨论过程中发生的争论

  虽然在1中划出了两点争论,但其实本质的争论点只有1条,即是否应该在检查前置条件时抛出RuntimeException或其子类。
  (起初)我认为前置条件就用受查异常和断言来检测就行了,使用RuntimeException毫无意义,因为如果发生了诸如空指针引用、数据越界访问等问题,也会在那一行代码就自己抛出异常,自己再额外检查一遍就是在单纯地浪费性能。
  而我的小伙伴则非常直接地拿出了课件里的内容。
HIT软件构造经验漫谈(六)_第2张图片
HIT软件构造经验漫谈(六)_第3张图片

第2张图是从Java官方文档里截的,当时我以为illegalArgumentException是受查异常,但事实上它是RuntimeException的子类。

  然而我并不信邪,于是也翻出了课件中的一张图。
HIT软件构造经验漫谈(六)_第4张图片
  于是,一个问题出现在了我的脑海中:用非受查异常检查前置条件到底有什么意义,条件不满足让用户重新输入一个正确的不就行了么,既然如此应该用受查异常,为什么是非受查异常呢?

3. 我对利用RuntimeException检查前置条件思考

  在illegalArgumentException这个语义明确的异常名的启发之下,我终于意识到了我之前的认识上的一些缺陷。

考虑如下场景:

  我封装了一个方法,计算n的阶乘,要求n非负,用户在控制台输入一个数字,客户端会调用我的这个方法,然后把用户输入的数字作为参数传给我。
  虽然一般而言,客户端在调用我的方法时应该确定这个参数满足我要求的前置条件,但不妨假设,写客户端的小伙伴恰好忘记检查了。而此时,聪明的我恰好想到了这个笨蛋小伙伴会出幺蛾子,所以预先检查了传进来的参数是否非负,如果不是,则抛出受查异常,然后让写客户端的小伙伴写个异常捕获程序,引导用户重新输入参数。
  在这个场景中,使用受查异常似乎没什么问题,它能帮助行将崩溃的程序回归正轨。

但是考虑另一个场景:

  笨比小伙伴在调用我的方法手滑写成了如下代码:

int input = in.nextInt()
if(input < 0) {
     
...
}
int ans = calFactorial(-input) // 传给我的是-input

  笨比小伙伴检查了input是否非负,但是他在调用我的方法时写成了传负的input给我。如果发生了这种错误,那似乎我再怎么抛出受查异常也毫无意义。那么此时让程序尽快在调用calFactorial的地方附近终止,以便debug似乎会是一个更好的选择。
这个例子可能不太好,之后想到了再回来改

4. 一些额外说明

  事实上,我认为受查异常和非受查异常其实也分别代表着对健壮性和对正确性的追求。前者可以让程序不要轻易崩溃,后者可以让程序发生错误时尽快终止,方便程序员debug时定位。
  我个人并不希望因为用户的一些(可能的)手滑,就导致程序终止,因为我觉得这样很影响用户体验,所以我更倾向于受查异常。但是为了正确性,我也会大量应用断言。
  此外,本博客不代表最终观点,写这篇博客的时候还忙着其他各种大作业和竞赛的DDL,所以还没来得及把课件再回顾一遍,只是想赶紧先记录我一下我的思考,省得之后忘了,之后会再补充我的新的思考。

你可能感兴趣的:(软件构造,java)