静态检查

阅读1:静态检查

今日课程的目标

今天的课程有两个主题:

  • 静态打字
  • 好的软件的三大特性

冰雹序列

与一些起始Ñ,序列中的下一个号码是N / 2,如果Ñ是偶数,或3N + 1如果Ñ为奇数。该序列在达到1时结束。以下是一些示例:

2,1 3,10,5,16,8,4,2,1 4,2,1 2 Ñ,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
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在语句末尾需要分号。额外的标点符号可能会很痛苦,但是它也为您提供了更多组织代码的自由–您可以将一条语句分成多行以提高可读性。
  • Java要求周围的条件括号if和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。
  • 作为对象的一种方法。 例如,bigint1.add(bigint2)调用操作add: BigInteger × BigInteger → BigInteger。
  • 作为功​​能。 例如,Math.sin(theta)调用操作sin: double → double。在这里,Math不是对象。是包含sin函数的类。
    将Javastr.length()与Python对比len(str)。两种语言中的操作都是相同的一个接受字符串并返回其长度的函数-但它只是使用不同的语法。

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

静态打字

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

在像Python这样的动态类型语言中,这种检查将推迟到运行时(程序正在运行时)。

静态类型化是一种特殊的静态检查,这意味着在编译时检查错误。错误是编程的祸根。本课程中的许多想法都是为了消除代码中的错误,而静态检查是我们所看到的第一个想法。静态类型可以防止大量的bug感染您的程序:确切地说,是由于对错误类型的参数执行操作而导致的bug。如果您编写像这样的折行代码:

"5" * "6"

尝试将两个字符串相乘,那么静态类型将在您仍在编程时捕获此错误,而不是等到执行过程中到达该行为止。

静态检查,动态检查,不检查

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

  • 静态检查:在程序运行之前会自动发现该错误。
  • 动态检查:在执行代码时会自动发现该错误。
  • 不检查:该语言根本无法帮助您找到错误。您必须自己当心,否则最终会得到错误的答案。
    毋庸置疑,静态捕获错误要比动态捕获更好,而动态捕获总比根本不捕获要好。

以下是一些经验法则,您可以预期在每次这些错误中会遇到什么错误。

静态检查可以捕获:

  • 语法错误,例如多余的标点符号或虚假单词。甚至像Python这样的动态类型语言也进行这种静态检查。如果您的Python程序出现缩进错误,则会在程序开始运行之前先进行查找。
  • 错误的名称,例如Math.sine(2)。(正确的名称是sin。)
  • 错误的参数数量,例如Math.sin(30, 20)。
  • 错误的参数类型,例如Math.sin(“30”)。
  • 错误的返回类型,例如return “30”;从声明为返回的函数中返回int。

动态检查可以捕获:

  • 非法参数值。例如,整数表达式x/y只有在y实际上为零时才是错误的。否则它会起作用。因此,在此表达式中,被零除不是静态错误,而是动态错误。
  • 无法表示的返回值,即当特定的返回值不能在类型中表示时。
  • 超出范围的索引,例如,在字符串上使用负数或太大的索引。
  • null对象引用上调用方法(null就像Python一样None)。
    静态检查通常是关于类型,与变量具有的特定值无关的错误。类型是一组值。静态类型保证变量将具有该集合中的某些值,但是直到运行时我们才确切知道它具有哪个值。因此,如果错误仅由某些值引起,例如被零除或超出范围的索引,则编译器不会对此引发静态错误。

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

惊喜:原始类型不是真数字

Java和其他许多编程语言的一个陷阱是,它的原始数字类型具有极端的情况,其行为不像我们过去所使用的整数和实数。结果,根本不检查应该真正检查的一些错误。这是陷阱:

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

阅读练习

让我们尝试一些错误代码的示例,看看它们在Java中的行为。这些错误是静态,动态还是根本不捕获的?

1个
2个
3
4
5

数组和集合

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

数组是另一种类型T的定长序列。例如,以下是声明数组变量并构造数组值以分配给它的方法:

int[] a = new int[100];

