目录
- 静态检查(static checking)
- 雹石序列(Hailstone Sequence)
- 雹石序列的计算(Computing Hailstones)
- 类型(Types)
- 静态输入(Static Typing)
- 静态检查,动态检查,还有不检查(Static Checking, Dynamic Checking, No Checking)
- 意外:基本数据类型不是真的数字(Surprise: Primitive Types Are Not True Numbers)
- 数组和集合(Arrays and Collections)
- 迭代(Iterating)
- 方法(Methods)
- 变值与可重赋值变量(Mutating Values vs. Reassigning Variables)
静态检查(static checking)
雹石序列(Hailstone Sequence)
雹石序列是一串自然数序列,假设某一项ai,则该项如果是1,则它是该序列的最后一项;否则,它的后继项ai+1满足
a
i
+
1
=
{
a
i
÷
2
a
i
%
2
=
=
0
a
i
×
3
+
1
a
i
%
2
=
=
1
a_{i+1}=\begin{cases} a_i\div2 & a_i\%2==0 \\ a_i \times 3 + 1 & a_i\%2==1 \\ \end{cases}
ai+1={ai÷2ai×3+1ai%2==0ai%2==1
雹石序列不论以何数开头,总是以1结尾。
雹石序列的计算(Computing Hailstones)
对于雹石序列的计算输出,在Java和Python中有如下不同的写法:
// Java
int n = 3;
while (n != 1) {
System.out.println(n);
if (n % 2 == 0) {
n = n / 2;
} else {
n = 3 * n + 1;
}
}
System.out.println(n);
# Python
n = 3
while n != 1:
print(n)
if n % 2 == 0:
n = n / 2
else:
n = 3 * n + 1
print(n)
有几件值得注意的事情:
- Java中的表达式(expressions)和语句(statements)的语法和Python是非常相似的。
- Java的每条语句结尾需要加上分号(semicolons)。虽然这种要求比Python麻烦点,但是在代码的组织上,可以把一条语句分成多行,可读性更好。
- Java对if和while的条件判断要加上括号(parentheses)
- Java在每个代码块(blocks)外要加花括号(curly braces),而Python对代码块的要求是正确的缩进。不过虽然Java对代码块的缩进没有要求,但我们还是应该加上合适的缩进,这样做不是为了让编译器理解代码,而是为了让程序员看代码更方便。
类型(Types)
Java和Python的一个重要的语法不同是,Java的每个变量都要有类型声明。
Java有几个基本类型(primitive types):
- int:整型,范围在-231到231之间,大概是±20亿。
- long:长整型,比int范围更大的整型,范围在-263到263之间。
- boolean:布尔型,即真假值。
- double:浮点型,范围是一个实数的子集。
- char:字符型,诸如’A’和’$'这样的单个字符都属于字符型。
Java也有对象类型(object types),比如说:
- String:表示一串字符的序列,即字符串,可类比Python的string。
- BigInteger:表示任意大小的整数,可类比Python的integer。
按照Java的惯例,基本类型都是小写的(lowercase),而对象类型是大写字母(capital letter)开头的。
静态输入(Static Typing)
Java是一种静态输入语言(statically-typed language)。变量的类型是在编译时,真正运行前就确定的,因此编译器也可以推断出表达式的(返回值)类型。比如说如果a和b都是int类型的,那么a+b就是int类型的,这点是在编译时就确定的。Eclipse在你编辑代码时,就可以确定各种变量和表达式的类型,这样可以帮你找出很多错误。
在Python这样的动态输入语言(dynamically-typed languages) 中,只有到程序真正运行时才会做类型检查。
Java这种做法是一种静态检查(static checking),编译时就可以找bug。比如如果你写了一行糟糕的代码
"5" * "6"
那么静态输入会在你编程的时候就发现这个错误并提醒你,而不是等到真正执行程序的时候才报错。
静态检查,动态检查,还有不检查(Static Checking, Dynamic Checking, No Checking)
一种编程语言有三种自动检查的模式,理解它们会非常有用:
- 静态检查(Static checking):在程序真正运行之前就自动找到bug。
- 动态检查(Dynamic checking):当程序真正运行的时候才自动找到bug。
- 不检查(No Checking):编程语言根本就不帮你找bug,只能靠你自己找了,不然就都是错误。
找bug的模式还是静态检查最好,动态检查次之,不检查最糟糕了,仅靠程序员来肉眼debug的话,这样会有很多bug。
静态检查可以捕获这样的bug:
- 语法错误。比如说多打了标点或者单词拼写不对。动态检查也可以捕捉到这样的错误。如果Python程序里有缩进错误的话,程序开始运行之前你就知道了。
- 名字错误。比如说
Math.sine(2) //正确名字应该是sin
- 参数个数错误。比如说
Math.sin(30, 20)
。 - 参数类型错误。比如说
Math.sin("30")
。 - 返回值类型错误。比如说一个声明好了的要返回int类型的函数,却有
return "30"
这样的返回字符串的语句。
静态检查可以捕捉到:
- 非法的变量值。比如整数除法
x/y
在y
是0的情况,就是错误的,否则就是正确的。在这种情况下,除零不是静态错误,而是动态错误。 - 返回值不匹配。这种错误主要是返回值的类型和赋值类型不匹配导致的。
- 范围溢出。比如说在一个字符串中是用了负值索引,或者索引太大超出了字符串长度。
- 在一个指向
null
的对象上调用了方法(null
类比Python的None
)。
静态检查主要是检查变量的值类型。数据类型都是变量值的集合。静态输入保证了一个变量的值是合法的,就是必须是从相应的数据集合中选择的,但我们得在程序运行的时候,才能知道这个变量的值到底是什么。所以如果即将发生的错误是由于变量的一部分取值导致的,像除零错误和范围溢出这样的,编译器是不会对其报静态错误的。
相反,动态检查主要检查的是由于特定值引起的错误。
意外:基本数据类型不是真的数字(Surprise: Primitive Types Are Not True Numbers)
包括Java在内的很多编程语言都有一个小陷阱,就是它们的基本数字数据类型都有特殊情况,就是和我们平时使用的整数和实数不太一样。这会导致一些本应该由动态检查出的错误,检查不出来。这些小陷阱有如下几个方面:
- 整数除法(Integer division)。
5/2
不会返回一个分数,而是返回一个被截断的整数2.这就是一个被动态检查忽略掉的错误。这个式子本应该是能动态检查出来的,可以这么理解,两个操作数都是整型,那么表达式也是整型,但表达式的值是浮点型,两种类型矛盾,出现了动态错误。但是,这种情况是不会报错的,而是直接输出错误答案2。 - 整型溢出(Integer overflow)。
int
和long
类型能表示的整数范围都是有限的。如果一个计算结果太大或者太小,以至于超出了整型的表示范围,那么计算结果就会溢出(overflow),并且返回一个仍属于合法范围内的错误结果。 - 浮点型的特殊值(Special values in floating-point types)。像
double
这样的浮点类型有几个特殊的非实数变量值:NaN
,POSITIVE_INFINITY
,和NEGATIV_INFINITY
。所以当计算过程发生除零错误或者要对一个赋值开二次方的话,就会得到这些特殊值而不是正确答案,但它们都有各自的含义。
数组和集合(Arrays and Collections)
回到之前提到的雹石序列,如果我们想把雹石序列的元素保存起来,而不是输出出来,可以用Java的两个列表形式的数据类型:数组(arrays)和列表(Lists)。
数组是泛型T的定长序列,不如就理解为定长数组就好了。声明一个定长数组变量可以:
int[] a = new int[100];
数据类型int[]
是一个数组类型,一旦声明,就不能再改变它的长度了。基于数组的操作有:
- 索引取值:
a[2]
- 赋值:
a[2]=0
- 获取长度:
a.length
(注意这个语法和String.length()
是不一样的,a.length
不是一个函数调用,后面不加括号)
用数组来实现雹石序列生成并存储的一个实例:
int[] a = new int[100]; // <==== DANGER WILL ROBINSON
int i = 0;
int n = 3;
while (n != 1) {
a[i] = n;
i++; // very common shorthand for i=i+1
if (n % 2 == 0) {
n = n / 2;
} else {
n = 3 * n + 1;
}
}
a[i] = n;
i++;
这个版本的代码有一些不好的地方。如果雹石序列非常长,比开始声明的数组最大长度还要长,就会出现bug了。这种bug在C和C++语言中是非常常见的,因为这两种语言的安全性不是很好,运行时也不检查数组的访问权限,很有可能在数组越界时发生缓冲区溢出的错误,这种错误通常是致命的,在网络安全中非常危险。
用List
类型来声明一个变量用来保存序列,可以声明一个变长的泛型T的序列,可以理解为变长数组。要声明一个List
类型变量,可以:
List<Integer> list = new ArrayList<Integer>();
下面是基于变长数组的一些操作:
- 索引取值:
list.get(2)
- 赋值:
list.set(2, 0)
- 获取长度:
list.size()
注意List
类型是一个接口,这个类型不能直接用new
来构造。ArrayList
是一个类,一个提供以上操作的明确类型,List
类型中不止有ArrayList
类,但这是最常用的。
还要注意的是写法List<Integer>
而不是List<int>
。虽然在Java中Integer和int这两种写法在某些情况下是互通的,比如Integer i = 5 //相当于int i = 5
,但实际上,Integer的含义是一个对象类型,而int是基本数据类型。而列表只能识别对象类型而不能识别基本数据类型,要注意,Integer和int还是有区别,一些情况下Java忽略它们的区别从而自动转换,另一些情况下Java会报错。
用列表来实现雹石序列的生成和存储如下:
List<Integer> list = new ArrayList<Integer>();
int n = 3;
while (n != 1) {
list.add(n);
if (n % 2 == 0) {
n = n / 2;
} else {
n = 3 * n + 1;
}
}
list.add(n);
向列表类型变量中添加元素通常是无限制的,除非没有更多的内存了。
迭代(Iterating)
在前面雹石序列的前提下,对列表的一种遍历方式:
// find the maximum point of a hailstone sequence stored in list
int max = 0;
for (int x : list) {
max = Math.max(x, max);
}
对数组遍历也可以用上述模式。
方法(Methods)
Java中,语句必须在一个方法里,每一个方法必须在类里,所以最简单的方式来实现雹石序列程序是这样的:
public class Hailstone {
/**
* Compute a hailstone sequence.
* @param n Starting number for sequence. Assumes n > 0.
* @return hailstone sequence starting with n and ending with 1.
*/
public static List<Integer> hailstoneSequence(int n) {
List<Integer> list = new ArrayList<Integer>();
while (n != 1) {
list.add(n);
if (n % 2 == 0) {
n = n / 2;
} else {
n = 3 * n + 1;
}
}
list.add(n);
return list;
}
}
这里有一些新概念。
public
意味着程序中的全局代码都可以访问public修饰的类或方法。其它的权限修饰符,比如说private可以提高被修饰者在程序中的安全级别,保证不可更改。
static
意味着方法不能用self
作为参数,这点是和Python作区分的,在Java中,这点是隐式存在的,绝不会看到一个self
作为一个方法的参数。静态方法也不能被对象调用。像List
的add()
方法和String
的length()
方法的前面都得有一个对象调用它们,这些就不是静态方法。正确调用静态方法的方式是用类调用它们,而不使用对象调用:
Hailstone.hailstoneSequence(83)
另外给方法写注释笔记也是很重要的习惯。上述代码对方法的注释标记了操作的输入和输出。这些注释中最好体现出那些代码中不容易体现出的信息,比如说一个整型变量,可以是正的也可以是负的,但在该代码实例中,它只能是正的,否则不符合实际。
变值与可重赋值变量(Mutating Values vs. Reassigning Variables)
改变变量和改变变量的值是不一样的。给一个变量赋值的时候,可以改变变量的指针,把它指向一个不同的值。
改变一个变量的值,比如说改变数组或者列表中的值,都是改变变量中的内容。但改变可能会带来麻烦,最好避免那些会意外改变的东西。
不可变类型是指一旦创建后,就不能再改变值的类型,这种改变至少可以说是对外界不可见的,也不是绝对不可改变的。
Java对不可变类型的声明可以用关键字final来修饰。
final int n = 5;
如果在程序中被final修饰的变量被赋值超过一次,那么编译器就会检测到并报错。