冰雹序列
冰雹序列的定义
从正整数n开始,如果n是偶数,则下一个数是n/2,否则下一个数是3n+1,直到n等于1。
示例如下:
如果n是偶数,下一个数会比n小。如果n是奇数,下一个数会比n大。n的取值忽大忽小,那么n最后都会归为1么?(目前没有解决办法)
冰雹序列的计算代码
这里需要注意的是:
python以缩进分割语句块,以换行判断语句的结束。
Java以花括号分割语句块,以分号判断语句的结束。
这是我认为两种语言风格上较大的区别。
数据类型
在上面的代码当中我们就可以看到,Python和Java最大的不同在于,Java需要指定变量的类型。Java提供的数据类型部分如下:
·原始类型:
byte short int long boolean double char float
·包装类型(对象类型):
Byte Short Integer Long Boolean Double String Float
对于Java来说,原始类型用小写字母,对象类型的大写字母开头。
操作符
操作符是一些能接受输入并输出结果的功能。
Java3种常见操作符:
静态类型
Java是一种静态类型的语言。所有变量的类型在编译的时候就已经知道了(程序还没有运行)。静态类型是静态检查的一种——检查发生在编译的时候。而在动态类型语言中(例如Python),这种类型检查是发生在程序运行的时候。
这里有一些经验,告诉你这静态和动态检查通常会发现什么bug:
静态检查
·语法错误,例如多余的标点符号或者错误的关键词。即使在动态类型的语言例如Python中也会做这种检查:如果你有一个多余的缩进,在运行之前就能发现它。
·错误的名字,例如Math.sine(2). (应该是 sin.)
·参数的个数不对,例如 Math.sin(30, 20).
·参数的类型不对 Math.sin(“30”).
·错误的返回类型 ,例如一个声明返回int类型函数return “30”;
对比动态检查的经验:
动态检查
·非法的变量值。例如整型变量x、y,表达式x/y。只有在运行后y为0才会报错,否则就是正确的。
·无法表示的返回值。例如最后得到的返回值无法用声明的类型来表示。
·越界访问。例如在一个字符串中使用一个负数索引。
·使用一个null对象解引用。(null相当于Python中的None)
静态检查倾向于类型错误,即与特定的值无关的错误。
而动态检查倾向于特定的值会触发的错误。
原始类型并不是真正的数字!
原始类型并不是真正的数字!
在Java和许多其他语言中存在一个“陷阱”——原始数据类型的对象在有些时候并不像真正的数字那样得到应有的输出。
例如有一些我们很常见的错误:
·整数的除法:5/2不会返回2.5。
·整型溢出:当数值过大的时候,发生负溢出。
·浮点类型中的特殊值:NaN、无穷的表示。
练习
1
int n = 5;
if (n) {
n = n + 1;
}
1
2
3
4
静态错误
2
int big = 200000; // 200,000
big = big * big; // big should be 40 billion now
1
2
无报错,但是得到错误的结果
3
double probability = 1/5;
1
无报错,但是得到错误的结果
4
int sum = 0;
int n = 0;
int average = sum/n;
1
2
3
动态错误
5
double sum = 7;
double n = 0;
double average = sum/n;
1
2
3
无报错,但是得到错误的结果
浮点数除零为Infinity
数组和列表
在Java中有两种常用的线性存储结构:array和list。
数组是一连串类型相同的元素组成的结构,而且它的长度是固定的(元素个数固定)。
int[] a = new int[100];
需要注意的是,当你求取数组长度的时候:
a. length (注意和 String.length() 的区别—— a.length 不是一个类内方法调用,你不能在它后面写上括号和参数)
下面是我们利用数组写的第一个求“冰雹序列”的代码,它存在一些bug:
相信很快你就能发现错误:幻数100?
注:幻数是指那些特定的设计好的数值.
像这样的bug称为越界访问,在Java中能够被动态检查检测出来,但是在C和C++这样的语言中则会造成缓冲区溢出(能通过编译),这也是很多漏洞的来源。
解决方式是使用list类型。
列表类型是一个长度可变的序列结构。我们可以这样声明列表:
List list = new ArrayList();
常用的操作符有下:
·索引一个元素: list.get(2)
·赋予一个元素特定的值: list.set(2, 0)
·求列表的长度: list.size()
这里要注意List是一个接口,这种类型的对象无法直接用new来构造,必须用能够实现List要求满足的操作符的方法来构造。ArrayList是一个实类型的类(concrete type),它提供了List操作符的具体实现。当然,ArrayList不是唯一的实现方法(还有LinkedList等),但是最常用的一个)。
另外要注意的是,我们要写List 而不是List。因为List只会处理对象类型而不是原始类型。当我们使用尖括号参量化一个类型时,Java要求我们使用对象类型而非原始类型。在其他的一些情况中,Java会自动在原始类型和对等的对象类型之间相转换。例如在上面的代码中我们可以使用Integer i = 0
下面是用列表写的“冰雹序列”的实现:
遍历
例如,找到list当中最大元素的代码:
为什么使用规格说明
在编程中,很多让人抓狂的bug是由于两个地方的代码对于接口行为的理解不一样。当程序崩溃的时候,就很难发现问题在哪里。简洁准确的的规格说明使得我们远离bug,更可以快速发现问题所在。
规格说明对使用者(客户)来说也是很有用的,它们使得使用者不必去阅读源码。
现在我们来看看下面这个标准的Java规格说明和它对应的源码,它是BigInteger中的一个方法:
API文档中的规格说明:
public BigInteger add(BigInteger val)
Returns a BigInteger whose value is (this + val).
Parameters: //注:参数
val - value to be added to this BigInteger.
Returns:
this + val
1
2
3
4
5
6
7
8
9
10
11
Java 8中对应的源码:
if (val.signum == 0)
return this;
if (signum == 0)
return val;
if (val.signum == signum)
return new BigInteger(add(mag, val.mag), signum);
int cmp = compareMagnitude(val);
if (cmp == 0)
return ZERO;
int[] resultMag = (cmp > 0 ? subtract(mag, val.mag): subtract(val.mag, mag));
resultMag = trustedStripLeadingZeroInts(resultMag);
return new BigInteger(resultMag, cmp == signum ? 1 : -1);
1
2
3
4
5
6
7
8
9
10
11
12
13
可以看到,通过阅读 BigInteger.add 的规格说明,客户可以直接了解如何使用BigInteger.add ,以及它的行为属性。如果我们去阅读源码,我们就不得不看 BigInteger的构造体, compareMagnitude, subtract以及trustedStripLeadingZeroInts的实现——而这还仅仅只是开始。
规格说明对于实现者也是很有好处的,因为它们给了实现者更改实现策略并且可以不告诉使用者的自由。规格说明也可以限定一些特殊的输入,这样实现者就可以省略一些麻烦的检查和处理,代码也可以运行的更快。
如上图所示,规格说明就好像一道防火墙一样将客户和实现者隔离开。它使得客户不必知道这个单元是如何运行的(不必阅读源码),也使得实现者不必管这个单元会被怎么使用(因为客户要遵守前置条件)。这种隔离造成了“解耦”(decoupling),客户自己的代码和实现者的代码可以独立发生改动,只要双方都遵循规格说明对应的制约。
行为等价
思考下面两个方法的异同:
static int findFirst(int[] arr, int val) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == val)
return i;
}
return arr.length;
}
static int findLast(int[] arr, int val) {
for (int i = arr.length -1 ; i >= 0; i–) {
if (arr[i] == val)
return i;
}
return -1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
为了判断“行为等价”,我们必须判断一个方法是否可以替换另一个方法,而程序的行为不发生改变。
它们的行为不一样:
1.当val找不到时,fingFirst返回arr的长度而findLast返回-1;
2.当数组中有两个val的时候,findFirst返回较小的那个索引,而findLast返回较大的那个。
但是当val在数组中仅有一个的时候,这两个方法的行为是一样的。也只有在这种情况下,我们才可以将方法的实现在两者中互换。
“行为等价”是对于“旁观者”来说的——就是客户。为了让实现方法可以发生改动,我们就需要一个规格说明要求客户遵守某一些制约/前置条件。
所以,我们的规格说明可能是这样的:
static int find(int[] arr, int val)
- requires:
val occurs exactly once in arr - effects:
returns index i such that arr[i] = val
1
2
3
4
5
练习
Best behavior
对于上述的两个代码,我们接着来讨论。现在来改变一下规格说明,假设客户对返回值要求:
如果val在a中,返回任何索引i ,使得a[i] == val 。否则,返回一个不在a索引范围内的整数j
在这种情况下,findFirst 和 findLast 的行为等价吗?
Yes
规格说明的结构
一个规格说明含有以下两个“条款”:
1.一个前置条件,关键词是requires
2.一个后置条件,关键词是effects
其中前置条件是客户的义务(谁调用的这个方法)。它确保了方法被调用时所处的状态。
而后置条件是实现者的义务。如果前置条件得到了满足,那么该方法的行为应该符合后置条件的要求,例如返回一个合适的值,抛出一个特定的异常,修改一个特定的对象等等。如上图所示。
如果前置条件不满足的话,实现也不需要满足后置条件——方法可以做任何事情,例如不终止而是抛出一个异常、返回一个任意的值、做一个任意的修改等等。如上图所示。
练习
Logical implication
思考下面这个规格说明
static int find(int[] arr, int val)
- requires:
val occurs exactly once in arr
- effects:
returns index i such that arr[i] = val
1
2
3
4
5
6
7
8
9
作为find的实现者,下面哪些行为是合法的?
[x] 如果arr为空,返回0
[x] 如果arr为空,抛出一个异常
[x] 如果val在arr出现了两次,抛出一个异常
[x] 如果val在arr出现了两次,将arr中的元素都设置为0,然后抛出一个异常
[x] 如果arr不为空但是val没有出现,选取一个随机的索引,将其对应的元素设置为val
,然后返回这个索引
[x] 如果arr[0]是val
,继续检查剩下的元素,返回索引最高的那个val对饮的索引(没有再次找到val就返回0)
Logical implementation
作为find的实现者,当arr为空的时候,为什么要抛出一个异常?
[ ] DRY(译者注:Don’t repeat yourself)
[x] 快速失败/报错
[ ] 避免幻数
[ ] 一个变量只有一个目的
[ ] 避免全局变量
[ ] 返回结果
Java当中的规格说明
Java对于文档注释有一些传统,例如参数的说明以@param作为开头,返回的说明以@return 作为开头。你应该将前置条件放在@param的地方,后置条件放在 @return的地方。
例如,一个规格说明可能是这样:
static int find(int[] arr, int val)
- requires:
val occurs exactly once in arr
- effects:
returns index i such that arr[i] = val
1
2
3
4
5
6
7
8
9
它在Java中可能被注释为这样:
/**
-
Find a value in an array.
-
@param arr array to search, requires that val occurs exactly once in arr
-
@param val value to search for
-
@return index i such that arr[i] = val
*/
static int find(int[] arr, int val)
1
2
3
4
5
6
7
8
9
10
11
12
13
Eclipse也可以根据你的规格说明产生对应的文档),或者产生和Java API一个格式的HTML文档,这对你和你的客户来说都是很有用的信息。
练习
Javadoc
思考以下规格说明:
static boolean isPalindrome(String word)
- requires:
word contains only alphanumeric characters
- effects:
returns true if and only if word is a palindrome
注:回文,例:aba
1
2
3
4
5
6
7
8
9
10
11
对应的Javadoc注释:
/*
-
Check if a word is a palindrome.
-
A palindrome is a sequence of characters
-
that reads the same forwards and backwards.
-
@param String word
-
@requires word contains only alphanumeric characters
-
@effects returns true if and only if word is a palindrome
-
@return boolean
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
请问Javadoc中哪一行是有问题的?
[x] /*
注:应该是两个星
[ ] * Check if a word is a palindrome.
[ ] * A palindrome is a sequence of characters
[ ] * that reads the same forwards and backwards.
[x] * @param String word
[x] * @requires word contains only alphanumeric characters
[x] * @effects returns true if and only if word is a palindrome
注:不需要写effects,这应该写到return里
[x] * @return boolean
注:需要再多说明返回ture的情况
[ ] */
Concise Javadoc specs
思考下面这个规格说明Javadoc,判断每一句的作用(逆序):
/**
-
Calculate the potential energy of a mass in Earth’s gravitational field.
-
@param altitude altitude in meters relative to sea level
-
@return potential energy in joules
*/
static double calculateGravitationalPotentialEnergy(double altitude);
1
2
3
4
5
6
7
8
9
10
11
12
13
static double calculateGravitationalPotentialEnergy(double altitude);
[ ] 前置条件
[ ] 后置条件
[ ] 是前置条件也是后置条件
[x] 都不是
@return potential energy in Joules
[ ] 前置条件
[x] 后置条件
[ ] 是前置条件也是后置条件
[ ] 都不是
@param altitude altitude in meters relative to sea level
[x] 前置条件
[ ] 后置条件
[ ] 是前置条件也是后置条件
[ ] 都不是
Calculate the potential energy of a mass in Earth’s gravitational field.
[ ] 前置条件
[ ] 后置条件
[ ] 是前置条件也是后置条件
[x] 都不是