OpenDSA 完整目录

Chapter 0 Introduction to Software Design

| 关于   «  Chapter 0 Introduction to Software Design   ::   目录   ::   2. 面向对象继承、重写与接口  »

1. 对象、类与微观世界

计算机科学不是编程。相反,编程是我们艺术的媒介,正如写作是英语及其他学科的媒介。首先,我们教你如何通过这一媒介表达解决问题的方法,然后你在众多令人愉悦的子领域中练习富有艺术性的解题。在这门课中,你会感觉像是在学习用一种新方式思考和写作。学习编程是具有变革性和赋权性的。 -- 托德·内勒

编写一组指令,按下“运行”按钮,看着一个虚拟世界在你眼前活跃起来,这种奇妙感是独一无二的。本章将开启你作为软件创造者的旅程。你不仅在学习一种编程语言,更踏入了一个数字沙盒,在这里你将设计自主的参与者,赋予它们感知与行为,并编排它们与环境互动的方式。

在面向对象的世界中编程本质上是关于创造性建模的。你将从探索互动微型世界开始——在 LightBot 中指挥有趣的机器人,以及在 Jeroo 的 Santong 岛上引导奇思妙想的生物。当你在这些微型世界中进行实验时,你会发现将复杂挑战分解为令人愉悦的小型谜题的优雅之处,并体验到看着你的数字创作以完美精准执行指令所带来的激动心情。

1.1. 安装 BlueJ

按照 Canvas 课程页面上的说明安装 BlueJ。

1.2. 介绍 LightBot

LightBot 是一款非常简单的机器人编程模拟游戏。它直接在您的网页浏览器中运行,外观如下:

_images/LightBotLevel2.png

在 LightBot 游戏中,你创建一个 程序 来控制一个小机器人,解决存在于微型方块式世界中的简单任务。LightBot 中的编程采用图标形式。游戏显示界面右上角的图标代表对机器人的指令。每个图标都指示机器人执行一项操作——向前移动一格、向右转、向左转、跳跃前进,以及点亮机器人所在方格。当然,只有当机器人位于蓝色方格上时,该方格才会被点亮。界面左上角的扬声器图标可用来开启或关闭游戏音效。左下角的重置按钮将清除所有指令并重新开始当前关卡。

在 LightBot 游戏的每一关中,你的任务是让机器人点亮所有的蓝色方块。要创建控制机器人的程序,只需将顶部的图标拖动并放置到主方法方块中,从左到右依次排列。

Try It Yourself

在 LightBot 中玩前两个关卡,来此处尝试一下(在新标签页中打开;如果某些控件不可见,请最大化窗口):

     LightBot

Be sure your browser window is tall enough to see the "Instructions" label in the upper right of the game. Keep the separate browser tab/window showing the game open so you can refer to it later in this chapter.

LightBot 游戏展示了几个基本的编程概念。机器人本身是一个 对象,我们可以请求它执行它所知道的具体动作。我们可以将一系列这样的请求写下来,形成一个 程序。我们所写下的这一系列动作,是解决特定问题的计划(一个 算法)。

1.3. 从 LightBot 到微型世界

LightBot 游戏是一个 微型世界 的实例。微型世界是一种小型模拟环境,其中包含一个或多个具有明确定义行为的对象,并且可以进行视觉化表示,以便你能够观察对象随时间移动和行为的变化。

微世界通常使用 网格 来表示模拟世界中的位置。你可以在 LightBot 游戏中看到这一点,机器人在方格瓷砖上行走。在本课程中,我们将采用相同的方法。我们可以使用 x 和 y 坐标来指代网格上的任意位置:

_images/micro-world-coordinates.png

我们将使用一种 笛卡尔坐标系 的变体,将任意位置表示为 (x, y) 对,其中 x(第一个坐标)表示从原点水平方向的距离,y(第二个坐标)表示从原点垂直方向的距离。唯一的不同是,在我们的网格中,y 轴指向下方,因此这是一个 左手笛卡尔平面。我们采用左手坐标系的方向,是因为这种约定在计算机编程环境中占主导地位(尽管这与几何学中通常的做法相反!)。

因此,上图中高亮的格子位于位置 (4, 2),因为它在原点右侧水平方向第 4 格,下方垂直方向第 2 格。同时请注意,原点 (0, 0) 在我们的网格中是明确表示的,位于 左上角。上图的网格大小为 10 乘 10,坐标范围从 0 到包括 9. 这意味着我们在指代 网格位置时将始终使用 零基索引——即我们总是从原点(左上角)开始以零为起点计数,而不是从一开始计数。

1.4. 再多一点 LightBot

