Skip to content

Latest commit

 

History

History
455 lines (295 loc) · 25.2 KB

File metadata and controls

455 lines (295 loc) · 25.2 KB

第6节 静态检查

❤️💕💕包含软件工程、算法与架构:设计模式、软件架构、协同开发、质量保障。更多关注我的博客:Myblog:http://nsddd.top


[TOC]

主要工作

这一节主要要完成的内容:

  • 静态类型
  • 好的软件具备的三大属性

冰雹序列

冰雹序列,其定义如下。从数字n开始,如果n 为偶数,则序列中的下一个数字为 n/2,如果 n为奇数,则为3n + 1。序列在达到 1 时结束。以下是一些示例:

2, 1 
3, 10, 5, 16, 8, 4, 2, 1 
4, 2, 1 
2 n , 2 n-1 , ... , 4, 2, 1 
5, 16, 8, 4, 2, 1 
7 , 22, 11, 34, 17, 52, 26, 13, 40, ...? (这在哪里停止?)

由于奇数规则,该序列可能会在下降到 1 之前上下反弹。据推测,所有冰雹最终都会落到地上——即,所有起始n的冰雹序列都达到 1——但这仍然是一个悬而未决的问题。为什么叫冰雹序列?因为冰雹在云层中通过上下弹跳形成,直到它们最终形成足够的重量以落到地球上。

计算冰雹

这是一些用于计算和打印某些起始n的冰雹序列的代码。我们将并列编写 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

### Python
n = 3
while n != 1:
    print(n)
    if n % 2 == 0:
        n = n / 2
    else:
        n = 3 * n + 1
print(n)

这里有几点值得注意:

  • Java 中表达式和语句的基本语义与 Python 非常相似:例如while,if行为相同。
  • Java 要求语句结尾处有分号。
  • if Java 需要在和的条件周围加上括号while。
  • Java 在块周围使用花括号,而不是缩进。您应该始终缩进该块,即使 Java 不会注意您的额外空格。编程是一种交流形式,您不仅要与编译器交流,还要与人类交流。

类型

上面的 Python 和 Java 代码之间最重要的语义区别是变量的声明n,它指定了它的类型:int

一个类型是一组值,以及可以对这些值执行的操作。

