哈工大软件构造学习心得from 01 静态检查

01 静态检查

冰雹系列

定义:从正整数n开始,如果n是偶数,则下一个数是n/2,否则下一个数是3n+1,直到n等于1。
哈工大软件构造学习心得from 01 静态检查_第1张图片

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和Python语法上存在的区别

1.Java和Python的基本语法很相似,例如while 和 if
2.Java在每一个句子结束时要求以分号作为结尾。这看起来有些麻烦,更方便安排代码——例如你可以将一行代码写成两行然后以分号结束。
3.Java在使用 if 和 while需要添加圆括号。
4.Java使用花括号将一个语句块分割开来——python是以缩进作为区分。编程是一种交流,不仅要和编译器交流,还要和别的程序员交流,所以缩进也是必要的。

数据类型

Python和Java最大的不同就是Java需要指定变量n的类型:int( 即需要指定n的数据类型)
一个数据类型是一系列值的集合,以及这些值对应的操作。
eg 5种常用的原始类型 :
int :例如5和-200这样的整数,但是必须在2^31之间,或者说是大概在±20亿
long:比int更大范围的整数,大概至2^63
boolean:对或错这两个值 true false
double :浮点数,其表示的是实数的子集
char:单个字符例如 ‘A’ 和 ‘$’
String 表示一串连续的字符。
BigInteger 表示任意大小的整数。
从Java来说,原始类型用小写字母,对象类型的大写字母开头。

操作符是一些能接受输入并输出结果的功能。句法虽然有所区别但是仍然认为他们是函数。下面是java和Python中三种不同的书写方式:
1.前缀、中缀、后缀操作符。 例如, a + b 调用这样一种操作(映射) + : int × int → int
( + 是这个操作符的名字, 箭头之前的int × int 描述了这两个输入, 最后的 int 描述了输出)
2.一个对象的方法. 例如, bigint1.add(bigint2) 调用这样一种操作(映射) add: BigInteger × BigInteger → BigInteger
3.一个函数. 例如: Math.sin(theta) 调用这样一种操作(映射) sin: double → double. 注意, Math 不是一个对象,它是一个包含sin函数的类。

比较Java 中**str.length()和 Python的len(str)**虽然都是输入一个字符串然后返回他的长度,但是却运用了不同的句法。

静态类型

Java是一种静态类型的语言。在编译阶段(程序运行之前)所有的变量的类型都是已知的,因此编译器也可以推断所有表达式的类型。
eg:
如果a和b是int类型的,那么编译器就可以知道a+b是int类型的。idea和Eclipse编译环境在写代码时如果出错就会发现。

动态类型的编程语言(Python)中,这种类型的检查是被推迟直到运行时才做的
静态类型是特殊类型的静态检查,这意味着在编译时检查bug。静态检查是为了避免bug的发生。静态类型阻止了许多类型相关的bug:准确的说,是由于将操作符用到了不对应的类型对象上上。如果你写了这样一行不完整的代码:

"5" * "6"

对两个字符串进行乘法操作,然后静态检查就会在你还在编程的时候提示你这个错误,而不是等到这一行被执行的时候才说。

静态检查,动态检查,无检查

三种自动检查方法:
静态检查:bug在程序还没有被执行的时候被自动地检查出来。----》运行前
动态检查:bug在程序正在被执行的时候被发现 --------》运行中
无检查:编程语言根本不帮助你找到bug。你必须自己认真检查,不然就会最终得到错误的程序。
三个自动检查比较:静态捕获bug比动态捕获它要好,而动态捕获比根本不捕获它要好。

下面是一些各种类型的检查能检查出来的错误:
静态检查:

  • 语法错误,例如多余的标点符号或者是错误的关键词。动态类型的语言像Python也会做这种类型错误的检查。如果你在你的Python程序中有一个多余的缩进,你会在程序执行之前发现出错。(编译不会通过)
  • 错误的名字,例如Math.sine(2).(正确的应该是sin)
  • 错误的参数个数,例如Math.sin(30,20))误的参数形式,例如Math.sin(“30”)
  • 错误的返回类型,例如从一个应该返回int类型的函数中return “30”。