如果你只玩了LightBot的前两个关卡,那肯定还有更多你尚未见过的有趣内容!随着关卡推进,谜题会变得越来越复杂。仅靠程序内存中为小机器人预留的有限槽位来解决这些谜题将更加困难。幸运的是,LightBot游戏为你提供了几个额外的图标供使用。

原始版本的 LightBot 提供了两个较小的指令区域:f1 和 f2。f1 与 f2 图标代表 LightBot 游戏主方法下方的两个函数(图标区域)。这两个函数是你放置机器人需要重复执行的指令的位置。无论你将 f1 或 f2 图标放置在何处,机器人都会执行与该函数相关联的全部动作序列。这将使你能够构建更复杂的程序,从而解决后期关卡中更具挑战性的任务。

Try It Yourself

返回 LightBot 游戏——希望它仍显示在独立的浏览器标签页或窗口中(如果已关闭,请参见本章前面的链接)。继续玩到第 7 级。完成第 7 级后停止。你将在下一个练习中返回来完成第 8 级。

1.5. 程序的文本表示

计算机程序通常以文本形式编写——事实上,编程最重要的目标之一就是编写程序,使其能够良好地向 其他人 传达信息。因此,让我们来看看如何将 LightBot 谜题的解法表示为文本。

从最基本的角度来看,计算机程序不过是一系列计算机需要遵循的指令序列。我们的 LightBot 关卡解法正是如此:程序。我们只需为图标赋予对应名称,即可用文本形式编写程序。例如,若将我们的小机器人命名为 :code:andy ,则可使用以下名称分别对应其图标(从左至右):

SymbolTextual Program Statement (Method Call)
andy.move();
andy.turnRight();
andy.turnLeft();
andy.jump();
andy.turnLightOn();
andy.f1();
andy.f2();

每个操作都表述为对执行所需行为的对象的请求。因此,每个请求都会发送到我们的机器人, :code:andy 。例如,考虑第 2 级:

_images/LightBotLevel2.png

此层级的一个解决方案是:

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 游戏——希望它仍显示在独立的浏览器标签页或窗口中(如果已关闭,可参见本章前面的链接)。若尚未完成第 7 级,请先完成。完成第 7 级后,仔细思考第 8 级的解决方案。构思第 8 级的解法,但暂不要运行。将第 8 级的解法以文字形式写在草稿纸上。

写下你的解决方案后,再运行它。如果运行结果不符合你的预期,请先修改代码,并同时修改相应的文字说明,然后再重新运行。这将帮助你练习描述程序步骤。

1.6. LightBot 对编程有何说法?

LightBot 游戏展示了几个基本的编程概念:

  • 对象

  • 方法

  • 方法调用

  • 状态

  • 算法

  • 程序

  • 源代码

  • 解释器

在本课程中,我们将专注于学习 Java 中的 面向对象编程。本节描述了我们在第一个示例中所见的面向对象程序的一些通用概念。在阅读后续章节时,请回溯本章以复习重要词汇和短语的含义。

1.6.1. 程序与编程语言

无论我们是发送电子邮件、浏览网络、听音乐、撰写文章,还是玩游戏,只要使用计算机,就是在使用一个或多个计算机程序。每个程序本质上都是一组给计算机的指令。

备注

一个计算机 程序 是一组供计算机执行的指令。

每个程序都是由一个或多个程序员编写的。程序员使用编程语言,如 Java、C++、Python 或 Visual Basic,来编写计算机程序。对于 LightBot,你最初使用的是图标语言,然后学习了如何使用一种简单的文本语言,该语言精确地表示相同的动作。

程序以多种形式存在。程序员编写的那种形式称为程序的源代码。然而,计算机不能直接使用源代码。源代码必须被翻译成机器语言后,才能被计算机执行(运行)。

备注

程序的 源代码 由程序员用某种编程语言编写。

有多种类型的翻译。编译器将程序整体从一种形式转换为另一种形式,但不一定是转换成机器语言。解释器逐条将程序翻译成机器语言。每条语句一旦被翻译完毕,立即执行。LightBot 是一个解释器的例子,因为它将源代码中的每个动作逐条直接转换为计算机行为。

备注

一个 编译器 将程序作为一个整体,从一种形式转换为另一种形式。

一个 解释器 将程序逐条语句地翻译成机器语言。

1.6.2. Algorithms

每个计算机程序都始于一个计划。这个计划被称为算法。编写算法有多种方式。有些非常非正式,有些则相当正式且具有数学性质,还有一些则相当图形化。形式并不特别重要,只要它能提供一种良好描述和检验计划逻辑的方法即可。

备注

一个 算法 是解决问题的计划。

1.6.3. 对象

