MIT6.031Software Construction学习笔记(一)

静态检查(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}

雹石序列不论以何数开头,总是以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/yy是0的情况,就是错误的,否则就是正确的。在这种情况下,除零不是静态错误,而是动态错误。
  • 返回值不匹配。这种错误主要是返回值的类型和赋值类型不匹配导致的。
  • 范围溢出。比如说在一个字符串中是用了负值索引,或者索引太大超出了字符串长度。
  • 在一个指向null的对象上调用了方法(null类比Python的None)。

静态检查主要是检查变量的值类型。数据类型都是变量值的集合。静态输入保证了一个变量的值是合法的,就是必须是从相应的数据集合中选择的,但我们得在程序运行的时候,才能知道这个变量的值到底是什么。所以如果即将发生的错误是由于变量的一部分取值导致的,像除零错误和范围溢出这样的,编译器是不会对其报静态错误的。

相反,动态检查主要检查的是由于特定值引起的错误。

意外:基本数据类型不是真的数字(Surprise: Primitive Types Are Not True Numbers)

包括Java在内的很多编程语言都有一个小陷阱,就是它们的基本数字数据类型都有特殊情况,就是和我们平时使用的整数和实数不太一样。这会导致一些本应该由动态检查出的错误,检查不出来。这些小陷阱有如下几个方面:

  • 整数除法(Integer division)5/2不会返回一个分数,而是返回一个被截断的整数2.这就是一个被动态检查忽略掉的错误。这个式子本应该是能动态检查出来的,可以这么理解,两个操作数都是整型,那么表达式也是整型,但表达式的值是浮点型,两种类型矛盾,出现了动态错误。但是,这种情况是不会报错的,而是直接输出错误答案2。
  • 整型溢出(Integer overflow)intlong类型能表示的整数范围都是有限的。如果一个计算结果太大或者太小,以至于超出了整型的表示范围,那么计算结果就会溢出(overflow),并且返回一个仍属于合法范围内的错误结果。
  • 浮点型的特殊值(Special values in floating-point types)。像double这样的浮点类型有几个特殊的非实数变量值:NaNPOSITIVE_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作为一个方法的参数。静态方法也不能被对象调用。像Listadd()方法和Stringlength()方法的前面都得有一个对象调用它们,这些就不是静态方法。正确调用静态方法的方式是用类调用它们,而不使用对象调用:

Hailstone.hailstoneSequence(83)

另外给方法写注释笔记也是很重要的习惯。上述代码对方法的注释标记了操作的输入和输出。这些注释中最好体现出那些代码中不容易体现出的信息,比如说一个整型变量,可以是正的也可以是负的,但在该代码实例中,它只能是正的,否则不符合实际。

变值与可重赋值变量(Mutating Values vs. Reassigning Variables)

改变变量和改变变量的值是不一样的。给一个变量赋值的时候,可以改变变量的指针,把它指向一个不同的值。

改变一个变量的值,比如说改变数组或者列表中的值,都是改变变量中的内容。但改变可能会带来麻烦,最好避免那些会意外改变的东西。

不可变类型是指一旦创建后,就不能再改变值的类型,这种改变至少可以说是对外界不可见的,也不是绝对不可改变的。

Java对不可变类型的声明可以用关键字final来修饰。

final int n = 5;

如果在程序中被final修饰的变量被赋值超过一次,那么编译器就会检测到并报错。

猜你喜欢

转载自blog.csdn.net/qq_41662115/article/details/86602711
今日推荐