1.1. 开始¶
1.1.1. 介绍 LightBot¶
LightBot 是一个非常简单的机器人编程模拟游戏。它直接在您的 Web 浏览器中运行,看起来如下所示:
在 LightBot 游戏中,您创建一个 程序 来控制一个小机器人,以解决 LightBot 所在的微型积木世界中的简单任务。LightBot 中的编程是图标式的。游戏显示右上方的图标代表给机器人的指令。每个图标指示机器人执行某个操作——向前移动一格、向右转、向左转、向前跳跃,以及点亮机器人所站立的方块。当然,只有当机器人站在蓝色方块上时,该方块才会被点亮。左上角的扬声器图标允许您切换游戏声音的开关。左下角的重置按钮将删除您的所有指令并重新开始关卡。
在 LightBot 游戏的每一关,你的任务都是让机器人点亮所有的蓝色方块。要创建一个控制机器人的程序,只需将顶部的图标拖拽并放置到主方法方块上,从左到右排列。
LightBot 游戏说明了几个基本的编程概念。机器人本身是一个 对象,我们可以请求它执行它知道如何完成的特定操作。我们可以将这些请求写成一系列 程序。我们写下的操作序列是解决特定问题(一个 算法)的计划。
1.1.2. 从 LightBot 到 Micro Worlds¶
LightBot 游戏是一个 微世界 的例子。微世界是一个微型模拟环境,其中包含一个或多个具有明确定义行为且能够以视觉方式表示的对象,以便您可以看到这些对象如何随时间移动和表现。
微世界通常使用 网格 来表示模拟世界中的位置。你可以在 LightBot 游戏中看到这一点,机器人走在方格瓷砖上。在本课程中,我们将采用相同的方法。我们可以使用 x 和 y 坐标来指代网格上的任何位置:
我们将使用笛卡尔坐标系的变体来将任何位置表示为 (x, y) 对,其中 x(第一个坐标)表示从原点水平方向的距离,y(第二个坐标)表示从原点垂直方向的距离。唯一的例外是,在我们的网格中 y 轴指向下方,这使得它成为一个 左手笛卡尔平面。我们使用左手方向的坐标系,因为这种约定在计算机编程环境中占主导地位(尽管它与研究几何学中通常的情况相反!)。
因此,上图网格中高亮的单元格位于位置 (4, 2),因为它距离原点水平向右 4 个单元格,垂直向下 2 个单元格。还要注意,原点 (0, 0) 在我们的网格中是显式表示的,并且位于 左上角。上图的网格大小为 10 乘以 10,其中坐标从 0 运行到包括 9.这意味着我们在引用网格位置时总是使用 基于零的索引——即,我们总是从原点(左上角)的零开始计数,而不是从一开始。
1.1.3. 自我检查:微世界¶
1.1.4. 再多一点 LightBot¶
如果您只玩了 LightBot 的前两个关卡,那么肯定还有一些有趣的玩法您尚未见识!随着关卡的推进,谜题会变得愈发复杂。由于用于小机器人的程序内存中的槽位数量有限,因此使用这些槽位来解题变得更加困难。幸运的是,LightBot 游戏为您提供了一些额外的图标供您使用。
LightBot 的原始版本提供了两个较小的指令区域:f1 和 f2。f1 和 f2 图标代表 LightBot 游戏中主方法下方的两个函数(图标区域)。您将在这些函数中放置机器人需要多次执行的指令。在任何放置 f1 或 f2 图标的位置,机器人都会执行与该函数关联的整个动作序列。这将使您能够构建更复杂的程序,从而解决后期关卡中更具挑战性的任务。
Try It Yourself
返回至 LightBot game。继续完成第 7 级。一旦完成第 7 级,即停止。您将在下一练习中返回完成第 8 级。
1.1.5. 用文本表示程序¶
计算机程序通常以文本形式编写——事实上,编程最重要的目标之一就是编写能够与其他人良好沟通的程序。因此,让我们看看如何将 LightBot 谜题的解决方案表示为文本。
在最基本的层面上,计算机程序仅仅是计算机需要遵循的一系列指令。我们的 LightBot 关卡解决方案正是如此:程序。为了以文本形式编写它们,我们只需要与图标相对应的名称。例如,如果我们称我们的机器人 andy,我们可以使用以下名称来对应他的图标(从左到右):
| Symbol | Textual Program Statement (Method Call) |
|---|---|
![]() |
andy.move(); |
![]() |
andy.turnRight(); |
![]() |
andy.turnLeft(); |
![]() |
andy.jump(); |
![]() |
andy.turnLightOn(); |
![]() |
andy.f1(); |
![]() |
andy.f2(); |
之前的每个动作都表述为对一个执行所需行为的对象的请求。因此,每个请求都发往我们的机器人 andy 。例如,考虑第 2 级:
该层的一个解决方案是:
andy.move();
andy.turnRight();
andy.move();
andy.turnLeft();
andy.move();
andy.move();
andy.turnLeft();
andy.move();
andy.turnRight();
andy.move();
andy.turnLightOn();
Try It Yourself
返回到 LightBot game。如果尚未完成,请完成第 7 级。一旦完成第 7 级,请仔细思考第 8 级的解决方案。编写第 8 级的解决方案,但不要运行它。在第 8 级的解决方案以文本形式写在草稿纸上。
写下你的解决方案后,然后运行它。如果它没有按你计划的方式运行,在再次运行之前,请修订它并修订你的文本版本。这将帮助你练习写下程序步骤。
1.1.6. 自测:将微观世界转化为代码¶
1.1.7. LightBot 关于编程说了什么?¶
LightBot 游戏说明了几个基本的编程概念:
对象
方法
方法调用
状态
算法
程序
源代码
解释器
在本课程中,我们将专注于学习 Java 中的 面向对象编程。本节描述了一些在第一个示例中见过的关于面向对象程序的一般概念。随着你阅读后续章节,请回看本章以复习重要词汇和短语的含义。
1.1.7.1. 程序与编程语言¶
无论是发送邮件、浏览网络、听音乐、撰写文章还是玩游戏,只要我们在使用计算机,我们就会使用一个或多个计算机程序。每个程序本质上都是计算机的一组指令。
备注
计算机 程序 是指计算机的一组指令。
每个程序都是由一个或多个程序员编写的。程序员使用一种编程语言,如 Java、C++、Python 或 Visual Basic 编写计算机程序。对于 LightBot,你首先使用了一种图标语言,然后学习了如何使用一种简单的文本语言来表示完全相同的操作。
程序以多种形式存在。程序员编写的那种形式称为程序的源代码。不幸的是,计算机不能直接使用源代码。源代码必须在计算机执行(运行)之前被翻译成机器语言。
备注
程序的 源代码 由程序员用某种编程语言编写。
存在几种类型的翻译。编译器将程序作为一个整体从一种形式转换为另一种形式,但不一定是转换为机器语言。解释器将程序逐条语句地翻译成机器语言。每条语句在翻译完成后立即执行。LightBot 是一个解释器的例子,因为它将每个源代码动作逐条语句地直接转换为计算机行为。
备注
编译器 将一个程序作为一个整体,从一种形式转换为另一种形式。
解释器 将程序逐条语句地翻译成机器语言。
1.1.7.2. 算法¶
每个计算机程序都始于一个计划。该计划被称为算法。编写算法的方法有很多种。有些非常非正式,有些相当正式且具有数学性质,还有些相当图形化。只要它能提供一种描述和检查计划逻辑的良好方式,形式并不特别重要。
备注
一个 算法 是解决问题的计划。
1.1.7.3. 对象¶
了解到面向对象编程语言与对象一起工作,这并不令人惊讶。但什么是对象?不幸的是,这个概念很难定义,因为对象既是仅存在于计算机程序中的事物,又是其他事物的逻辑表示。一种好的方法是根据对象所代表的内容来定义对象。
备注
一个 对象 代表与我们试图解决的问题相关的特定概念或项目。
一个典型的程序会使用多种不同的对象。其中一些可能代表同一种类的事物。例如,在 LightBot 中,机器人本身显然是一个对象。但也有一些砖块会阻挡机器人的路径,并且可以堆叠。还有一些可以点亮的蓝色方块。一个关卡中可能会有多个砖块,或多个蓝色方块,它们都是独立的对象,尽管它们属于同一种类。
对象代表某事物,但作为程序员,我们需要确定该事物的哪些特征对于我们要解决的问题是重要的。对象有两部分:关于其所代表事物的事实(蓝色方块已点亮,还是仍然暗淡?机器人面向哪个方向?)以及对象可以执行的任务(机器人可以移动、向左或向右转等)。这些事实称为属性(我们稍后会介绍),这些任务称为方法。
1.1.7.4. 方法和行为¶
在设计一个对象时,我们需要确定它应该执行哪些任务。在此过程中,我们倾向于将对象所代表的事物拟人化。(拟人化意味着将人类的特征赋予非人类的事物。)例如,我们可能希望 LightBot 从一个位置移动到另一个位置,或者跳到一个障碍物上。
备注
行为 是对象可以执行的动作,或者在响应来自外部源的请求时可以执行的任务。
方法 是用某种编程语言编写的一系列语句,用于描述特定行为。
方法的 前置条件 是在调用该方法之前假设成立的某些条件。
方法的一个 后置条件 是在该方法执行完成之后被假定为真的一些东西。
例如,我们的 LightBot 支持多种方法,这些方法对应于我们可以在其中使用的图标,并且我们在上一节中为它们赋予了文本名称。此外,某些方法仅在特定条件下有效:机器人只有在正前方紧邻的方块高度恰好比其站立位置高一个或低一个方块时,才能跳跃(向上或向下)。这是一个 前置条件。同样,点亮当前方块的图标仅在机器人站立在蓝色方块上时才有效(这也是一个前置条件)。然而,如果机器人确实站立在蓝色方块上,然后执行其“打开灯光”的行为,那么之后该蓝色方块将被点亮(这是一个后置条件,它描述了执行特定行为或方法后的结果)。
1.1.7.5. 消息(调用方法)¶
当我们编写面向对象程序时,我们会实例化适当的对象并请求它们执行特定任务。 我们使用消息来发出这些请求。
备注
一条 消息 是对特定对象执行特定任务的请求。
当我们要求一个对象执行某项任务时,我们说我们 发送了一条消息 或 调用了描述该任务的方法 。
1.1.8. Java 中的可编程 LightBot¶
1.1.8.1. LightBot 世界自上而下¶
在上面的章节中,你玩了 LightBot 游戏,甚至看到了小机器人所遵循的图形命令的文本表示。现在是将这些概念转化为一个完整的程序的时候了,而这个程序你可以自己编写。
首先,回顾一下,LightBot 本质上只是一种 微世界:每个“关卡”都是游戏的一个微型世界,可以表示为一个网格,所有对象——机器人、方块、会发光的蓝色瓷砖——都占据网格上的不同位置。该 Flash 游戏使用等轴测投影使世界看起来更具三维感,并使用动画使其更具吸引力。例如,这是您在第 1 章中看到的 LightBot 游戏的第二关:
然而,为了在一定程度上简化问题,让我们使用同一情况的普通自上而下的视图:
在这种自上而下的视角中,更容易看出哪个方向是 x 方向,哪个是 y 方向,因此我们可以保持坐标清晰。此外,这张图强调了每个 LightBot 关卡都是一个 8 x 8 的网格。作为另一个例子,LightBot 游戏的第三关在原图中看起来是这样的:
但同一层也可以像这样自顶向下地表示:
现在我们已经弄清楚了我们的“世界”,我们可以讨论编写源代码来控制该世界中的机器人了。
1.1.8.2. LightBot 的方法¶
让我们从回顾 LightBot 理解的所有基本命令开始讨论编写 LightBot 程序。之前,我们了解到 LightBot 知道如何执行七种基本操作或行为:
| Method | What Happens |
|---|---|
move() |
The robot moves forward one square (if it can) |
turnRight() |
The robot turns 90 degrees to its right (clockwise) |
turnLeft() |
The robot turns 90 degrees to its left (counterclockwise) |
jump() |
The robot moves forward by jumping up one block higher, or by jumping down one or more blocks lower (if it can) |
turnLightOn() |
The robot lights up the blue tile, if it is standing on one |
f1() |
The robot carries out whatever sequence of actions you have defined for the method f1 |
f2() |
The robot carries out whatever sequence of actions you have defined for the method f1 |
在面向对象的程序中,我们会将 LightBot、其世界(我们当前游玩的关卡)、世界中的方块以及蓝色瓷砖建模为 对象。每个对象提供一组它所能理解的行为,这些行为通过 方法 实现。因此,上述 LightBot 所理解的七种行为即为其方法。当我们 调用一个方法 (这与 调用一个方法 或 发送消息 是同一回事),我们是在请求某个对象执行一个我们通过名称指定的特定方法。当然,要调用一个方法,我们必须确切知道我们希望哪个对象执行该操作。与 调用方法 或 发送消息 相同),我们请求对象执行我们通过名称标识的特定方法。当然,要调用一个方法,我们必须确切知道我们想要执行该操作的哪个对象。
1.1.8.3. 我们在哪里写入它?¶
我们编写的所有程序代码都必须存放在某个地方。 但是我们要把它放在哪里呢? 在面向对象的程序中,所有程序语句都放在 方法 内,而每个方法都属于一个对象(或属于一个 类,即一组理解相同方法的对象家族)。
本课程将使用的编程环境称为 Eclipse。我们还在使用来自 Greenfoot 的类库,这是一个允许我们与许多种微世界进行工作的框架。
当您在 Eclipse 中打开一个项目时,您将在左侧的 Package Explorer 视图中看到正在处理的各个 Java 类的列表。在这里,我们可以看到 Eclipse 的主窗口,它正在查看一个用于解决 LightBot 第 3 关的简单 Java 项目。
您可以双击项目中的任何 Java 类,在编辑器中打开它并查看其程序代码。对于我们的微型世界作业,您还将看到一个以 Run 开头的类,我们将用它来启动和运行程序(完整说明见第一个实验作业)。
运行任何微世界程序时,您都会看到显示的世界。显示区域的主部分由世界的俯视视图组成,在本例中为 LightBot 游戏的第 3 层。您还将看到位于底部的控制项,用于 Act (使对象执行其下一个动作)、Run (使对象重复执行动作)以及控制执行速度。
您编写的每个 Java 类都将在一个文本文件中存储其源代码,该文件的名称与类名相同,并带有".java"扩展名。我们称这些为“源文件”,因为它们包含定义您类的 Java“源代码”。通过在 Eclipse 的 Package Explorer 中双击源文件名,您可以在编辑窗口中查看(并编辑)您的类源代码。如果您双击 Level3Solution.java,您将看到其内容:
在编辑器中,请注意已经编写了一个用于存放新代码的方法。此方法名为 myProgram(),它表示当您按下程序窗口底部的 Run 按钮时将执行的行为。该方法具有以下结构:
1.1.9. 语法练习 1a¶
1.1.10. 创建新对象¶
为了调用对象上的方法来解决一个问题,我们首先需要有一个对象!这意味着创建解决问题所需的对象是我们构建解决方案工作的一部分。
备注
创建新对象称为 实例化。
有时,我们只需要创建一个对象,而该行为本身就会完成我们需要的工作。但大多数时候,我们创建对象是为了要求它执行某些操作——调用其方法。为了在对象上调用方法,我们需要某种方式来指代我们正在与之对话的对象。在大多数情况下,我们通过引入一个指代该对象的 名称 来实现这一点。这些名称被称为 变量 (尽管我们稍后会对此进行更精确的说明)。
备注
程序中引入一个新名称是在一个 声明 中。
这两种任务——声明一个新名称和创建一个新对象——通常是一起进行的,而 Java 为我们提供了一种方便的方式来表达这种两部分的模式:
new 运算符是用于声明新名称并创建新 LightBot 对象的语法。声明部分表明程序员计划使用 LightBot 来帮助解决手头的问题。程序员必须为 LightBot 对象提供一个标识符(或名称)。
备注
Java 中的 标识符 (或名称)应以字母开头,其后可以跟随其他字母或数字。
当我们为变量或方法命名时,我们将使用 小写字母 作为标识符的第一个字母。
当我们为 类 (类是一组理解相同方法的对象家族,例如
LightBot,它代表我们可以创建的所有LightBot对象——它们都理解相同的七个基本方法)命名时,我们将使用 大写字母 作为标识符的第一个字母。Java 也允许在标识符中使用下划线 (_) 和美元符号 ($),但本课程中我们不会使用它们。
上图中的 实例化部分 是一个请求,要求创建 LightBot 对象。实例化的关键部分是 构造函数,这是一种仅用于创建新对象的特殊方法。此图中的构造函数形式为 LightBot()。它通过给出类的名称——此处,我们正在创建一个新的 LightBot——后跟一对括号,来指定我们要创建的对象的类型。在 Java 中,当调用方法或调用构造函数时,名称后总是使用括号。稍后,我们将看到如何在括号内提供值以定制正在构造的对象。
假设我们想要创建一个新的 LightBot,并希望用名字 andy (一个适合安卓机器人的名字)来引用它。那么我们可以声明名字 andy,并使用以下语句创建 LightBot:
LightBot andy = new LightBot();
我们可以将此内容放入我们的 myProgram() 方法中:
public void myProgram()
{
LightBot andy = new LightBot();
// ... more goes here ...
}
虽然这创建了一个新的 LightBot,但我们尚未说明在何处将其放置于世界中。假设我们想将 andy 添加至世界中的位置 (0, 4)。我们可以使用所有世界对象都理解的一个名为 add() 的方法来实现,如下所示:
public void myProgram()
{
LightBot andy = new LightBot();
this.addObject(andy, 0, 4);
// ... more goes here ...
}
现在该尝试运行你的第一个程序了!你将在第一次实验作业中找到与此类似的程序运行说明。
是的,这并不令人兴奋,但你会看到一个新的 LightBot 被创建并添加到你所指定坐标的关卡中。你可以使用窗口底部的“Reset”(重置)按钮丢弃当前世界,重新开始一个全新的副本;也可以使用“Run”(运行)再次运行你的程序。“Act”(执行)按钮将单步执行你的程序,一次执行一个动作,依次顺序地执行 myProgram(),适合你想以超慢速查看会发生什么的时候。另外,你还可以使用速度滑块来加快或减慢代码运行时动作执行的速度。
1.1.11. 在对象上调用方法¶
好的,我们已经创建了一个 LightBot,但它的作用并不大。我们如何请求它执行操作?操作语句是要求对象执行特定任务的请求。该任务可以是对象已知的某个基本操作方法之一,也可以是你自己编写的某个新方法。操作语句的语法如下:
动作语句也称为 方法调用 语句,因为它会调用对象上的一个方法。点号左侧的 标识符 指明哪个对象将接收该消息——也就是说,我们要求哪个对象执行所需的动作。接收消息的对象(我们对其调用方法的对象)称为 接收者。methodName 指明我们要求对象执行哪个方法。Java 中的方法调用在方法名之后总是包含一对括号。对于需要额外信息才能完成其工作的方法,我们将这些额外信息放在括号之间(即方法的 参数)。我们的 LightBot 方法都不需要额外信息,因此它们的所有括号都是空的——但括号仍然是必需的。
因此,如果我们希望 andy 向前移动,可以像这样编写相应的动作语句:
andy.move();
此方法调用由三个关键部分组成:圆点左侧的 接收者、圆点右侧的 方法名,以及方法名后的 括号。除了这三个部分外,请注意行尾的 分号。就像我们用来引入名称 andy 的声明和实例化语句一样,Java 中的所有语句都以分号结尾。不要遗漏它——它告诉计算机每个语句在哪里结束。
1.1.12. 将所有内容整合在一起¶
我们可以像这样将刚刚编写的方法调用添加到 myProgram():
public void myProgram()
{
LightBot andy = new LightBot();
this.addObject(andy, 0, 4);
andy.move();
// ... more goes here ...
}
如果进行此项修改、编译代码并运行,您将看到机器人被创建并放置在其起始位置,然后向前移动一格。为了完成我们的解决方案,andy 需要向上跳上墙壁,然后向下跳:
public void myProgram()
{
LightBot andy = new LightBot();
this.addObject(andy, 0, 4);
andy.move();
andy.jump();
andy.jump();
// ... more goes here ...
}
完成 andy 的其余指令,以便他点亮所有蓝色方格。
1.1.13. 关于编写易读代码的几点说明¶
当程序员编写源代码时,他们必须特别注意使其易于他人阅读。大多数程序员在小组中工作,其他程序员需要拾起并处理可能由他人几周、几个月甚至多年前编写的现有代码。因此,源代码必须易于他人阅读,这一点至关重要。
我们使代码易于阅读的一种方法是为所有标识符选择有意义的名称。例如,Level3 代表 LightBot 游戏中第 3 层的布局,而 Level3Solution 代表该特定布局的一个解决方案。同样,LightBots 可执行的行为的方法名类似于 move()、jump()、turnLeft() 等。这些名称的选择与行为相匹配,因此几乎不需要解释。
使源代码易于阅读的另一个重要工具是 注释。
备注
注释 是包含在源代码中的文本说明,旨在供其他(人类)读者参考。注释对程序的执行方式或计算机的行为没有任何影响。
一些注释在源代码中至关重要,因为它们向其他程序员解释 如何使用 某个方法或类。在许多编程语言(包括 Java)中,已确立了编写这些注释的约定,以便可以自动生成关于如何使用代码的文档。稍后您将看到大量此类文档,但现在让我们先通过基础知识来初窥门径。
// A pair of slashes side by side marks the beginning of a Java comment.
// Everything after them, including the two slashes themselves, is a comment.
// Sometimes, // is used to place a comment on the end of a line
// that also contains something else. But more commonly, we'll see
// double-slash comments as single-line comments on a line by
// themselves.
/*
* It is also possible to write a multi-line comment in Java by
* starting it with a slash followed by an asterisk (/*). Everything
* from this marker onward, across as many lines as you want, makes up
* a single comment. The comment ends with a matching asterisk followed
* by a slash, like this:
*/
// Finally, in Java there are specially marked comments that appear
// just before a name is declared, providing documentation about what
// that name means and how it is used. All the comments above are
// plain old comments that mean nothing outside of the source file
// where they are written. The following comment describes the purpose
// of a method, however, and can be used to generate documentation for
// it:
/**
* This comment describes the purpose of this method. It is called a
* Javadoc comment. You can tell it is different from other comments
* because it starts with "/**", instead of just "/*". You should
* include this kind of comment just before each method you write, and
* just before each class you write. It should describe the meaning/purpose
* of what immediately follows--here, the method called doSomething().
* The Javadoc comment should also explain how to use the method or
* class it describes.
*/
public void doSomething()
{
// ...
}
暂时记住,对于你编写的每个方法和每个类,都必须包含一个 Javadoc 注释(即描述)。随着我们开始学习,我们会尽量让这变得更容易,但这是一种你必须养成并适应的习惯(而且它将对你大有裨益!)。
使代码易于他人阅读的一个第三个重要工具是 缩进。程序员在行首使用空白字符来展示其源代码中的结构。你可能已经注意到,本章中展示的每个方法都以一个左花括号 ({) 开始,也称为左大括号、花括号、花曲括号或简称花括号。每个方法也以相应的右花括号 (}) 结束。这些花括号之间的语句位于方法 内部——它们是定义方法被调用时发生行为的语句。因为它们位于花括号 内部 (即,位于方法体 内部),我们在每行行首添加额外的空格来展示这种嵌套。
在本课程中,我们将使用所有空格字符来缩进代码。我们将通过四个空格缩进那些“包含在”更大结构中的行。你会看到编辑器会自动尝试在你编写代码时帮助你保持这种缩进习惯,但它不会强制你。最终,你负责让你的代码看起来整洁、有序且易于阅读。
1.1.14. 语法练习 1b¶
1.1.15. 编程练习 1¶
下面的练习提供了使用本模块中的概念编写简单问题解决方案的基本练习。







