Specifications
原文链接
http://web.mit.edu/6.031/www/sp20/classes/06-specifications/
Introduction
The specification acts as a contract: the implementer is responsible for meeting the contract, and a client that uses the method can rely on the contract.
同老师上课讲法,spec就是客户端和开发者之间的合同,开发者必须按照合同实现方法,用户必须按照合同去使用方法。
本文将讨论preconditions、postconditions、exceptions三个重要概念
Java needed for this reading
需要我们了解exception的概念
try、catch的用法
很简单的基本概念,用过java的同学应该都知道。
Behavioral equivalence
所谓的行为等价性:whether we could substitute one implementation for the other。即我们是否可以用一段代码去代替另一段代码。
如下面两个小例子:
1.
static int find(int[] arr, int val) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == val) return i;
}
return -1;
}
static int find(int[] arr, int val) {
for (int i = 0, j = arr.length-1; i <= j; i++, j--) {
if (arr[i] == val) return i;
if (arr[j] == val) return j;
}
return -1;
}
两个方法都是在一个数组中寻找是否存在某个数,如果存在则返回下标,如果不存在则返回-1。
我们的问题是:是否能够用方法2来代替方法1呢?即这两个方法是否具有行为等价性呢?
答案是不能的,如果有待查找的数字多次出现在了这个数组中,第一个方法会返回第一次出现时的下标,第二个方法会返回最小或者最大的下标,取决于谁更加靠近端点。
而如果我们加上一个限定条件:只有一个待寻找的数存在在数组当中的话,那么两个方法的返回值就是唯一的,在这个条件的约束下,两个方法具有行为等价性。
行为等价性的定义是从客户端的角度来说的,如果一个方法能够替换另一个,则两方法具有行为等价性。
static int find(int[] arr, int val)
requires:
val occurs exactly once in arr
effects:
returns index i such that arr[i] = val
博客中给出的习题,复习时均应阅读:
spec和test(test-first-programming)一定要先写好
两句很有启发的话:
strong precondition hides their potential differences in behavior.
前置条件太强的话,会盖住潜在的行为不同性,两个看起来不同的方法在很强的前置条件下会具有行为等价性。
== a weak postcondition permits their differences in behavior==
前置条件太弱,就会允许一些行为上的差异,但是两方法仍具有行为等价性。
Why specifications?
spec可以帮助我们使程序更好理解,我们要了解一个方法能干什么,只读spec即可,不必费心神去读源代码。也可以帮助我们更快的确定bug的位置。
spec不仅像用户和实现者之间的合同,也像用户和实现者之间的防火墙。
implementer可以用任何的数据结构和算法来实现spec要求的功能,而不必将细节暴露给客户端。
这样的防火墙带来的好处就是将客户端和实现者解耦(decoupling)。
Specification structure
spec的构成:
1.方法名,参数类型,返回值类型,抛出异常的类型
2.requires(pre)
3.effects(post)
满足precodition是客户端的义务。
而满足postcondition是实现者的义务。
参数类型和requires都是precondition。
返回值类型和抛出异常的类型和effects都是postcondition。
- 前置条件满足,后置条件必须满足
- 前置条件不满足,后置条件随意,实现者可以做任何事(即使是不道德的事情)
When our precondition is violated, the client has a bug. We can make that bug easier to find and fix by failing fast, even though we are not obligated to do so.
自律的要求(即使没有任何人要求我们这么做):抛出异常,fail fast(让程序尽快死掉),检查bug。
Specifications in Java
@param
@return
@throw
用这样的语法可以自动生成帮助手册。
例:
- spec的注释第一行两个星号
- 不用加参数类型或着返回值类型
- 没有@requires 和 @effects这样的关键字
** 什么是spce不应该说的: **
局部变量,私有属性,算法实现的任何细节,实现者应该保证细节对读代码的人不可见。
Do not allow null references
原始类型不能被赋值为null:(8种primitives)
byte、short、int、long、float、double、boolean、char。
你无法访问null对象的任何属性和方法。例如 String name = null;
name.length() -----------------> throws NullPointException
null通常是危险的,所以spec除非显示说明可以接受null,否则null是不能做为参数和返回值的。
所以默认任何spec都有一个precondition:参数不能是null,也都有一个postcondition:返回值不能是null!!!
Avoid null !!!
建议显示的使用@NonNull关键字来说明禁止出现null,这样编译器就会自动检查。
void也是一个值,如果返回类型是void,只能用return;
null.length()会导致NullPointerException
关键:只要没提参数或者返回值可以是null,就默认不能是null。
Include emptiness
null和emptiness的差别:
null和空串的差别,类似于形式语言与自动机那门课中,
ϕ
和
ϵ
\phi 和\epsilon
ϕ和ϵ的区别。
List.of()返回一个空列表。
只要没说可以用null,就绝对不能用null。
前件不成立,后件无条件成立。
Testing and specifications
Testing units
集成测试:一个测试用例结合各个模块的功能。
不应该使测试用例违反前置条件,否则测试没有意义。
如果编译器能检查出某种错误,那么它不应该被写入spec,那是多余的。
Specifications for mutating methods
后置条件存在的side-effects,可能会修改某个内部的值。
pre必须限制list1不等于list2,否则有可能算法无法停止。
就像null如果不特殊说明就被禁止出现在参数和返回值中一样,mutation不特殊说明也被禁止出现在参数和返回值中。
Exceptions
例:
IndexOutOfBoundsException
NullPointException
ArithmeticException
NumberFormatException
出现不应该出现的情况时,以往的程序员往往返回一个特殊值,如-1,null,9/9/99等,然而这样时有问题的,我们不仅容易忘了这么做,而且还会导致我们无法发现程序中存在的bug。
Java的异常处理机制,提供以下两种做法:
- 用throw抛出异常
- 用try catch 处理异常
例题:
try必须跟catch或者finally,否则静态检车都过不去。
try和catch会把变量的作用域分开,在try中声明的变量到这个大括号外面就直接被释放了,不再存在,不能对它赋值或者引用。
空的catch体是一个很坏的编程习惯,尽量不要这么去做。它捕获了异常,但又什么都不做,让程序继续执行下去。
chained exception,将原来捕获的异常作为新的异常的构造器的参数传入,构造一个新的异常,然后继续抛出。
Checked and unchecked exceptions
从上一个部分可以看出异常机制的两个重要理由:特殊的返回值和bug检测。有这样一通用的规则:用checked exceptions来标识特殊值,用unchecked exceptions来标识bug。
checked exceptions:需要在函数声明中写throw XXXXException,这样编译器就会进行静态检查。
Exception之下有一支全是checked exception
RuntimeException下面全是unchecked exception
当用catch,你最好用最细分的exception类型,而不是简单的用Exception或者RuntimeException,可能会影响静态检查或者隐藏bug。
- unchecked的exception最好不要抛出来,而是直接打印信息,方便我们找到bug所在。
- checked的exception要抛给上一层,告诉我们需要处理特殊值了(情况不符合)。抛出的异常就类似于我们以往的编程工作中所写的函数所返回的特殊值(例如遇到找不到的情况就返回-1,并在main函数中检测这个特殊值)。可以把抛特殊值等价于抛异常。
Error是Unchecked Exception,但是error不是exception的子类,所以无法被try,catch捕获。而且直接自动打印栈信息。
checked exception:@throws 和 throws都要写
unchecked exception:只用写@throws
Summary
- Safe from bugs
- Easy to understand
- Ready for change