学习到面向对象编程语言以对象为基础运作,并不会令人感到意外。但什么是对象呢?遗憾的是,这个概念很难定义,因为对象既是仅存在于计算机程序中的事物,又是其他事物的逻辑表示。一个较好的方法是通过它所代表的内容来定义对象。

备注

一个 对象 代表与我们试图解决的问题相关的特定概念或项目。

一个典型的程序会处理多个不同的对象。其中一些可能代表同一种事物。例如,在 LightBot 中,机器人本身显然就是一个对象。但还有阻挡机器人路径且可堆叠的砖块。此外还有可以点亮的蓝色方块。一个关卡中可能有多个砖块或多个蓝色方块,它们都是独立的对象,尽管属于同一类。

一个对象代表某样事物,但作为程序员,我们需要确定该事物的哪些特征对于我们要解决的问题是重要的。对象包含两个部分:关于其所代表事物的事实(蓝色方块是否已经点亮,还是仍然昏暗?机器人朝向哪个方向?),以及对象可以执行的任务(机器人可以移动、向左或向右转动等)。这些事实称为属性(我们稍后会介绍),而任务称为方法。

1.6.4. 方法与行为

当我们设计一个对象时,我们需要确定它应该执行哪些任务。在进行这一过程时,我们往往会将该对象所代表的事物拟人化。(所谓拟人化,是指赋予非人类事物以人类特征。)例如,我们可能希望 LightBot 从一个位置移动到另一个位置,或者跳上障碍物。

备注

一个 行为 是指对象可执行的动作,或其对外部源请求作出响应时可完成的任务。

一个 方法 是由某种编程语言编写的语句集合,用于描述特定的行为。

一个方法的 前提条件 是指在调用该方法之前被假定为真的条件。

方法的一个 后置条件 是指在方法执行完毕后被认为为真的某种情况。

例如,我们的 LightBot 支持若干对应于我们可使用的图标的方法,这些方法在上一节中已赋予了文字名称。此外,某些方法仅在特定条件下有效:机器人只有在前方正对的方格恰好比机器人所在位置高或低一个方块时才能跳跃(向上或向下)。这是一个 前提条件。同样,点亮当前方格的图标仅在机器人站在蓝色方格上时才有效(这也是一个前提条件)。然而,如果机器人确实站在蓝色方格上,然后执行其“打开灯光”的行为,则之后该蓝色方格将被点亮(这是一个后置条件,用于描述执行特定行为或方法后的结果)。

1.6.5. 消息(调用方法)

当我们编写面向对象程序时,会实例化适当的对象并请求它们执行特定任务。我们使用消息来发出这些请求。

备注

一条 消息 是请求某个对象执行特定任务的请求。

当我们要求一个对象执行某项任务时,我们说我们正在 发送一条消息 或 调用描述该任务的方法。

1.6.6. 面向对象编程为何重要?

虽然我们已经学会了如何在编程中定义和使用对象,但理解为何要使用它们非常重要。面向对象编程(OOP)是一种强大的编程范式,因为它能帮助我们将复杂的程序组织得更加易于管理且可扩展。想象用乐高积木搭建:每块乐高积木都是一个具有自身特征(颜色、形状)和行为(可以与其他积木连接)的对象。当你构建大型结构时,并不会去思考每一个塑料原子;你关注的是组合现成的积木。同样地,在面向对象编程中,对象使我们能够:

  • 模块化我们的代码:将一个大问题分解为更小、自包含的部分(对象),使代码更易于理解、编写和调试。

  • 促进可重用性:一旦你创建了一个对象(如我们的 LightBot 对象),就可以在程序的不同部分甚至全新的程序中重复使用它,而无需重新编写其内部逻辑。这可以节省时间并减少错误。

  • 简化维护:如果需要更改某个特定行为,只需修改该对象内的代码,而不必在整个程序中搜索。

  • 封装数据与行为:对象将数据(如 LightBot 的位置)与其操作这些数据的方法(如 move() 或 turnLightOn() )捆绑在一起。这意味着对象可以控制自身的内部状态,防止程序其他部分意外破坏它。

通过理解这些优势,你将明白面向对象编程不仅仅是一套规则;它是一种设计健壮且高效软件的哲学。

1.7. 用 Java 编写的可编程光球机器人

1.7.1. LightBot 世界俯视视角

在上文的各节中,你已经体验了 LightBot 游戏,并且甚至看到了那个小机器人所遵循的图形化命令的文字表示形式。现在是时候将这些概念转化为一个完整的程序,由你自己编写了。

首先,回想一下 LightBot 实际上只是一种 微观世界:游戏的每个“关卡”都是一个微型世界,可以表示为一个网格,所有物体——机器人、方块、会发光的蓝色瓷砖——都占据该网格上的不同位置。这款 Flash 游戏使用等轴测投影使世界看起来更具三维感,并通过动画使其更吸引人。例如,这是你在第 1 章中看到的 LightBot 游戏的第二关:

_images/LightBotLevel2.png

为了使事情稍微简化一些,我们不妨采用一个普通的自上而下的视角来看待同样的情况:

_images/light-bot-lev2g.png

在这个自上而下的视角中,更容易看出哪个方向是 x 方向,哪个方向是 y 方向,因此我们可以清楚地保持坐标的一致性。此外,这张图突出了每个 LightBot 关卡都是一个 8 x 8 的网格。作为另一个例子,LightBot 游戏的第三关在原始版本中的样子如下:

_images/light-bot-lev3.png

但同一层也可以自顶向下这样表示:

_images/light-bot-lev3g.png

现在我们已经弄清楚了“世界”的情况,就可以讨论编写源代码来控制其中的机器人了。

1.7.2. LightBot 的方法

让我们通过回顾 LightBot 所理解的基本命令,开始讨论编写 LightBot 程序。之前我们看到,LightBot 能够执行七种基本动作或行为:

MethodWhat 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.7.3. 我们该把它写在哪里?

我们编写的每一段程序代码都必须放置在某个位置。但应该放在哪里呢?在面向对象的程序中,所有程序语句都必须放在一个 方法 内,而每一个方法都属于一个对象(或属于一个 类,即一组理解相同方法的对象集合)。

本课程使用的编程环境称为 BlueJ。我们还使用了 Greenfoot 的类库,这是一个框架,使我们能够处理各种类型的微型世界。

当您在 BlueJ 中打开一个项目时,您将看到正在处理的各个 Java 类的图表。在这里,我们可以看到 BlueJ 的主窗口正在查看一个用于解决 LightBot 第 3 级的简单 Java 项目。

_images/bluej-Level3Solution.png
  • 你可以双击项目中的任意 Java 类,以在编辑器中打开并查看其程序代码。对于我们的微型世界作业,你还将看到一个名为 Application 的类——右键点击 Application 类,然后选择 运行 JavaFX 应用程序 来执行你的微型世界应用程序。

_images/lightbot3.png

当您运行任意一个微型世界程序时,您将看到世界被显示出来。显示界面的主区域呈现了世界的俯视图,在本例中为 LightBot 游戏的第 3 层。您还会在底部看到控制按钮: Act (使对象执行下一步动作)、 Run (使对象持续不断地重复执行动作),以及控制执行速度的选项。

每个你编写的 Java 类,其源代码都会存储在一个文本文件中。通过在 BlueJ 项目窗口中双击代表该类的圆角矩形,你可以在编辑窗口中查看(并编辑)你的类源代码。如果你双击 Level3Solution ,你将看到其内容:

_images/Level3Solution.png

在编辑器中请注意,已有方法用于存放新代码。该方法名为 myProgram() ,表示当你点击程序窗口底部的 Run 按钮时将执行的行为。此方法具有以下结构:

_images/method_structure0.png

1.8. 创建新对象

为了调用对象的方法来解决问题,我们首先实际上需要一个对象!这意味着创建解决问题所需的对象是我们构建解决方案工作的一部分。

备注

创建一个新对象称为 实例化。

有时候,我们只需要创建一个对象,这个行为本身就能满足我们的需求。

备注

程序中通过 声明 引入一个新名称。

这两个任务——声明一个新名称和创建一个新对象——通常会同时出现,Java 提供了一种方便的方式让我们表达这种两步模式:

_images/vardecl.png

这是声明新名称并创建新 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,但我们尚未说明应将他放置在世界中的何处。假设我们希望在位置 (0, 4) 处向世界添加 andy 。可以使用所有世界对象都理解的方法 add() 来实现,如下所示:

public void myProgram()
{
    LightBot andy = new LightBot();
    this.addObject(andy, 0, 4);

    // ... more goes here ...
}

现在是时候尝试运行你的第一个程序了!如果你已经将这些代码输入到 BlueJ 的编辑器中,你可能会注意到 BlueJ 主窗口中的 Level3Solution 图标已发生变化,出现了对角线。这些对角线表示源代码已被修改——因为你输入了新的指令。然而,在计算机执行我们的程序之前,我们首先需要将你编写的源代码转换为计算机可执行的形式。点击编辑器窗口顶部的“编译”按钮(或 BlueJ 主窗口上的“编译”按钮)。这将把源代码转换为计算机可以执行的形式。最后,右键单击 Application 类,并选择“运行 JavaFX 应用程序”。