Java有几个原始类型, 其中:

  • int(对于像 5 和 -200 这样的整数,但限于大约 ± 2 31或大约 ± 20 亿的范围)
  • long(对于高达约 ± 2 63的较大整数)
  • boolean(判断真假)
  • double(对于浮点数,代表实数的子集)
  • char 'A'(对于像和这样的单个字符'$'

Java也有对象类型, 例如:

  • String表示一个字符序列,如 Python 字符串。
  • BigInteger表示任意大小的整数,因此它的行为类似于 Python 整数。

按照 Java 约定,原始类型是小写的,而对象类型以大写字母开头。

运营是接受输入并产生输出的函数(有时会改变值本身)。操作的语法各不相同,但无论它们如何编写,我们仍然将它们视为函数。以下是 Python 或 Java 中操作的三种不同语法:

  • 作为运营商。 例如,a + b调用操作+ : int × int → int。 (在这个符号中:+是操作的名称,int × int在箭头之前描述两个输入,int在箭头之后描述输出。)
  • 作为对象的方法。 例如,bigint1.add(bigint2)调用操作add: BigInteger × BigInteger → BigInteger
  • 作为一个函数。 例如,Math.sin(theta)调用操作sin: double → double。在这里,Math不是一个对象。它是包含sin函数的类。

对比 Javastr.length()和 Python 的len(str). 这在两种语言中是相同的操作——一个接受字符串并返回其长度的函数——但它只是使用不同的语法。噢对,JavaScript和Go语言….其他语言都有支持字符串长度的函数。

一些操作是重载从某种意义上说,相同的操作名称用于不同的类型。对于 Java 中的数字基本类型,算术运算符+-*/是重载的。方法也可以重载。大多数编程语言都有一定程度的重载(Go语言除外)。

静态类型

Java 是一个静态类型语言。所有变量的类型在编译时(程序运行之前)是已知的,因此编译器也可以推断出所有表达式的类型。如果ab被声明为int,则编译器得出结论a+b也是int。事实上,Eclipse 环境会在您编写代码时执行此操作,因此您会在仍在键入时发现许多错误。

int a,b;   //明确定义了a和b是int   a + b

动态类型像 Python 和JavaScript这样的语言,你不知道是什么类型,只有真正编译的时候知道。这种检查被推迟到运行时(程序运行时)。

a,b = 1;   #编译器:我不知道啊???

静态类型是一种特殊的静态检查,这意味着在编译时检查错误。错误是编程的祸根。我们的许多想法旨在消除代码中的错误,而静态检查是我们看到的第一个想法。

静态类型可以防止一大类错误感染您的程序:准确地说,是由于对错误类型的参数应用操作而导致的错误。

如果你写了一行代码,比如:

"5" * "6"

尝试将两个字符串相乘,然后静态类型将在您仍在编程时捕获此错误,而不是等到执行期间到达该行。

支持动态类型语言中的静态类型

尽管 Python 是动态类型的,但 Python 3.5 及更高版本允许您在代码中声明类型提示,例如:

# Python function declared with type hints
def hello(name:str)->str:
    return 'Hi, ' + name

声明的类型可以被Mypy 类的检查器用来静态查找类型错误,而无需运行代码。

或者是说python的缩进检查是可以的

其他动态类型语言也有类似的扩展。例如,JavaScript 已通过静态类型进行扩展以创建语言TypeScript,例如:

// TypeScript function
function hello(name:string):string {
    return 'Hi, ' + name;
}

在这些动态类型语言中添加静态类型反映了软件工程师普遍认为静态类型的使用对于构建和维护大型软件系统至关重要。本文的其余部分,实际上是整个课程,将说明这种信念的原因。与像 Java 这样从一开始就是静态类型的语言相比,将静态类型添加到动态类型的语言可以实现一种称为渐进类型的编程方法,其中代码的某些部分具有静态类型声明,而其他部分则省略它们。渐进式键入可以为小型实验原型成长为大型、稳定、可维护的系统提供更顺畅的路径。

静态检查、动态检查、不检查

考虑一种语言可以提供的三种自动检查是很有用的:

  • 静态检查: 甚至在程序运行之前自动发现错误。
  • 动态检查: 执行代码时会自动发现 bug。
  • 不检查:该语言根本无法帮助您找到错误。您必须自己注意,否则最终会得到错误的答案。

不用说,静态捕获一个 bug 比动态捕获它好,动态捕获它总比不捕获它好。

以下是一些经验法则,说明您可以预期在这些时间中的每一次捕获哪些错误。

静态检查可以捕获:

  • 语法错误,例如额外的标点符号或虚假词。甚至像 Python 这样的动态类型语言也会进行这种静态检查。如果你的 Python 程序中有缩进错误,你会在程序开始运行之前发现。
  • 拼写错误的名称,例如Math.sine(2). (正确的写法是sin。)
  • 参数数量错误,例如Math.sin(30, 20).
  • 错误的参数类型,例如Math.sin("30").
  • 错误的返回类型,例如return "30";来自声明为返回int.

动态检查可以捕获:

  • 非法的参数值。例如,整数表达式只有在实际为零x/y时才是错误的;y对于 的其他值y,其值是明确定义的。所以在这个表达式中,被零除不是静态错误,而是动态错误。
  • 非法转换,即当特定值不能转换为目标类型或在目标类型中表示时。例如,Integer.valueOf("hello")是动态错误,因为字符串"hello"无法解析为十进制整数。也是Integer.valueOf("8000000000"),因为 80 亿超出了intJava 的合法值范围。
  • 超出范围的索引,例如,在字符串上使用负数或过大的索引。
  • null在对象引用上调用方法(null就像 Python 的那样None)。

静态检查可以检测与变量类型相关的错误——允许它具有的值集——但通常无法从该类型中找到与特定值相关的错误。静态类型保证一个变量会从它的类型中获得一些值,但是直到运行时我们才知道它到底有哪个值。因此,如果错误仅由某些值引起,例如被零除或索引超出范围,则编译器不会引发有关它的静态错误。

相比之下,动态检查往往是关于由特定值引起的错误。

原始类型不是真数

Java 和许多其他编程语言中的一个陷阱是,它的原始数字类型具有与我们习惯的整数和实数不同的极端情况。结果,一些真正应该动态检查的错误根本没有被检查。以下是陷阱:

  • 整数除法5/2不返回分数,它返回一个截断的整数。所以这是一个例子,我们可能希望动态错误(因为分数不能表示为整数)经常产生错误的答案。
  • 整数溢出int和类型实际上是有限的long整数集,具有最大值和最小值。当您进行的计算结果太正或太负而无法适应该有限范围时会发生什么?计算悄悄地溢出(环绕),并从合法范围内的某个地方返回一个整数,但不是正确的答案。
  • 浮点类型中的特殊值。浮点类型double有几个不是实数的特殊值:(NaN代表“非数字”)POSITIVE_INFINITY、 和NEGATIVE_INFINITY. 因此,当您将某些操作应用于double您希望产生动态错误的 a 时,例如除以零或取负数的平方根,您将获得这些特殊值之一。如果你继续用它计算,你最终会得到一个糟糕的最终答案。

数组和集合

让我们改变我们的冰雹计算,以便它将序列存储在数据结构中,而不是仅仅将其打印出来。Java 有两种我们可以使用的类列表类型:数组和列表

数组是另一种类型的固定长度序列,例如整数。例如,下面是如何声明一个数组变量并构造一个数组值来分配给它:

int[] a = new int[100];

数组类型包括所有可能的int[]数组值,但是一个特定的数组值一旦创建,就永远不能改变它的长度。对数组类型的操作包括:

  • 索引:a[2]
  • 任务:a[2] = 0
  • 长度:a.length

请注意,这a.lengthString.length()区别。 因为a.length是实例变量,而不是方法调用,所以不要在它后面加上括号。

这是使用数组破解冰雹代码的方法。我们从构造数组开始,然后使用索引变量i遍历数组,在生成序列时存储它们的值。

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是什么?如果我们尝试一个n结果是一个很长的冰雹序列会发生什么?它不适合长度为 100 的数组。我们有一个错误。Java 会静态地、动态地捕获错误,还是根本不捕获?顺便说一句,这种错误被称为缓冲区溢出,因为它溢出了一个固定长度的数组。固定长度数组通常用于不太安全的语言,如 C 和 C++,它们不会对数组访问进行自动运行时检查。缓冲区溢出是造成大量网络安全漏洞和互联网蠕虫的罪魁祸首。

List让我们使用类型而不是固定长度的数组。列表是另一种类型的可变长度序列。下面是我们如何声明一个List变量并创建一个列表值:

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

以下是它的一些操作:

  • 索引:list.get(2)
  • 任务:list.set(2, 0)
  • 长度:list.size()

为什么List在左边却ArrayList在右边? List是一个接口,一种不能直接构造的类型,而是指定 aList必须提供的操作,例如get()andset()size()ArrayList是一个类,一个提供这些操作实现的具体类型。 ArrayList但是,它并不是该List类型的唯一实现;LinkedList是另一个。所以我们List在声明变量类型或返回类型时更喜欢该类型,因为它允许代码更通用和灵活,而不关心实际使用的是哪种具体类型的列表。在整个课程中,我们将多次重温这个接口和实现类的想法,所以如果你还没有深入理解它也没关系。

您可以查看 Java API 文档的所有操作或List详细信息:通过网络搜索“Java 15 API”找到它。了解 Java API 文档,它们是您的朋友。(“API”表示“应用程序编程接口”,这里指的是 Java 提供的用于帮助您构建 Java 应用程序的方法和类。)ArrayList``LinkedList


为什么List<Integer>而不是List<int>?不幸的是,我们不能写List<int>。**列表只知道如何处理对象类型,而不知道原始类型。**每个原始类型都有一个等效的对象类型:例如intand Integer, long and Long, float and Float, double and Double。当我们使用另一种类型参数化一种类型时,Java 要求我们使用这些对象类型等价物。但是在其他情况下,Java 会自动在 and 之间进行转换intInteger因此我们可以编写Integer i = 5;而不会出现任何类型错误。

Integer i = 5;
Double j = 10.0;

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

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循环遍历数组或 a 的元素,List就像在 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()是来自 Java API 的一个方便的函数。该类Math充满了像这样的有用功能——网络搜索“Java 15 Math”以找到它的文档。

方法

在 Java 中,语句通常必须在方法中,并且每个方法都必须在类中,因此编写我们的 hailstone 程序的最简单方法如下所示:

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意味着该方法是一个不带self参数的函数(在 Java 中称为this,并且是隐式传递的,因此您永远不会将其视为显式方法参数)。不会在对象上调用静态方法。List add()将其与方法或方法进行对比,String length()例如,要求对象先出现。相反,调用静态方法的正确方法是使用类名而不是对象引用:Hailstone.hailstoneSequence(83).

还要注意/** ... */方法前的蓝色注释,因为它非常重要。此注释是该方法的规范,描述了操作的输入和输出。规范应简洁、清晰、准确。注释提供了方法类型中尚不清楚的信息。例如,它没有说那n是一个整数,因为int n下面的声明已经说过了。但它确实说n必须是肯定的,这不是由类型声明捕获的,但对于调用者来说非常重要。

关于如何在几门课中编写好的规范,我们将有更多要说的,但是您必须立即开始阅读并使用它们。

变异值与重新分配变量

改变是必要的恶。但是优秀的程序员会尽量避免发生变化的事情,因为它们可能会发生意想不到的变化。不变性——故意禁止某些东西在运行时改变——将是本课程的主要设计原则。

例如,不可变类型是一种其值一旦被创建就永远不会改变的类型。字符串类型在 Python 和 Java 中都是不可变的。

Java 还为我们提供了不可变的引用:**分配一次且永不重新分配的变量。**要使引用不可重新分配,请使用关键字声明它final

final int n = 5;

如果 Java 编译器不相信您的final变量只会在运行时分配一次,那么它将产生编译器错误。因此final,您可以静态检查不可重新分配的引用。

final用于声明方法的参数和尽可能多的局部变量是一种很好的做法。与变量的类型一样,这些声明是重要的文档,对代码读者有用,并由编译器静态检查。

记录假设

写下变量的类型记录了一个关于它的假设:例如,这个变量总是引用一个整数。Java 实际上在编译时会检查这个假设,并保证在您的程序中没有任何地方违反了这个假设。

声明变量final也是一种文档形式,声明变量在初始赋值后永远不会被重新赋值。Java 也会静态地检查这一点。

我们记录了另一个假设,即 Java(不幸的是)不会自动检查:那n肯定是肯定的。

为什么我们需要写下我们的假设?因为编程充满了它们,如果我们不把它们写下来,我们就不会记住它们,以后需要阅读或更改我们程序的其他人也不会知道它们。他们将不得不猜测。

编写程序时必须牢记两个目标:

  • 与计算机通信。首先让编译器相信你的程序是合理的——语法正确且类型正确。然后让逻辑正确,以便在运行时给出正确的结果。
  • 与其他人交流。使程序易于理解,以便将来有人必须对其进行修复、改进或调整时,他们可以这样做。

黑客与工程

在这篇阅读文章中,我们编写了一些 hacky 代码。黑客通常以肆无忌惮的乐观主义为标志:

  • 不好:在测试任何代码之前编写大量代码
  • 不好:把所有细节都记在脑子里,假设你会永远记住它们,而不是把它们写在你的代码中
  • 不好:假设错误将不存在或者很容易找到和修复

但是软件工程不是黑客。工程师是悲观主义者:

  • 好:一次写一点,边做边测试。在以后的课程中,我们将讨论测试优先编程。
  • 好:记录您的代码所依赖的假设
  • 好:保护你的代码免受愚蠢——尤其是你自己的!静态检查对此有所帮助。

阅读练习

考虑以下简单的 Python 函数:

from math import sqrt
def funFactAbout(person):
  if sqrt(person.age) == int(sqrt(person.age)):
    print("The age of " + person.name + " is a perfect square: " + str(person.age))

6.031 的目标

我们在本课程中的主要目标是学习如何制作软件:

  • 远离错误。我们构建的任何软件都需要正确性(现在的正确行为)和防御性(未来的正确行为)。
  • 容易理解。代码必须与需要理解它并对其进行更改(修复错误或添加新功能)的未来程序员进行沟通。未来的程序员可能是你,几个月或几年后。如果你不把它写下来,你会惊讶于你忘记了多少,以及它对你未来的自己有多大的帮助来拥有一个好的设计。
  • 准备好改变。软件总是在变化。有些设计使更改变得容易;其他人需要丢弃并重写大量代码。

软件还有其他重要属性(如性能、可用性、安全性),它们可能会在这三个方面进行权衡。但这些是我们在 6.031 中关心的三巨头,软件开发人员通常将这些三巨头放在构建软件的实践中。值得考虑我们在本课程中学习的每一种语言特性、每一种编程实践、每一种设计模式,并了解它们与三巨头之间的关系。

<iframe class="exercises-status" src="https://6031.mit.edu/handx/sp21-java/status.php" style="box-sizing: border-box; width: 232.164px; height: 260px; border: none; position: absolute; right: -232.164px;"></iframe>

为什么我们在本课程中使用 Java

由于您拥有 6.009,我们假设您对 Python 感到满意。那么为什么我们不在本课程中使用 Python 呢?为什么我们在 6.031 中使用 Java?

安全是首要原因。Java 有静态检查(主要是类型检查,但也有其他类型的静态检查,比如您的代码从声明的方法返回值)。我们在这门课程中学习软件工程,避免错误是该方法的关键原则。Java 将安全等级提高到 11,这使其成为学习良好软件工程实践的好语言。使用 Python 等动态语言编写安全代码当然是可能的,但如果您学习如何使用一种安全的、经过静态检查的语言来编写安全代码,则更容易理解您需要做什么。

无处不在是另一个原因。Java 广泛用于研究、教育和工业领域。Java 可以在许多平台上运行,而不仅仅是 Windows/Mac/Linux。Java 在服务器端 Web 编程中很受欢迎,还有其他几种编程语言在 Java 虚拟机之上运行,其中包括 Scala、Clojure 和 Kotlin。原生 Android 编程是用 Java 和 Kotlin 完成的。尽管一些编程语言更适合教授编程(尤其是 Scheme 和 ML),但遗憾的是这些语言在现实世界中并没有那么普遍。你简历上的 Java 将被认为是一种有市场的技能。但不要误会我们的意思:您将从本课程中获得的真正技能不是特定于 Java 的,而是适用于您可能使用的任何编程语言。本课程中最重要的课程将在语言时尚中幸存下来:安全性,清晰、抽象、

无论如何,一个好的程序员必须是多语言的。编程语言是工具,你必须为工作使用正确的工具。在你完成 MIT 职业之前,你肯定必须学习其他编程语言(JavaScript、C/C++、Scheme 或 Ruby 或 ML 或 Haskell),所以我们现在开始学习第二门语言。

由于其无处不在,Java 拥有大量有趣且有用的(包括其庞大的内置库和网络上的其他库),以及出色的免费开发工具(IDE,如 Eclipse、编辑器、编译器、测试框架、分析器、代码覆盖率、样式检查器)。即使 Python 在其丰富的生态系统方面仍然落后于 Java。

有一些理由让你后悔使用 Java。它很罗嗦,这使得在黑板上写例子变得很困难。它很大,多年来积累了许多功能。它在内部是不一致的(例如,final关键字在不同的上下文中表示不同的东西,而staticJava 中的关键字与静态检查无关)。它承载了 C/C++ 等旧语言的包袱(原始类型和switch语句就是很好的例子)。

但总的来说,Java 是目前学习如何编写没有错误、易于理解和准备更改的代码的合理语言选择。这就是我们的目标。

概括

我们今天介绍的主要思想是静态检查。以下是这个想法与课程目标的关系:

  • 远离错误。 静态检查通过在运行前捕获类型错误和其他错误来帮助提高安全性。
  • 容易理解。 它有助于理解,因为类型在代码中明确说明。
  • 准备好改变。 静态检查通过识别需要同时更改的其他地方,使更改代码变得更加容易。例如,当您更改变量的名称或类型时,编译器会立即在所有使用该变量的地方显示错误,并提醒您也更新它们。

多练

如果您想对本文所涵盖的概念进行更多练习,可以访问[题库](https://qable.mit.edu:8001/practice.html#Static Checking)。该银行的问题是由学生和教职员工在上学期编写的,仅供复习使用——这样做不会影响您的课堂作业成绩。

END 链接