动态检查

  • 非法变量值:例如,表达式x/y只有当y为0的时候才会报错;否则他都是合法的。因此,在这个表达式中除以0不是一个静态错误而是一个动态错误.
  • 无法表示的返回值,例如最后得到的返回值无法用声明的类型来表示。
  • 越界访问,例如在字符串中使用负数索引。
  • 对null对象引用调用方法(null 相当于Python中的None)

静态检查倾向于类型错误,与变量的特定值无关。一个类型是一组值的集合,静态类型可以保证变量将从该集合中得到一些值,但是我们直到运行时才知道它到底有哪些值。因此,如果错误只会由某些值引起,比如除以零或索引超出范围,那么编译器就不会产生关于它的静态错误。
相比之下,动态检查往往是由特定值引起的错误

原语类型不是真数
Java(许多其他的编程语言也一样)中的一个陷阱是它的原始数值类型的对象并不像我们熟悉的整数或者实数那样得到应有的输出。导致一些本应该被动态检查出来的问题没有被发现,下面是一些例子:

  • 整数除法:5/2不返回一个分数,而是返回一个被截断了的整数。所以这就是我们以为会出现动态错误的地方却没有被发现(因为分数不能表示为整数
  • 整数溢出:int 类型和long类型都是整数的有限子集,都有它们的范围。当你做一个运算的结果太正或者太负,不适合那个有限的范围会怎样的呢?计算悄然溢出,并且从那个有限的范围中返回一个不正确的答案。
  • 浮点类型中的特殊值:浮点类型像double类型有一些不是真实的数字的特殊值:NaN(“Not a Number”),正无穷, and 负无穷。所以当你做浮点类型的数的计算时,你的错误像除零或者是给负数开平方根,不会被检查出来,而是会返回一个特别的答案。

数组和集合

让我们更改冰雹序列的计算方法,以便将序列存储在一种数据类型中,而不是只将它打印出来。Java有两种我们能够使用的列表类型的数据类型:数组和列表

数组类型

数组是另一种类型的固定长度的序列,例如,下面是如何声明一个数组变量。

int[] a = new int[100];

int[ ]包括所有长度的数组,但是一个特定数组,一旦被创建就确定了长度。数组类型上的操作包括:

  • 索引其中一个元素:a[2]
  • 为其中一个元素赋值:a[2]=0
  • 查看数组长度:a.length(与String.length()不同, a.length不是一个方法调用,因此在此之后不用括号)

下面是利用数组写的冰雹代码,它存在一些bug。

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++;

在这个例子中,我们可以发现有些东西不太对劲,为啥数组的长度是100(100称为幻数)?如果我们尝试的n的冰雹序列长到大于100的话就无法使用这个数组。万一我们犯了错误,Java是否能够静态地、动态地检查出这个错误或者根本不检查?偶然地,像这样一个固定长度的数组的溢出在像C或者C++这样不太安全的语言中是非常常见的,而且被称为缓冲区溢出。这种溢出是大量网络安全漏洞和网络爬虫的罪魁祸首。

集合类型

试试List类型而不是固定长度的数组,List类型是不定长度的,下面看我们如何声明一个List类型的变量:

List<Integer> list = new ArrayList<Integer>();

下面是List类型的一些操作:

  • 查看任意元素的数值:list.get(2)
  • 修改任意元素的值:list.set(2,0)
  • 获得List的长度:list.size()
    这里要注意List是一个接口,不能够直接用new来构造,必须用能够实现List要求满足的操作符的方法来构造。我们将在未来的抽象数据型课程中讲到这点。ArrayList是一个类,是提供这些功能的具体类型。ArrayList不是唯一的提供者,但是是最常用的一个。在Java API文件中均可查到。
    注意我们写的是List< Integer >而不是List< int>,不幸的是我们不能直接模仿int[]那样写List< int>。List只知道如何处理对象类型,而不知道原始类型。在Java中,每个原语类型(它们是用小写写的,通常是缩写的,比如int)都有一个等效的对象类型(这个对象类型是大写的,并且拼写完整,就像Integer一样)。Java要求我们在参数化带有尖括号的类型时使用这些对象类型等价物。但是在其他情况下,Java会自动在int和Integer之间进行转换,因此我们可以编写Integer i=5,而不会出现任何类型错误。

下面是用列表编写的冰雹代码:

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);