好吧,这不算精彩,但你会看到一个新 LightBot 在你指定的坐标处被创建并添加到关卡中。你可以使用窗口底部的“重置”按钮丢弃当前世界,重新开始一个全新的副本,也可以使用“运行”按钮再次执行你的程序。点击“执行”按钮可逐行单步执行程序,按顺序依次执行 myProgram() 中的动作,以便以超慢速模式观察每一步的变化。或者,使用速度滑块来加快或减慢程序运行时动作的执行速度。

1.9. 对对象调用方法

好的,我们已经创建了一个 LightBot ,但它几乎什么也做不了。我们该如何请求它执行操作呢?一个动作语句是请求对象执行特定任务的指令。该任务可以是对象已知的基本动作方法之一,也可以是你自己编写的新方法。动作语句的语法是:

_images/actionstatement.png

一个动作语句也称为 方法调用 语句,因为它在对象上调用了方法。点号左侧的 identifier 指定了将接收消息的对象——也就是说,我们正在请求哪个对象执行所需的操作。接收消息的对象(即我们调用方法的对象)称为 接收者。methodName 表示我们要求对象执行的方法。Java 中的方法调用始终在方法名后包含一对括号。对于需要额外信息才能完成任务的方法,我们将额外信息放在括号中(即方法的 参数)。我们所有的 LightBot 方法都不需要任何额外信息,因此它们的所有括号都将为空——但括号仍然必需。

因此,如果我们希望 andy 向前移动,可以写出如下对应的动作语句:

andy.move();
  • 该方法调用包含三个关键部分:点号左侧的 接收者,点号右侧的 方法名,以及方法名后的 括号。除了这三部分外,请注意行尾的 分号。与我们用来引入名称 andy 的声明和实例化语句一样,Java 中的所有语句均以分号结尾。切勿遗漏——它告诉计算机每个语句的结束位置。

1.10. 将所有内容整合在一起

我们可以将刚刚编写的方法调用添加到 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.11. 我们都会犯错:错误会发生

当你开始编写更复杂的程序时,不可避免地会遇到 语法错误 (或 编译器错误)。这些错误类似于人类语言中的语法错误;计算机根本无法理解你的意图。好消息是,你的编程环境 BlueJ 和 Java 编译器都设计用于帮助你发现这些错误。当你点击 Compile 时,如果存在问题,BlueJ 通常会高亮显示它认为出错的行,并在编辑窗口底部提供一条消息。