的int[]阵列型包括所有可能的排列的值,但一个特定的数组值,一旦创建,可以永远不会改变其长度。数组类型的操作包括:

  • 索引: a[2]
  • 任务: a[2]=0
  • 长度:(a.length请注意,这与String.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类型,而不是固定长度的数组。列表是另一种类型的变长序列T。这是我们如何声明List变量并产生列表值的方法:

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

这是它的一些操作:

  • 索引: list.get(2)
  • 任务: list.set(2, 0)
  • 长度: list.size()
    请注意,这List是一个接口,该类型不能直接使用构造new,而是指定List必须提供的操作。我们将在以后的抽象数据类型课程中讨论这个概念。 ArrayList是一类,是提供这些操作的实现的具体类型。 ArrayList尽管List是最常用的一种,但它不是List类型的唯一实现。 LinkedList是另一个。在Java API文档中查看它们,您可以通过在网络上搜索“ Java 8 API”来找到它们。了解Java API文档,他们是您的朋友。(“ API”的意思是“应用程序接口”,通常用作“库”的同义词。)

另请注意,我们写的List<Integer>不是List<int>。不幸的是,我们不能List直接模拟int[]。列表只知道如何处理对象类型,而不是原始类型。在Java中,每种基本类型(用小写字母表示,通常缩写int),都有一个等效的对象类型(用大写字母表示,并完全拼写出来,例如Integer)。Java要求我们在使用尖括号对类型进行参数化时使用这些对象类型等效项。但是在其他情况下,Java会在int和之间自动转换Integer,因此我们可以编写Integer i = 5而不会出现任何类型错误。

这是用Lists编写的冰雹代码:

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

这不仅更简单,而且也更安全,因为列表会自动放大以适应您添加到其中的数字(当然,直到内存用完为止)。

反复进行

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

// 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 8加减乘除”在网络上找到它的文档。

方法

在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表示该方法不带self参数-在Java中该参数是隐式的,您永远不会将其视为方法参数。静态方法不能在对象上调用。例如,将这种方法与要求对象首先出现List add()String length()方法进行对比。相反,调用静态方法的正确方法是使用类名称而不是对象引用:

Hailstone.hailstoneSequence(83)

还请注意该方法之前的注释,因为它非常重要。此注释是方法的规范,描述了操作的输入和输出。规范应简明扼要,准确。注释提供的信息尚未从方法类型中清除。例如,它没有说这n是一个整数,因为int n下面的声明已经说明了这一点。但是它确实说n必须肯定,这不能被类型声明捕获,但是对于调用者来说非常重要。

关于如何在几个类中编写良好的规范,我们还有很多话要说,但是您必须开始阅读并立即使用它们。

突变值与重新分配变量

在下一篇文章中,将介绍快照图,以便为我们提供可视化更改变量和更改值之间区别的方法。分配给变量时,您正在更改变量箭头指向的位置。您可以将其指向其他值。

当您分配可变值的内容(例如数组或列表)时,您正在更改该值内的引用。

改变是必不可少的罪恶。优秀的程序员应避免发生变化,因为它们可能会发生意料之外的变化。

不变性(不受更改的影响)是本课程的主要设计原则。不可变类型是其值一旦创建就永远不会改变的类型。(至少不以外界可见的方式-那里有一些微妙之处,我们将在以后的课程中讨论不可变性。)到目前为止,我们讨论了哪些类型是不可变的,哪些是不可变的可变的?

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

final int n = 5;

如果Java编译器不确信最终变量在运行时只会分配一次,那么它将产生编译器错误。所以final为您提供了对不可变引用的静态检查。

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

我们的hailstoneSequence方法中有两个变量:是否可以将它们声明为final?

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

记录假设

写下变量的类型会记录有关该变量的假设:例如,此变量将始终引用整数。Java实际上在编译时检查了该假设,并保证程序中没有违反该假设的地方。

声明变量final也是一种文档形式,声称变量在初始赋值后将永远不会改变。Java也会静态地进行检查。

我们记录了另一个假设,即Java(不幸的是)不会自动检查:n必须为正。

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

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

  • 与计算机通信。首先说服编译器您的程序是明智的–语法正确且类型正确。然后弄清楚逻辑,以便在运行时给出正确的结果。
  • 与其他人交流。使程序易于理解,以便当有人必须对其进行修复,改进或将来对其进行修改时,他们可以这样做。

黑客与工程

我们在此类中编写了一些hacky代码。骇客通常会表现出无比乐观的态度:

  • 不好:在测试任何代码之前都要写很多代码

  • 坏:假设所有细节都永远保留在您的脑海中,而不是在您的代码中写下来

  • 坏:假设错误将不存在,或者容易发现和修复
    但是软件工程不是黑客。工程师是悲观主义者:

  • 很好:一次写一点,在进行过程中进行测试。在以后的课程中,我们将讨论测试优先编程。

  • 很好:记录您的代码所依赖的假设

  • 良好:保护您的代码免受愚蠢之害-尤其是您自己的代码!静态检查对此有所帮助。

6.031的目标

本课程的主要目标是学习如何生产以下软件:

  • 安全的错误。正确性(当前正确的行为)和防御性(未来正确的行为)。

  • 容易理解。必须与需要了解它并进行更改(修复错误或添加新功能)的未来程序员进行沟通。从现在开始,几个月或几年后,将来的程序员可能就是您。如果您不写下来,您会感到惊讶,如果您不写下来,有多少会忘记,以及对您自己未来的自我有一个好的设计有多大的帮助。

  • 准备改变。软件总是在变化。有些设计使更改变得容易。其他人则需要扔掉并重写大量代码。
    软件还有其他重要属性(例如性能,可用性,安全性),它们可能会与这三个属性相抵触。但这是我们在6.031中关注的三大巨头,并且软件开发人员通常在构建软件的实践中将其放在首位。值得考虑的是我们在本课程中学习的每种语言功能,每种编程实践,每种设计模式,并了解它们与三巨头的关系。

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

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

安全是首要原因。Java具有静态检查(主要是类型检查,但也有其他类型的静态检查,例如您的代码从声明的方法中返回值)。我们正在本课程中研究软件工程,而避免错误的安全性是该方法的主要宗旨。Java将安全性提高到11,这使其成为学习良好软件工程实践的好语言。当然可以用动态语言(例如Python)编写安全的代码,但是如果您学习使用安全的,经过静态检查的语言,将更容易理解您需要做什么。

无处不在是另一个原因。Java广泛用于研究,教育和行业。Java在许多平台上运行,而不仅仅是Windows / Mac / Linux。Java可以用于Web编程(在服务器和客户端上),而本机Android编程是用Java完成的。尽管其他编程语言更适合于教学编程(我想到了Scheme和ML),但遗憾的是,这些语言在现实世界中并不那么广泛。简历上的Java将被认为是可销售的技能。但是请不要误解我们的意思:本课程将学到的真正技能不是特定于Java的,而是会延续到您可能会编程的任何语言。本课程中最重要的课程将在语言流行中生存:安全,清晰,抽象,工程本能。

无论如何,一个好的程序员必须是多语言的。编程语言是工具,您必须使用正确的工具来完成工作。在您结束MIT生涯之前,您肯定必须使用其他编程语言(JavaScript,C / C ++,Scheme或Ruby或ML或Haskell),因此,我们现在开始学习第二种语言。

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

有一些原因使您后悔使用Java。这是冗长的,这使得很难在板上编写示例。它很大,多年来积累了许多功能。它在内部是不一致的(例如,final关键字在不同的上下文中表示不同的事物,而staticJava中的关键字与静态检查无关)。它与诸如C / C ++之类的较早语言的包重在一起(原始类型和switch语句是很好的示例)。它没有像Python那样的解释器,您可以在其中使用少量代码来学习。

但是总的来说,Java现在是学习语言的一种合理选择,以学习如何编写可避免错误,易于理解且随时可以更改的代码。这就是我们的目标。

概括

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

  • 安全的错误。 静态检查通过在运行时捕获类型错误和其他错误来帮助提高安全性。
  • 容易明白。 因为类型在代码中明确声明,所以有助于理解。
  • 准备好进行更改。 静态检查可以通过识别需要串联更改的其他位置来更轻松地更改代码。例如,当您更改变量的名称或类型时,编译器会立即在使用该变量的所有位置显示错误,并提醒您也更新它们。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值