迭代

for循环遍历数组或列表的元素,就像Python中的那样,尽管语法看起来有点不同。例如:

// find the maximum point of a hailstone sequence stored in list
int max = 0;
for (int x : list) {
    max = Math.max(x, max);
}

你可以遍历数组和列表。如果将列表替换为数组,则相同的代码将工作。
Math.max()是JavaAPI中的一个函数。Math类中有很多有用的函数。

方法

在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、private、static的定义
public意味着程序中任何地方的任何代码都可以引用这个类或这个类中的方法。
其他访问修饰符(如private)用于在程序中获得更多的安全性。并确保不可变类型的不可变性。
static意味着该方法是一个不接受Self参数的函数(在Java中它是一个名为this的隐式参数,你永远不会将它看作一个方法参数)。静态的方法不能通过对象来调用,例如List add()方法或者 **String length()**方法,它们要求先有一个对象。静态方法的正确调用应该使用类来索引,例如:

Hailstone.hailstoneSequence(83)

另外,记得在定义的方法前面写上注释。这些注释应该描述了这个方法的功能,输入输出/返回,以及注意事项。记住注释不要写的啰嗦,而是应该直切要点,简洁明了。例如在上面的代码中,n是一个整型的变量,这个在声明的时候int已经体现出来了,就不需要进行注释。但是如果我们设想的本意是n不能为负数,而这个编译器(声明)是不能检查和体现出来的,我们就应该注释出来,方便阅读理解和修改。

变化的值、可被赋值的改变

“快照图”(snapshot diagrams)用来辨别修改一个变量和修改一个值的区别。给一个变量赋值:改变这个变量指向的对象(值也不一样)。
对一个可变的值进行赋值操作:例如数组或者列表,实际上是在改变对象本身的内容。
变化是“邪恶”的,好的程序员会避免可改变的东西,因为这些改变可能是意料之外的。
不变类型是指那些这种类型的对象一旦创建其内容就不能被更改的类型。
Java也给我们提供了不变的索引:声明的时候加上final,变量被初始化后就不能再次被赋值了 :

final int n = 5;

如果编译器发现你的final变量不只是在初始化的时候被“赋值”,那么它就会报错。换句话说,final会提供不变索引的静态检查。
正确的使用final是一个好习惯,就好像类型声明一样,这不仅会让编译器帮助你做静态检查,同时别人读起来也会更顺利一些。
在hailstoneSequence方法中有两个变量n和list,我们可以将它们声明为final吗?n不行,list可以。因为我们需要改变n指向的对象,而List对象本身的内容是可以更改的,我们也不需要改变list对应的对象

public static List<Integer> hailstoneSequence(final int n) { 
    final List<Integer> list = new ArrayList<Integer>();

黑客派(Hacking)和 工程派(Engineering)比较

黑客派的编程风格可以理解为“放飞自我并且乐观的”:
缺点: 在已经编写大量代码以后才测试它们
缺点: 将所有的细节都放在脑子里, 以为自己可以永远记住所有的代码, 而不是将它们编写在代码中
缺点: 认为 BUG 都不存在或者它们都非常容易发现和被修复.
工程派对应的做法是:
优点: 一次只写一点点, 一边写一边测试. 在将来的课程中, 我们将会探讨"测试优先编程" (test-first programming)
优点: 记录代码的设想、意图 (document the assumptions that your code depends on)
优点: 静态代码检查将会保护你的代码不沦为“愚蠢的代码”

总结

我们今天的主题是静态检查,下面是这一主题与这门课的关联:

  • 帮助我们远离bug
  • 易于阅读
  • 易改动:静态检查会在你改写代码的同时检查出你与此同时犯的一些错误。例如,当你更改变量的名称或类型时,编译器会立即在使用该变量的所有位置显示错误,并提醒你更正这些错误。

你可能感兴趣的:(哈工大软件构造学习心得from 01 静态检查)