常见的语法错误包括:

    • 缺少分号:记住,Java 中几乎所有的语句都必须以分号结尾 ; 。忘记添加分号是一个非常常见的错误!

    正确: andy.move()

    正确: andy.move();

  • 括号不匹配:方法调用(及构造函数)在方法名后始终需要一对括号 () 。请确保既有左括号 ( ,也有右括号 ) 。

    Incorrect: andy.turnRight(

    正确: andy.turnRight();

  • 排版错误(拼写错误):方法名中的任何一个拼写错误(例如 and.move(); 而非 andy.move(); )都会导致错误,因为计算机无法识别该命令。请仔细检查拼写和大小写。

    • 缺失大括号:方法体以一个左大括号 { 开始,以相应的右大括号 } 结束。请确保它们正确配对。

当你遇到错误时:

  1. 阅读错误信息:不要轻易忽略它。该信息通常会提供关于出错原因和位置的线索。

  2. 查看高亮行或行号:BlueJ 通常会告诉你检测到错误的行。从那里开始调查。但请记住,实际问题可能出现在代码更早的位置。

  3. 检查常见错误:检查指示行及其附近分号、括号和大括号的语法规则。

  4. 逆向排查:如果错误信息不够明确,请查看指示行之前的代码,因为有时代码中较早的错误会导致后续行显示为错误。

学习调试是一项基本的编程技能。这就像成为一名侦探,通过拼凑线索来解开谜团!

1.12. 关于让代码易于阅读的一点说明

当程序员编写源代码时,必须特别注意使其易于他人阅读。大多数程序员都是在团队中工作,其他程序员需要接手并使用由他人数周、数月甚至多年前编写的现有代码。因此,源代码必须易于他人阅读至关重要。

我们让代码易于阅读的一种方式是为所有标识符选择有意义的名称。例如, Level3 表示 LightBot 游戏中第 3 层的布局, Level3Solution 表示对该特定层布局的解法。类似地,LightBot 可执行行为的方法名称如 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 注释(也就是描述)。我们会尽量在开始阶段让你更容易上手,但这确实是一个你需要逐渐习惯的习惯(而且这会对你大有裨益!)。

一个让代码更易于他人阅读的第三个重要工具是 缩进。程序员在行首使用空白字符来表示源代码的结构。你可能已经注意到,本章所展示的每个方法都以一个开括号({)开头,也称为左括号、花括号、大括号或简称花括号。每个方法也以相应的闭括号(})结尾。这些括号之间的语句属于 方法内部——它们是定义调用方法时发生行为的语句。由于这些语句位于括号内部(即方法体内部),我们会在每行开头添加额外的空格以显示这种嵌套关系。

在本课程中,我们将使用所有空格字符来缩进代码。对于“包含于”较大结构中的行,我们将用四个空格进行缩进。您会发现编辑器在编写代码时会自动帮助您保持这种缩进习惯,但不会强制执行。最终,您有责任使您的代码看起来整洁、清晰且易于阅读。

你已经了解到,为所有标识符选择有意义的名称对于编写可读性强的代码至关重要。但除了意义明确之外,程序员还遵循特定的命名规范,例如变量或方法名以小写字母开头( andy , move() ),而类名则以大写字母开头( LightBot )。这不仅仅是美观问题;这些规范具有重要的实际作用:

  • 清晰与一致:当所有人都遵循相同的规范时,代码会变得立即可识别且易于理解,即使你几个月或几年后阅读他人编写的代码也是如此。这就像为程序员们提供了一种通用的肢体语言。

  • 一目了然的可读性:大小写有助于你立即区分不同类型的编程元素。例如,看到 LightBot 中大写的“L”,你会知道它很可能是一个类(对象的蓝图),而 andy 中小写的“a”则表明它是该类的一个实例(一个具体的对象)。

  • 减少歧义:一致的命名可以防止混淆。如果 Move() 和 move() 都可用于同一操作,那么使用哪一个将变得不明确。

  • 行业标准:这些规范(变量/方法通常称为“驼峰命名法”,类则称为“帕斯卡命名法”)在 Java 社区以及许多其他编程语言中被广泛采用。遵循它们能使你的代码显得专业,并促进团队环境中的协作。

  • 工具支持:许多集成开发环境(如 BlueJ)在设计时就考虑到了这些规范,有时会根据预期的命名模式提供自动补全或错误提示功能。

从编程旅程一开始就养成使用正确且有意义的命名规范的习惯,将显著提升代码的质量与可维护性。

1.13. 视觉编程与文本编程的比较

你已经体验了两种使用 LightBot 创建程序的方法:通过可视化图标界面和编写类似 Java 的文本代码。两者都达到了相同的目标——控制机器人,但代表了编程的不同方法。

  • 标志性编程(例如 LightBot 的拖拽式编程):

    • 优点:

      • 初学者友好:对新手非常直观,因为它消除了语法和输入的负担,使学习者能够专注于逻辑。

      • 即时视觉反馈:你可以直接看到机器人动作与所放置图标的对应关系,便于理解控制流程。

      • 减少错误:由于选择的是预定义的动作,因此不会出现语法错误。

    • 缺点:

      • 复杂度有限:由于视觉空间和可用图标数量的限制,对于非常大型或复杂的程序会变得繁琐。

      • 灵活性较低:你只能使用可视化环境提供的预定义操作和结构。

      • 非行业标准:现实世界中的编程主要使用文本语言。

  • 文本编程(例如 Java):

    • 优点:

      • 高度表达且灵活:可以编写几乎任何你想象得到的复杂和细致的逻辑,因此具有极强的表现力。

      • 可扩展性好:文本代码在大型项目中更易于管理、阅读和组织。

      • 行业标准:你在文本编程中学到的技能可直接应用于专业软件开发。

    • 缺点:

      • 学习曲线更陡峭:必须严格遵守语法规则,这对初学者来说可能具有挑战性(例如,记住分号、括号等)。

      • 抽象性:代码与程序行为之间的联系并不总是能立即直观呈现,需要更多的心理建模。

      • 易出拼写错误:小的错误可能导致编译问题。

LightBot 的标志性界面是理解基础计算思维的绝佳工具——顺序执行、方法或函数的使用(通过 f1/f2 实现),以及问题解决。随后的文本式 Java 在此基础上进一步引入了真实世界软件开发所需的精确语言和结构,使你对程序拥有更大的控制力与更强的能力。

1.14. 桑东岛的杰鲁斯

现在你已经通过 LightBot 探索了基础的计算思维与文本型 Java,接下来可以过渡到我们的主要微型世界: 桑东岛 与 杰鲁 模拟。类、对象、使用 new 进行实例化,以及使用点号表示法调用方法这些概念,可直接用于指挥杰鲁在岛屿网格上的行动。

1.14.1. 欢迎来到三通岛

_images/jeroo.png

桑东岛是南太平洋中一块无人居住的陆地。在20世纪80年代末,自然学家杰西卡·童发现该岛上存在两种此前未知的特有物种。其中一种是 杰鲁,一种稀有的哺乳动物,与澳大利亚的沙袋鼠相似。另一种是大型的 温森花,为杰鲁的主要食物来源。

与遥远的澳大利亚亲戚类似,杰鲁会跳跃移动,但其运动方式在自然界中独一无二。

每个杰鲁都拥有一个袋子,但雄性的袋子稍小一些。雌性用袋子来保护和哺育后代,而两性都用袋子携带它们采摘的温森花。

在雨季期间,从一月到十二月,突如其来的风暴可能会形成临时的湖泊和河流,甚至改变岛屿本身的形状。尽管杰鲁斯在桑通岛上已生活了无数个千年,但它们却是极差的游泳者。虽然它们对周围环境十分警觉,但杰鲁斯有时会粗心大意。如果一只恰好跳进海洋,其毛发会吸满水,必须在阳光下休息数小时才能恢复正常的活动。以一种只能被描述为心灵感应的联系,所有其他杰鲁斯也会停止活动,直到那只湿漉漉的杰鲁斯恢复过来。

直到最近,由于仅少数研究人员知晓,且桑东岛面积很小、极为偏远,大多数地图上都找不到它,因此巨鹿免受人类干扰。

1.14.2. 一个杰鲁模拟器

在最近一次前往桑东岛旅行后,杰西卡请她的同事迪安娜开发一个模拟器,用于演示鸠鲁及其与岛屿之间的关系。迪安娜工作的成果是一个用 Java 编写的计算机程序,帮助杰西卡进行她的研究。该程序提供了一个桑东岛的微观世界表示,并展示了岛上一个或多个鸠鲁的行为。

当迪安娜设计模拟器时,她利用了杰鲁的特殊移动方式,将岛屿建模为一个网格,其中 x 轴沿纬线从西向东延伸,y 轴沿经线从北向南延伸。该模型中的每个元素(单元格)对应一个杰鲁跳跃后可能落脚的位置。

丹娜选择从岛屿所在地图的西北角开始,将横坐标和纵坐标都从零编号(她的地图包含了岛屿周围的水域)。当被问及为何从零开始时,丹娜表示她是在计算从任意位置到西北角的安全跳跃次数。我们将用符号 (x, y) 表示一个特定格子,其中 x 代表格子的水平位置,y 代表格子的垂直位置。

1.14.3. Jeroo 动作方法

Deanna 的模拟程序中包含两个 Java 类: Island 和 Jeroo 。 Island 类是一个世界子类,用于表示岛屿地图,而 Jeroo 类则是一个角色子类。此外还有 Flower 、 Net 和 Water 类,用于表示岛上的其他物体。 Jeroo 理解七种动作方法(其中三种需要参数值):

MethodPurposeExample
hop()Hop one space ahead. The program terminates with a logic error if the hopping Jeroo lands in the water, lands on another Jeroo, or hops onto a net. A Jeroo can hop onto a flower. jessica.hop();
hop(number)Hop number times in a row, where number is a positive integer. jessica.hop(3);
jessica.hop(12);
pick()Pick a flower from the current location. Nothing happens if there is no flower at the current location.jessica.pick();
plant()Plant a flower at the current location. Nothing happens if the jeroo does not have a flower to plant.jessica.plant();
toss()Toss a flower one space ahead. The tossed flower is lost forever. If the flower lands on a net, the net is disabled. jessica.toss();
turn(relativeDirection)Turn in the indicated direction [ turn(AHEAD) and turn(HERE) are meaningless ] jessica.turn(LEFT);
jessica.turn(RIGHT);
give(relativeDirection)Give a flower to a neighboring Jeroo in the indicated direction. Nothing happens if the giving Jeroo has no flowers, or if there is no neighboring Jeroo in the indicated direction. [ give(HERE) is meaningless ] jessica.give(LEFT);
jessica.give(RIGHT);
jessica.give(AHEAD);

方向 """""""""

每个 Jeroo 知道四种相对方向和四种方位方向,如以下表格所示。每个方向必须以大写字母书写(在 Java 中,永不改变的常量值通常采用这种方式书写)。相对方向 HERE 指的是 Jeroo 当前所在位置,因此与上一节中的动作方法不适用(我们将在后续内容中看到 HERE 有意义的其他方法)。

Relative DirectionsCompass Directions
LEFTNORTH
RIGHTEAST
AHEADSOUTH
HEREWEST

属性 """"""""""

当设计一个用于表示某事物的对象时,我们必须选择与我们试图解决的问题相关的该事物的事实。例如,书的标题对书店很重要,但对从出版商仓库发货的公司来说则不重要。另一方面,书的重量对发货方很重要,但对书店来说则不重要。像书的标题或重量这样的事实被称为属性。

备注

一个 属性 是与特定对象相关联的事实。

Jeroo 对象的属性包括其位置、口袋中的花朵数量以及朝向。实例化一个类时,该类的 构造函数 负责初始化新对象的属性。因此,当我们创建一个 Jeroo 对象时,其构造函数负责初始化口袋中的花朵数量和朝向——其位置是在将其添加到世界中时设定的。如果我们创建 jeroo 时未指定某个(或多个)属性的值,构造函数会为我们提供默认值。

作为便利, Jeroo 类提供了 多个构造函数,以便我们可以省略(或提供)任意属性的值。除一个以外的所有构造函数都允许我们仅提供部分属性的值。这些构造函数总结如下表:

ExampleAttributes
// Accept all defaults
Jeroo jessica = new Jeroo();
Direction:EAST
Flowers:0
// Specify just the flowers
Jeroo jessica = new Jeroo(8);
Direction:EAST
Flowers:8
// Specify just the direction
Jeroo jessica = new Jeroo(WEST);
Direction:WEST
Flowers:0
// Specify both attributes
Jeroo jessica = new Jeroo(WEST, 8);
Direction:WEST
Flowers:8

声明部分与实例化部分之间的等号表示新创建的 Jeroo 对象应与声明部分中的标识符相关联。

1.14.4. 在岛上创建 Jeroos

一个 Island 类的实例相当简单:

_images/island.png

这座岛屿完全光秃,没有杰鲁斯,没有花朵,形状也相当简单。但要 改变一个库类的行为,我们需要 创建该类的自定义版本。我们通过定义一个 子类 来实现这一点。子类 继承 其所基于类的所有特性。这意味着它理解原类的所有方法,并可添加更多自己的方法。我们使用以下基本模式来创建子类:

public class NewClassName
    extends ExistingClassName
{
    ...
}

关键字 public class 表示我们正在引入一个可以在代码任意位置使用的全新 类 (即,该类在外部“公开”可用)。NewClassName 是我们定义的新类的名称,ExistingClassName 是我们希望基于其构建的类的名称。关键字 extends 用于表明我们的新类是一个 子类。每当使用 extends 时,表示我们声明的类将 继承 另一个现有类的所有特性。

备注

一个 子类 是继承另一个类(称为其 父类)所有方法和属性的类。

备注

派生类 和 子类 是 子类 的不同名称。

备注

基类 和 父类 是 父类 的不同名称。

在 BlueJ 中,我们可以通过点击 BlueJ 主窗口中的“新建类...”按钮,从 Island 类创建一个新的子类。在随后出现的新类对话框中,输入新类的名称,选择“岛类子类”,然后点击“确定”按钮。此操作将为您创建一个新类及其关联的源文件,并填入所需的初始内容(包括一些注释和占位符)。

备注

在 Java 中,给定类的源代码存储在一个纯文本文件中,该文件的名称与类名 相同,末尾加上".java"。文件名必须与类名完全相同地大写。

当你在 BlueJ 中创建一个新子类时,生成的类将已经包含一个 构造函数。构造函数是一种特殊的方法,仅用于初始化一个全新的对象。

备注

构造方法 是一种特殊类型的方法,用于初始化一个全新的对象。

在我们的岛屿子类的构造函数中,可以定义岛屿的尺寸,并执行创建岛屿时希望进行的任何操作——例如,向其中添加一些花朵,或在新位置放置水。

我们也可以通过添加一个 myProgram() 方法来说明当 运行按钮被按下 时希望发生的事情。你可以将此方法添加到你的岛屿子类中:

public void myProgram()
{
    Jeroo jessica = new Jeroo(8);
    this.addObject(jessica, 3, 4);

    //  Give instructions to your jeroo to carry out here
}

当按下“运行”(或“执行”)按钮时,您放置在 myProgram() 中的操作将会被执行。

Try It Yourself

创建一个 Island 子类,并向其中添加如上所述的 myProgram() 方法。将 myProgram() 中的注释替换为指令,使海鸥 jessica 从 (3, 4) 到 (5, 4) 植入一排三朵花。

1.15. 通过练习提升你的技能

学习编程是一项通过刻意练习和实验逐步建立的技能。以下 可选、不计分的资源 旨在帮助你巩固关键概念,提升熟练度,并为即将进行的测验、实验和编程作业做好准备。

  • Java 语法练习 1 :通过快速、小规模的练习建立信心并培养肌肉记忆,重点学习语法规则和基本语言机制。 特别推荐给编程新手 。

1.16. 编程实践 1

以下练习提供了使用本模块概念解决简单问题的编写解决方案的基本训练。

 

   «  Chapter 0 Introduction to Software Design   ::   目录   ::   2. 面向对象继承、重写与接口  »

关闭窗口