CS 1114 软件设计导论

第 6 章 响应式智能体模拟与委托

| 关于   英文版   «  5.1. 字段、封装与二维像素网格   ::   目录   ::   7.1. 面向对象继承、重写与接口  »

6.1. 反应式代理模拟与委托

6.1.1. 桑通岛的 Jeroos

在异域岛屿的野生动物模拟情境中,进入基础 Java 编程。

_images/jeroo.png

6.1.1.1. 欢迎来到三通岛

Santong 岛是南太平洋上一座无人居住的小岛。在 20 世纪 80 年代末,博物学家 Jessica Tong 发现了两种此前未知且为该岛独有的物种。一种是 Jeroo,一种类似于澳大利亚沙袋鼠的稀有哺乳动物。另一种是大型 winsum flower,它是 Jeroo 的主要食物来源。

与其遥远的澳大利亚亲戚类似,Jeroo 通过跳跃移动,但其运动方式在自然界中独一无二。每只 Jeroo 仅沿四个主要罗盘方向之一移动,且仅以 90 度为增量进行转向。这种异常行为继续让杰西卡以及少数其他在野外观察过 Jeroo 的研究人员感到困惑。一些人认为这种行为以某种方式与地磁力有关,但另一些人则认为这仅仅是一种奇特的习得行为。

每只 Jeroo 都有一个育儿袋,但雄性的育儿袋略小一些。雌性用她的育儿袋保护和养育后代,而两性都用育儿袋携带它们采摘的 winsum 花。

在雨季(一月至十二月),突如其来的风暴可能会形成临时的湖泊和河流,甚至改变岛屿本身的形状。尽管三同岛上的杰罗已生活了无数千年,但它们却是极差的游泳者。虽然杰罗对周围环境非常敏锐,但也可能粗心大意。如果某只杰罗不慎跳入海洋,它的皮毛就会浸透水分,必须在阳光下休息几个小时才能恢复正常活动。在这种只能被描述为心灵感应般的连接中,所有其他杰罗也会停止活动,直到那只湿透的杰罗恢复为止。

直到最近,Jeroo(Jeroo)都免受人类干扰,因为只有少数研究人员知道它们的存在,而且桑通岛非常小、非常偏远,在大多数地图上都找不到。2001 年,布莱恩抵达该岛,打破了Jeroo的田园世界。布莱恩是一位著名的收藏家,受雇为密苏里州 C. A. Baret 动物园的极端动物展览捕捉一只Jeroo。通过研究Jeroo独特的移动模式,布莱恩定期在Jeroo可能访问的位置设置网。幸运的是,敏感的Jeroo能够探测到他的网,并且已经学会将一朵温森花(winsum flower)扔到网上即可使网失效,从而使该位置对未来通行变得安全。布莱恩只能捕获那些不加思索就跳跃的粗心Jeroo。

6.1.1.2. Jeroo 模拟器

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

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

Deanna 选择从包含该岛屿的地图的西北角开始,将 x 坐标和 y 坐标都从零开始编号(她的地图包括了岛屿周围紧邻的水域)。当被问及为何从零开始时,Deanna 说她是在计算从任意位置到西北角的安全跳跃次数。我们将使用记号 (x, y) 来表示特定单元格,其中 x 代表单元格的水平位置,y 代表单元格的垂直位置。

6.1.1.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 对象将与声明部分中的标识符相关联。

6.1.1.4. 在岛上创建 Jeroo

Island 类的一个实例非常普通:

_images/island.png

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

public class NewClassName
    extends ExistingClassName
{
    ...
}

关键字 public class 表示我们正在引入一个全新的 class,它可以在代码的任何地方使用(即,它是“公开”的,可供其他地方使用)。NewClassName 是我们正在定义的新类的名称,而 ExistingClassName 是我们想要基于其构建的类的名称。关键字 extends 用于表明我们的新类是一个 subclass。每当您使用 extends 时,您就是在声明该类 inherits 另一个现有类的所有特性。

备注

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

备注

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

备注

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

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

备注

在 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() 中的注释替换为指令,让 jeroo jessica 从 (3, 4) 到 (5, 4) 种下一行三朵花。

6.1.2. 类层次结构与继承

类之间如何关联?在 Java 以及任何其他面向对象语言中,类被组织成一个 类层次结构。类层次结构就像一棵倒置的树。层次结构的最顶端是最通用的类。在 Java 中,最通用的类是 Object 类。层次结构中位于 Object 下方的类被称为其 子类。由于我们在程序中使用的所有对象都属于某个类,这就相当于说所有对象都是 Object。

下图展示了使用本节中描述的类来说明类层次结构的概念。请注意, Object 类位于层次结构的顶部。它是最通用的类。它具有所有 Java 对象共有的特征。随着你向下移动层次结构,类变得越来越专门化。一个 Rectangle 是一个 Object ,但它包含属性—— length 和 width ——这些是所有矩形共有但层次结构中其他对象所没有的。例如,一个 ATM 对象不一定具有长度和宽度。请注意,我们在层次结构中添加了一个 square 类。一个 square 是一种特殊的 Rectangle ,即其长度等于宽度的那种。

_images/ClassHierarchy.png

为了介绍与此类层次结构相关的一些重要术语,我们说 Rectangle 类是 Object 类的子类。 square 类既是 Rectangle 的子类,也是 Object 的子类。在层次结构中位于给定类之上的类被称为其 超类。因此, Rectangle 类是 square 类的超类。 Object 类也是 square 的超类。一般来说,我们说子类 扩展 超类,这意味着它在超类所包含的元素(属性和/或方法)基础上添加了额外的元素。我们在 square 类的案例中看到了这一点。它增加了一个特性,即其长度和宽度始终相等。

与类层次结构相关的另一个重要概念是 类继承,即子类从其父类继承所有属性和方法。以自然界为例,想想马和哺乳动物之间发生的那种继承关系。马是一种哺乳动物。因此,马凭借其作为哺乳动物的身份,继承了温血的特征。(这不同于个体层面的遗传,例如你从母亲那里继承了蓝眼睛,从父亲那里继承了黑头发)。

为了说明继承在编程中的工作原理,考虑一个国际象棋程序。存在几种不同类型的 ChessPiece 对象。有 Pawn、 Knight、 Queen 和 King。下图展示了棋子层次结构。

_images/ChessPieceHierarchy.png

所有棋子共有的一对属性是它们在棋盘上的 row 和 column 位置。由于所有棋子都具有这些共同属性,它们位于 ChessPiece 层次结构的顶部,并由所有 ChessPiece 子类继承。当然, row 和 column 属性在每个 ChessPiece 对象中具有不同的值。

所有棋子共有的动作之一是它们可以 moveTo() 棋盘上的某个方格。但不同类型的棋子有不同的移动方式。例如, Bishop 只能沿棋盘的对角线移动,而 Rook 只能沿棋盘的 row 或 column 移动。因此,显然我们无法描述一个适用于所有 ChessPiece 的 moveTo() 方法。这就是为什么我们将 moveTo() 方法放入所有 ChessPiece 子类中的原因。 ChessPiece 类也有一个 moveTo() 方法,但请注意其名称在图中以斜体显示。这表明该方法无法在该层级被完全定义。

最后,请注意在国际象棋中,王具有某些特殊的属性和动作。因此,只有王会被 将军。这意味着王正受到攻击并面临被捕获的危险,从而导致游戏结束。同样,只有王具备易位的权利。这是在特定条件下,王可以与其中一个车共同执行的特殊移动。因此,我们在 King 类中展示 inCheck 属性和 castle() 动作的原因在于,这些是 King 对象所特有的特征。

通过这种方式,类层次结构在从上到下移动时表示类的 特化。最通用的类 ChessPiece 位于层次结构的顶部。其属性和方法会传递给(被其子类继承)子类。然而,除了从父类继承的属性和方法外,子类还定义了自己特有的属性和方法。每个子类,如 Pawn 、 Bishop 等,都代表了父类的某种特化。在此示例中,每个子类都有自己独特的移动方式。而 King 子类拥有独特的属性和动作( inCheck 和 castle() )。

6.1.2.1. 创建子类

让我们考虑上一节中的国际象棋示例。如果我们想创建一个 ChessPiece 类,它可能看起来像这样。现在,我们不必担心属性或方法,只需关注类定义即可。

public class ChessPiece
{

}

创建 King 类将如下所示:

public class King
    extends ChessPiece
{

}

此 extends 关键字告诉 Java, King 类是 ChessPiece 的子类。

6.1.2.2. 创建更智能的 Jeroo

除了创建 Island 的子类以提供不同的地图外,我们还可以创建自己的 Jeroo 子类。如果您创建了 Jeroo 的子类,就可以为其添加新方法,从而使您的个人 Jeroo 理解更多动作词汇。然后,您可以使用这些新方法解决问题。

例如,假设我们希望 Jeroo 学会同时跳跃和种花——也就是说,每次向前跳跃时,它也种下一朵花(如果它有的话)。我们可以通过添加一个 hopAndPlant() 方法来实现这一点。但要添加方法,我们需要一个自己的类来编写它。

Try It Yourself

创建一个名为 Jeroo 的子类(名称由你决定,记得首字母大写)。编写一个名为 hopAndPlant() 的方法,该方法先跳跃一格,然后种下一朵花。

6.1.3. 总结:什么是继承?

6.1.4. 问题解决与算法

学习解决问题的基本开发流程。本章内容并非计算机解题所独有,该流程可用于解决各类问题,包括与计算机无关的问题。

6.1.4.1. 问题、解决方案与工具

我遇到了一个问题!我需要感谢凯阿姨送我的生日礼物。我可以通过邮寄发送感谢信。我可以打电话给她。我可以给她发电子邮件。我可以开车去她家当面感谢她。事实上,我有许多方式可以感谢她,但这不是重点。重点是我必须决定如何解决这个问题,并使用合适的工具来实施(执行)我的计划。邮政服务、电话、互联网和我的汽车都是我可以使用的工具,但它们中没有任何一个能真正解决我的问题。类似地,计算机并不能解决问题,它只是我用来实施解决问题计划的工具。

知道凯阿姨欣赏创意和独特的事物,我决定雇佣一位歌唱信使来传达我的谢意。在此情境中,信使是一种工具,但需要我给出指令。我必须告诉信使凯阿姨的住址、我希望消息送达的时间,以及我想让他演唱的歌词。计算机程序类似于我给信使的指令。

凯阿姨的故事利用了一个熟悉的场景,为关于计算机和计算机程序的一个有用观点奠定了基础。以下列表总结了该观点的关键方面。

备注

  • 计算机是一个可以用来实现解决问题计划的工具。

  • 计算机程序是一组给计算机的指令。这些指令描述了计算机必须遵循的步骤来实现一个计划。

  • 算法(algorithm)是解决问题的计划。

  • 人必须设计算法。

  • 人必须将算法翻译成计算机程序。

这一观点为我们开发 Jeroo 问题解决方案的过程奠定了基础。该基本过程非常重要,因为它可用于解决各种问题,包括那些需要用其他编程语言编写解决方案的问题。

6.1.4.2. 算法开发过程

每个问题的解决方案都始于一个计划。该计划称为算法。

备注

算法 是解决问题的计划。

编写算法的方式有很多种。有些非常非正式,有些则相当正式且具有数学性质,还有些则以图形化方式呈现。将 DVD 播放器连接到电视机的说明就是一种算法。像 \(\pi R^2\) 这样的数学公式是算法的一个特例。形式并不特别重要,只要它能提供一种良好的方式来描述和检查计划的逻辑即可。

算法(即计划)的开发是解决问题的关键步骤。一旦有了算法,我们就可以将其翻译成某种编程语言的计算机程序。我们的算法开发过程包含五个主要步骤。

步骤 1:获取问题描述 """""""""""""""""""""""""""""""""""""""""""

这一步比看起来要困难得多。在下面的讨论中,client 一词指的是希望找到问题解决方案的人,而 developer 一词指的是找到解决问题方法的人。developer 必须创建一个能够解决 client 问题的算法。

客户负责创建问题描述,但这往往是过程中最薄弱的环节。问题描述常常存在以下一种或多种缺陷:(1) 描述依赖于未声明的假设,(2) 描述含糊不清,(3) 描述不完整,或 (4) 描述存在内部矛盾。这些缺陷很少是由于客户的疏忽造成的。相反,它们是由于自然语言(英语、法语、韩语等)相当不精确所致。开发人员的部分职责是识别问题描述中的缺陷,并与客户合作修复这些缺陷。

步骤 2:分析问题 """""""""""""""""""""""""""

此步骤的目的是确定解决问题的起点和终点。这个过程类似于数学家确定已知条件和必须证明的结论。良好的问题描述使执行此步骤更加容易。

在确定起点时,我们应首先尝试回答以下问题:

  • 有哪些可用数据?

  • 这些数据位于何处?

  • 哪些公式与该问题相关?

  • 处理这些数据时存在哪些规则?

  • 数据值之间存在哪些关系?

在确定终点时,我们需要描述解的特征。换句话说,我们如何知道何时完成?提出以下问题通常有助于确定终点。

  • 我们将获得哪些新事实?

  • 哪些项会发生变化?

  • 对这些项做了哪些更改?

  • 哪些事物将不再存在?

步骤 3:开发高层算法

算法是解决问题的计划,但计划有多个详细程度级别。通常最好从一个高层算法开始,该算法包含解决方案的主要部分,而将细节留待后续处理。我们可以用一个日常示例来演示高层算法。

问题: 我需要寄一张生日贺卡给我的哥哥 Mark。
分析: 我没有贺卡。我更喜欢买一张贺卡,而不是自己制作。
高层算法:

前往一家出售贺卡的商店
挑选一张贺卡
购买贺卡
邮寄贺卡

该算法足以满足日常使用,但若由计算机执行解决方案,则需补充若干细节。这些细节包括对如下问题的解答:

  • "我会去哪家商店?"

  • "我该如何前往:步行、开车、骑自行车,还是乘坐公交车?"

  • "Mark 喜欢什么样的卡片:幽默型、感伤型,还是略带挑逗型?"

这些细节将在我们流程的下一步中加以考虑。

步骤 4:通过添加更多细节来完善算法

高层算法展示了解决问题所需遵循的主要步骤。现在我们需要为这些步骤添加细节,但应该添加多少细节呢?不幸的是,这个问题的答案取决于具体情况。我们必须考虑由谁(或什么)来实施该算法,以及该人(或事物)已经知道如何完成多少工作。如果有人要代表我购买 Mark 的生日卡片,我的指示必须根据此人是否熟悉社区中的商店,以及购买者对我兄弟在贺卡品味上的了解程度来进行调整。

当我们的目标是开发能够转化为计算机程序的算法时,我们需要考虑计算机的能力并提供足够的细节,以便其他人能够使用我们的算法编写出遵循算法步骤的计算机程序。与生日卡片问题一样,我们需要调整细节层次以匹配程序员的能力。在有疑问或正在学习时,提供过多细节优于提供过少细节。

我们的大多数示例都会在单一步骤中从高层算法过渡到详细算法,但这并非总是合理的。对于更大、更复杂的问题,通常会多次经历这一过程,在此过程中逐步开发出中间层次的算法。每一次,我们都为前一个算法添加更多细节,直到看不出进一步细化还有何益处为止。这种从高层逐步过渡到详细算法的技术,通常称为 逐步细化。

备注

逐步细化 是一个通过逐步向高级算法添加细节来开发详细算法的流程。

步骤 5:回顾算法

最后一步是审查算法。我们在寻找什么?首先,我们需要逐步执行算法,以确定它是否能够解决原始问题。一旦我们确信该算法确实提供了解决方案,就开始寻找其他方面。以下问题是审查算法时通常应该提出的典型问题。提出这些问题并寻求答案,是培养可应用于下一个问题的技能的好方法。

  • 该算法是解决一个 非常具体的问题,还是解决一个 更一般的问题?如果它解决的是一个非常具体的问题,是否应该将其泛化?例如,计算半径为 5.2 米的圆面积的算法(公式 \(\pi (5.2)^2\) )解决的是一个非常具体的问题,而计算任意圆面积的算法(公式 \(\pi R^2\) )解决的是一个更一般的问题。

  • 该算法能否被 简化?计算矩形周长的一个公式是:

    length + width + length + width

    一个更简单的公式是:

    2.0 * (长度 + width)

  • 这个解法是否与另一个问题的解法 相似?它们有何相同之处?又有何不同?例如,考虑以下两个公式:

    矩形面积 = length * width
    三角形面积 = 0.5 * base * height

    相似之处:每个都计算面积。每个都将两个测量值相乘。
    不同之处:使用的测量值不同。三角形公式包含 0.5。
    假设:也许每个面积公式都涉及将两个测量值相乘。

6.1.4.3. 示例:拾取与种植

本节包含一个扩展示例,用于演示算法开发过程。要完成该算法,我们需要知道每个 Jeroo 都可以向前跳跃、向左或向右转弯、从当前位置采摘花朵,以及在当前位置种植花朵。

问题陈述(步骤 1)""""""""""""""""""""""""""

Jeroo 从 (0, 0) 出发,面朝东,花袋中没有花。位置 (3, 0) 处有一朵花。编写一个程序,指导 Jeroo 采摘这朵花并将其种植在位置 (3, 2)。种下花后,Jeroo 应向东跳跃一格并停止。岛上没有其他网、花或 Jeroo。

开始

_images/4.1-start.png

完成

_images/4.1-finish.png

问题分析(第 2 步)""""""""""""""""""""""""""""""""

  1. 花朵正好在 Jeroo 前方三个空格处。

  2. 花朵将被种植在其当前位置正南方两个空格处。

  3. Jeroo 最终应面向东方,位于已种植花朵东侧一个空格处。

  4. 无需担心任何网。

高层算法(步骤 3)"""""""""""""""""""""""""""""

让我们将 Jeroo 命名为 Bobby。Bobby 应该执行以下操作:

取花
放花
向东跳

详细算法(步骤 4)"""""""""""""""""""""""""""

让我们将 Jeroo 命名为 Bobby。Bobby 应该执行以下操作:

获取花朵

跳跃 3 次
采摘花朵

放置花朵

右转
跳跃 2 次
种一朵花

向东跳

向左转
单脚跳一次

回顾算法(第 5 步)"""""""""""""""""""""""""""""

  1. 该高层算法将问题划分为三个相当简单的子问题。这似乎是一个好技巧。

  2. 该算法解决了一个非常具体的问题,因为 Jeroo 和花位于非常具体的位置。

  3. 该算法实际上解决了一个稍更通用的问题,其中 Jeroo 从任意位置开始,而花位于 Jeroo 正前方 3 个空格处。

“拾取与放置”的 Java 代码""""""""""""""""""""""""""""""

优秀的程序员不会一次性写完整个程序。相反,程序员会通过一系列构建来编写和测试程序。每次构建都会在前一次的基础上进行扩展。高层算法将在此过程中指导我们。

备注

优秀的程序员会 增量式 地工作,一次添加一小部分代码,并不断检查已完成的工作。

首次构建 """""""""""

要查看此解决方案的实际效果,请创建一个新的 BlueJ 项目场景,并使用“新建类..."按钮创建一个名为任意名称的新 Island 子类。这个子类将容纳您的新代码。

推荐的首次构建包含三件事:

  1. 主方法(在您的 Island 子类中为 myProgram() )。

  2. 声明并实例化将要使用的每个 Jeroo 对象。

  3. 以注释形式呈现的高层算法。

public void myProgram()
{
    Jeroo bobby = new Jeroo();
    this.addObject(bobby, 0, 0);

    // --- Get the flower ---

    // --- Put the flower ---

    // --- Hop East ---

}   // ===== end of method myProgram() =====

myProgram() 开头的实例化将 bobby 放置在 (0, 0),面朝东,且没有花。

一旦第一次构建正常工作,我们就可以继续进行其他构建。在这种情况下,每次构建将对应高层算法中的一个步骤。为如此简单的程序使用四次构建似乎工作量很大,但这样做有助于养成习惯,随着程序变得更加复杂,这些习惯将变得无比宝贵。

第二次构建 """"""""""""

此构建添加了“获取花朵”的逻辑,在详细算法(上述步骤 4)中,该逻辑包括跳跃 3 次然后采摘花朵。新代码由注释标示,这些注释不会出现在原始代码中(它们仅用于引起对新增内容的注意)。空行有助于展示逻辑的组织结构。

public void myProgram()
{
    Jeroo bobby = new Jeroo();
    this.addObject(bobby, 0, 0);

    // --- Get the flower ---
    bobby.hop(3);     // <-- new code to hop 3 times
    bobby.pick();     // <-- new code to pick the flower

    // --- Put the flower ---

    // --- Hop East ---

}   // ===== end of method myProgram() =====

花点时间运行目前的工作,您可以确认计划算法中的此步骤是否按预期工作。在 BlueJ 主窗口中右键单击您的 island 子类,并使用"new"(第一个菜单项)创建一个新实例。

第三次构建 """""""""""

此构建添加了“放置花朵”的逻辑。新增代码由此处提供的注释标出,以标记这些新增内容。

public void myProgram()
{
    Jeroo bobby = new Jeroo();
    this.addObject(bobby, 0, 0);

    // --- Get the flower ---
    bobby.hop(3);
    bobby.pick();

    // --- Put the flower ---
    bobby.turn(RIGHT);    // <-- new code to turn right
    bobby.hop(2);         // <-- new code to hop 2 times
    bobby.plant();        // <-- new code to plant a flower

    // --- Hop East ---

}   // ===== end of method myProgram() =====

第四次构建(最终版)""""""""""""""""""""

此构建添加了“向东跳跃”的逻辑。

public void myProgram()
{
    Jeroo bobby = new Jeroo();
    this.addObject(bobby, 0, 0);

    // --- Get the flower ---
    bobby.hop(3);
    bobby.pick();

    // --- Put the flower ---
    bobby.turn(RIGHT);
    bobby.hop(2);
    bobby.plant();

    // --- Hop East ---
    bobby.turn(LEFT);     // <-- new code to turn left
    bobby.hop();          // <-- new code to hop 1 time

}   // ===== end of method myProgram() =====

6.1.4.4. 示例:将 Net 替换为 Flower

本节包含第二个示例,用于演示算法开发过程。

问题陈述(步骤 1)""""""""""""""""""""""""""

岛上有两个 Jeroo。一个 Jeroo 从 (0, 0) 出发,面朝北,囊中有一朵花。第二个从 (0, 2) 出发,面朝东,囊中也有一朵花。在位置 (3, 2) 处有一张网。编写一个程序,指挥第一个 Jeroo 将其花交给第二个 Jeroo。收到花后,第二个 Jeroo 必须禁用该网,并在原位置种下一朵花。种下花后,该 Jeroo 必须转身面朝南。岛上没有其他网、花或 Jeroo。

开始

_images/4.2-start.png

完成

_images/4.2-finish.png

问题分析(第 2 步)""""""""""""""""""""""""""""""""

  1. Jeroo_2 正好位于 Jeroo_1 后方两个空格处。

  2. 唯一的网正好位于 Jeroo_2 前方三个空格处。

  3. 每个 Jeroo 正好有一朵花。

  4. Jeroo_2 在从 Jeroo_1 接收一朵花后将拥有两朵花。
    必须使用一朵花来禁用网。
    另一朵花必须种植在网的位置,即 (3, 2)。

  5. Jeroo_1 将结束于 (0, 1),面向南方。

  6. Jeroo_2 将结束于 (3, 2),面向南方。

  7. 每个 Jeroo 结束时其花袋中将剩余 0 朵花。一朵花被用于禁用网,另一朵被种植。

高层算法(步骤 3)"""""""""""""""""""""""""""""

让我们将第一个 Jeroo 命名为 Ann,第二个命名为 Andy。

Ann 应该执行以下操作:

找到 Andy(但不要与他碰撞)
送一朵花给 Andy(他就在正前方)

收到花后,Andy 应该执行以下操作:

找到网(但不要跳上去)
禁用网
在网的位置种一朵花
面向南

详细算法(步骤 4)"""""""""""""""""""""""""""

让我们将第一个 Jeroo 命名为 Ann,第二个命名为 Andy。

Ann 应该执行以下操作:

查找 Andy

转身(向左或向右两次)
跳跃(到位置 (0, 1))

给 Andy 一朵花

提前给出

现在 Andy 应该执行以下操作:

找到净

跳两次(到位置 (2, 2))

禁用网

抛掷

在网
的位置种一朵花

跳跃(至位置 (3, 2))
种花

面向南

右转

回顾算法(第 5 步)"""""""""""""""""""""""""""""

  1. 高层算法有助于管理细节。

  2. 该算法解决一个非常具体的问题,但具体位置并不重要。唯一重要的是 Jeroos 彼此之间的起始位置,以及网相对于第二个 Jeroo 的位置和方向的位置。

“用花替换网”的 Java 代码

与之前一样,代码应以 增量 方式逐步编写。此问题适合采用四次构建。通常,第一次构建将包含 main 方法、Jeroo 对象的声明与实例化,以及以注释形式呈现的高层算法。第二次构建将使 Ann 将其花朵交给 Andy。第三次构建将使 Andy 定位并禁用网。在最后一次构建中,Andy 将放置花朵并转向东。

首次构建 """""""""""

此构建会创建 main 方法,实例化 Jeroos,并概述高层算法。在此示例中,其中 myProgram() 这个 main 方法位于 Island 的子类中。

public void myProgram()
{
    Jeroo ann  = new Jeroo(NORTH, 1);
    this.addObject(ann, 0, 0);
    Jeroo andy = new Jeroo(1);  // default EAST
    this.addObject(andy, 0, 2);

    // --- Ann, find Andy ---

    // --- Ann, give Andy a flower ---

    // --- Andy, find and disable the net ---

    // --- Andy, place a flower at (3, 2) ---

    // --- Andy, face South ---

}   // ===== end of method myProgram() =====

第二次构建 """"""""""""

此构建添加了让 Ann 定位 Andy 并送给他一朵花的逻辑。

public void myProgram()
{
    Jeroo ann  = new Jeroo(NORTH, 1);
    this.addObject(ann, 0, 0);
    Jeroo andy = new Jeroo(1);  // default EAST
    this.addObject(andy, 0, 2);

    // --- Ann, find Andy ---
    ann.turn(LEFT);
    ann.turn(LEFT);
    ann.hop();
    // Now, Ann is at (0, 1) facing South, and Andy is directly ahead

    // --- Ann, give Andy a flower ---
    ann.give(AHEAD);       // Ann now has 0 flowers, Andy has 2

    // --- Andy, find and disable the net ---

    // --- Andy, place a flower at (3, 2) ---

    // --- Andy, face South ---

}   // ===== end of method myProgram() =====

第三次构建 """""""""""

此构建添加了让 Andy 定位并禁用网的逻辑。

public void myProgram()
{
    Jeroo ann  = new Jeroo(NORTH, 1);
    this.addObject(ann, 0, 0);
    Jeroo andy = new Jeroo(1);  // default EAST
    this.addObject(andy, 0, 2);

    // --- Ann, find Andy ---
    ann.turn(LEFT);
    ann.turn(LEFT);
    ann.hop();
    // Now, Ann is at (0, 1) facing South, and Andy is directly ahead

    // --- Ann, give Andy a flower ---
    ann.give(AHEAD);       // Ann now has 0 flowers, Andy has 2

    // --- Andy, find and disable the net ---
    andy.hop(2);           // Andy is at (2, 2) facing the net
    andy.toss();

    // --- Andy, place a flower at (3, 2) ---

    // --- Andy, face South ---

}   // ===== end of method myProgram() =====

第四次构建(最终版)""""""""""""""""""""

此构建添加了逻辑,使 Andy 能够在 (3, 2) 放置一朵花并转向南方。

public void myProgram()
{
    Jeroo ann  = new Jeroo(NORTH, 1);
    this.addObject(ann, 0, 0);
    Jeroo andy = new Jeroo(1);  // default EAST
    this.addObject(andy, 0, 2);

    // --- Ann, find Andy ---
    ann.turn(LEFT);
    ann.turn(LEFT);
    ann.hop();
    // Now, Ann is at (0, 1) facing South, and Andy is directly ahead

    // --- Ann, give Andy a flower ---
    ann.give(AHEAD);       // Ann now has 0 flowers, Andy has 2

    // --- Andy, find and disable the net ---
    andy.hop(2);           // Andy is at (2, 2) facing the net
    andy.toss();

    // --- Andy, place a flower at (3, 2) ---
    andy.hop();
    andy.plant();

    // --- Andy, face South ---
    andy.turn(RIGHT);

}   // ===== end of method myProgram() =====

6.1.5. 创建和使用 Jeroo 方法

对于某些问题,如果我们能够扩展 Jeroo(或其他对象)的基本行为将会很方便。Java 允许我们编写程序员定义的方法,以扩展从给定类创建的每个对象的行为。

6.1.5.1. 创建并使用 Jeroo 方法

行为和 方法 的概念之前已定义,此处重复说明。 行为 是对象在响应外部来源的请求时可以采取的动作或可以执行的任务。 方法 是用某种编程语言编写的一组语句,用于描述特定行为。

这些定义意味着方法的创建是一个两部分的过程。首先,我们需要定义并命名新行为。其次,我们需要编写该方法的源代码。

定义行为 """""""""""""""""""

我们首先要问的问题是“如何决定一个好的行为?”这个问题没有固定的答案,但有一些指导原则可供遵循。

  1. 检查高层算法。任何复杂但定义明确的步骤都是新行为的候选,尤其是当两个或更多 Jeroos 需要执行该步骤时。

  2. 检查详细算法。任何多次出现的步骤序列都是新行为的候选。

这些指南可作为起点,但经验是一位好老师。检查您自己的程序以及他人的程序。良好的行为具有非常清晰的定义,并在程序中多次使用。

编写 Jeroo 方法 """"""""""""""""""""""

Jeroo 方法包含源代码,用于描述任意 Jeroo 为执行相应行为所需执行的操作。Jeroo 方法的形式如下:

_images/method_structure1.png

第一行(标题行)上的 methodIdentifier 是程序员为方法选择的名称。该名称应指示相应的行为。创建方法标识符的规则与 模块 1 中给出的规则相同——但请记住,我们总是以小写字母开头命名方法。在每个方法中,我们都应该缩进左花括号和右花括号之间的每一行。

备注

方法的名称应该是一个 动词 或一个描述方法执行内容的简短 动词短语 。

由于 Jeroo 方法定义的行为适用于每一个 Jeroo,因此我们无法向特定的 Jeroo 发送消息。相反,我们使用特殊的 Java 名称 this,它就像一个代词,指代正在执行整个方法的 Jeroo。

示例:转身 """"""""""""""""""""

如果我们想添加一个方法使 Jeroo 转身,就需要一个类来放置它。我们必须创建自己的 Jeroo 子类来容纳我们的代码。在 BlueJ 中,你可以使用"New Class..."按钮创建一个自定义名称的 Jeroo 新子类。在那个新子类中,你可以添加一个让 Jeroo 转身的方法:

// ----------------------------------------------------------
/**
 * Turn the jeroo around 180 degrees so it faces the opposite
 * direction.
 */
public void turnAround()
{
    this.turn(LEFT);
    this.turn(LEFT);
}

示例:一个方法可以使用另一个方法,甚至使用它自身

本示例介绍了两种新行为:在一行中种植四朵花,以及种植两行相邻的行,每行四朵花。

// ----------------------------------------------------------
/**
 * Plant four flowers in a row, starting at the current location.
 */
public void plantFour()
{
    this.plant();   // -- one ---

    this.hop();
    this.plant();   // -- two ---

    this.hop();
    this.plant();   // -- three ---

    this.hop();
    this.plant();   // -- four ---
}


// ----------------------------------------------------------
/**
 * Plant two adjacent rows of flowers.
 */
public void plantRowsOfFour()
{
    // --- Plant first row ---
    this.plantFour();

    // --- Move into position for next row ---
    this.turn(RIGHT);
    this.hop();
    this.turn(RIGHT);

    // --- Plant second row (in opposite direction) ---
    this.plantFour();
}

使用 Jeroo 方法 """"""""""""""""""""

Jeroo 方法的使用方式与其他任何方法相同。在我们岛屿的 myProgram() 方法中,我们只需确保从包含所需新方法的特殊子类创建一个 Jeroo 对象。然后,我们向特定的 Jeroo 对象发送消息,请求该 Jeroo 执行与方法关联的任务。

例如,假设我们创建了自己的 Jeroo 子类,称为 PlantingJeroo ,并向其中添加了 plantFour() 和 plantRowsOfFour() 方法。那么在我们的 island 子类中,我们可以让一个名为 Ali 的新 Jeroo 在 (5, 5) 的南侧和东侧种下两排花:

public void myProgram()
{
    PlantingJeroo ali = new PlantingJeroo(8);
    this.addObject(ali, 5, 5);

    ali.plantRowsOfFour();
}

关于构造函数的说明 """""""""""""""""""""""""

我们知道,当创建一个 子类 时,它会 继承 它所 扩展 的类的所有方法和属性。如果你创建了一个 Jeroo 的子类 PlantingJeroo ,那么任何 PlantingJeroo 对象都可以执行任何 Jeroo 所知的方法——因为 PlantingJeroo 是一种特殊的 Jeroo 。 PlantingJeroo 类继承了 Jeroo 类的所有方法和属性,并且还能理解你编写的任何新方法,例如 platRowsOfFour() 方法。计算机科学家有时将这种关系称为 is-a 关系,因为每个 PlantingJeroo 对象同时 是一个 Jeroo ——只是一个能做更多事情的 Jeroo。

备注

is-a 关系存在于子类和其父类之间,因为子类的每个实例同时也是父类的实例。

此外,正如上文所述,构造函数 是一种特殊的方法,用于初始化一个全新的对象。但是,虽然子类会自动继承其父类的所有(普通)方法和属性,它不会继承构造函数。这意味着前一个示例中为 Ali 创建对象实例的代码实际上无法编译——除非我们为 PlantingJeroo 子类提供一个合适的构造函数。

子类不会自动继承构造函数的一个原因是,除了新方法之外,子类还可以添加新属性,而这些属性 必须被初始化,无论如何都要如此。但来自超类的任何构造函数都不会知道子类的这些新属性,也无法适当地对它们进行初始化。因此,子类必须始终显式定义它们支持的每一个构造函数。

备注

每次创建子类时,你都有责任定义它支持的 所有 构造方法。构造方法不会从父类继承。

幸运的是,虽然构造函数不会被继承,但定义它们有一个简单的模式。在我们的 PlantingJeroo 中,可以添加以下构造函数:

// ----------------------------------------------------------
/**
 * Create a new Jeroo facing east.
 * @param flowers   The number of flowers the Jeroo is holding.
 */
public PlantingJeroo(int flowers)
{
    super(flowers);
}

虽然我们尚未涵盖这段小程序代码的所有特性,但其要点很直接。构造方法的声明方式与普通方法类似,只是我们 省略了 void 一词,且其名称 与类名完全相同。在此,我们为 PlantingJeroo 子类定义了一个构造方法,它接受一个数字(整数)作为参数,表示其花袋中的花朵数量。

此构造函数的函数体仅包含一行代码,使用了特殊的 Java 关键字 super 。该关键字只能作为子类构造函数中的第一个词使用,它允许我们调用父类构造函数,并传递其可能需要的任何信息。因此在这里,我们表示 PlantingJeroo 构造函数中的第一个(也是唯一一个)操作是调用其父类( Jeroo )的构造函数,并传递花的数量。这使得父类能够利用给定信息正确地初始化其所有属性。如果我们的子类需要更多的初始化操作,我们将在子类构造函数函数体的后续语句中执行这些操作。

但就目前而言,此构造函数足以满足我们的 PlantingJeroo 类。它允许我们通过指定位置和花朵数量来创建一个 PlantingJeroo 对象。这将使我们能够毫无问题地实例化前一个示例中的 Ali Jeroo。

备注

继承为构造方法增加了一层新的复杂性。一个常见的错误是忘记子类构造方法必须调用父类构造方法。如果你没有显式调用 super(),Java 编译器会尝试为你插入一个无参调用,如果父类没有默认构造方法,这将失败。了解这样的陷阱与学习正确的语法同样重要;这是成为优秀程序员的关键部分。

6.1.5.2. 前置条件与后置条件

在编写对应方法的代码之前,我们应当始终仔细定义其行为。一个完整的行为定义必须包含前置条件和后置条件的陈述。方法的 前置条件 是指在调用该方法之前被假定为真的内容。调用该方法的代码部分负责确保在调用方法之前所有前置条件均得到满足。方法的 后置条件 是指在方法执行完成后为真的内容。方法内部的代码负责确保所有后置条件均得以实现。确定合适的前置条件和后置条件的过程可能颇具挑战,但如果我们牢记对象和方法的一些特性,这一过程会变得更容易。

  1. 所有工作都是通过向对象发送消息来完成的。

  2. 恰好有一个对象在收到消息后执行一个方法。

  3. 方法可以修改执行该方法的对象的属性,但不能直接修改任何其他对象的属性。

  4. 一个方法可以向多个不同的对象发送消息,而这些消息可能导致其接收者发生修改。

以前述特征列表为指导,我们可以使用以下问题作为编写前置条件和后置条件的基础。当我们使用 Jeroos 时,需要考虑一个方法如何改变执行该方法的 Jeroo 对象的属性。在某些情况下,像 pick() 、 plant() 和 toss() 这样的 Jeroo 动作可以通过添加或移除对象来改变世界的属性,尽管我们通常不会直接向这些其他对象发送消息。在幕后, pick() 、 plant() 和 toss() 方法会向岛屿发送适当的消息,以添加或移除与所需行为相对应的对象。

Precondition QuestionsPostcondition Questions
Do any of the attributes of the receiving object need to have special values?
Location
Direction
Flowers
How does this method affect the attributes of the receiving object?
Location
Direction
Flowers
Are the contents of certain island cells important? Have the contents of any island cells changed?

前置条件和后置条件可以非正式地创建,但最终版本应以注释块的形式写在方法源代码的开头。例如,考虑上一节中在一行种植四朵花的方法:

// ----------------------------------------------------------
/**
 * Plant four flowers in a row, starting at the current location.
 *
 * @precondition The three spaces directly ahead of the Jeroo are clear.
 * @precondition The Jeroo has at least four flowers.
 * @postcondition The Jeroo has planted four flowers, starting at its
 *     current location and proceeding straight ahead.
 * @postcondition The Jeroo is standing on the last flower, and facing in
 *     its original direction.
 */
public void plantFour()
{
   this.plant();   // -- one ---

   this.hop();
   this.plant();   // -- two ---

   this.hop();
   this.plant();   // -- three ---


   this.hop();
   this.plant();   // -- four ---
}

6.1.5.3. 示例:清除网并拾取

本节包含一个扩展示例,演示算法开发过程,并展示开发包含 Jeroo 方法的源代码的推荐流程。

问题陈述(步骤 1)""""""""""""""""""""""""""

一个 Jeroo 从 (4, 1) 出发,面朝北,花袋里有 5 朵花。在 Jeroo 正南方的 (4, 2) 和 (4, 3) 位置有两个网。第二个网的正南方有一朵花。编写一个程序,指导 Jeroo 禁用这两个网并采摘那朵花。采摘花朵后,Jeroo 应返回起始位置并面朝南。

开始

_images/5.1-start.png

完成

_images/5.1-finish.png

问题分析(第 2 步)""""""""""""""""""""""""""""""""

  1. Jeroo 必须转身以定位第一张网

  2. 每张网都位于前一张网的正南方

  3. 第一张网位于 Jeroo 的正南方

  4. 花位于位置 (4, 4)

  5. Jeroo 必须在位置 (4, 1) 结束且面向南方

  6. Jeroo 结束时应有 5 - 2 + 1 = 4 朵花

详细算法(步骤 3 和 4)""""""""""""""""""""""""""""""""""

让我们将 Jeroo 命名为 Kim。Kim 应该执行以下操作:

转身 // 现在位于 (4, 1),面向南
禁用两行网

抛掷
跳跃一次 // 现在位于 (4, 2),面向南
抛掷
跳跃一次 // 现在位于 (4, 3),面向南

获取花朵

跳跃一次 // 现在位于 (4, 4) 的花朵上,面朝南
采摘

返回 (4, 1) 并转身

转身 // 现在位于 (4, 4),面朝北
跳跃 3 次 // 现在位于 (4, 1),面朝北
转身 // 现在位于 (4, 1),面朝南

回顾算法(第 5 步)"""""""""""""""""""""""""""""

  1. 高层算法有助于管理细节。

  2. 我们在示例 4.2 中使用了一个“转身”步骤。我们在这里可以使用相同的逻辑。

  3. 转身动作既作为高层算法中的一个步骤出现,也作为“回到 (4, 1) 并转身”步骤的一部分出现。有趣!

可能的行为 """"""""""""""""""

  1. “转身”使用了三次

  2. 序列“抛,跳”在“禁用网”步骤中使用了两次。

我们将创建一个自定义的 Jeroo 子类,并为每种行为编写一个 Jeroo 方法,但首先,我们需要为每个方法定义目的、前置条件和后置条件。这可以非正式地完成,因为我们将把这些内容写在每个方法开头的注释块中。

备注

方法: turnAround()
目的:使 Jeroo 旋转 180 度
前置条件:

无

后置条件:

Jeroo 已旋转 180 度
Jeroo 位于同一位置

备注

方法: tossAndHop()
目的:禁用网并移动到新清理的位置
前置条件:

前方有网
Jeroo 至少有一朵花

后置条件:

网已被禁用
Jeroo 少了一朵花
Jeroo 位于原本被网占据的位置
Jeroo 的方向未发生改变

tossAndHop() 方法的最后一个后置条件仅说明 Jeroo 面向的方向与其在方法开始时的方向相同。它并不禁止 Jeroo 在方法执行过程中改变方向,只要 Jeroo 在结束时回到其原始方向即可。

“清理网并拾取”的 Java 代码"""""""""""""""""""""""""""""""""""

和之前一样,我们应该将代码开发为一系列构建步骤。要开始此过程,请使用 BlueJ 创建一个新项目,使用"New Class..."按钮创建 Island 的一个新子类 ClearNetsAndPick 用于本示例。同时,创建 Jeroo 的一个新子类 ClearingJeroo 来存放您的 Jeroo 方法。创建这些类后,请确保它们已编译。编辑 ClearNetsAndPick 类中提供的构造函数,在岛上的适当起始位置创建并添加花和网。然后右键单击 ClearNetsAndPick 类并创建其一个实例,这将填充世界视图。

首次构建 """""""""""

推荐的首次构建包含三件事:

  1. 您的岛屿子类中的 myProgram() 方法,用于创建消息并将其发送给 Jeroo。

  2. 声明并实例化将要使用的每个 Jeroo。这意味着需要向我们的 Jeroo 子类添加一个适当的构造函数。

  3. 以注释形式呈现的高层算法。

  4. 您的 Jeroo 子类中每个 Jeroo 方法的骨架。这些骨架通常称为桩。

备注

方法桩 (或简称 桩)是能够编译的方法骨架,实际上只是为后续真实方法定义预留的占位符。

myProgram() 方法位于你的 ClearNetsAndPick 类中:

public void myProgram()
{
    Jeroo kim = new Jeroo(NORTH, 5);
    this.addObject(kim, 4, 1);

    // --- Turn around ---

    // --- Disable nets ---

    // --- Get the flower ---

    // --- Go back to (4, 1) and turn around ---

}

合适的构造函数和新的 Jeroo 方法应放在你的 ClearingJeroo 类中:

// ----------------------------------------------------------
/**
 * Create a new Jeroo.
 * @param direction The direction the Jeroo is facing.
 * @param flowers   The number of flowers the Jeroo is holding.
 */
public Jeroo(CompassDirection direction, int flowers)
{
    super(direction, flowers);    // Let the superclass initialize these
}


// ----------------------------------------------------------
/**
 * Turn the jeroo around 180 degrees so it faces the opposite
 * direction.
 *
 * @precondition  None.
 *
 * @postcondition The Jeroo has turned 180 degrees.
 * @postcondition The Jeroo is at the same location.
 */
public void turnAround()
{
}


// ----------------------------------------------------------
/**
 * Disable a net and move to the newly cleared location.
 *
 * @precondition  There is a net ahead.
 * @precondition  The Jeroo has at least one flower.
 *
 * @postcondition The net has been disabled.
 * @postcondition The Jeroo has one less flower.
 * @postcondition The Jeroo is at the location originally occupied by the net.
 * @postcondition The Jeroo has not changed direction.
 */
public void tossAndHop()
{
}

第二次构建 """"""""""""

本次构建完成了 turnAround() 方法,并在 myProgram() 方法中使用了它。明智的做法是测试该方法四次,每次让 Kim 面向不同的方向开始。一旦我们确信该方法能正确工作,就可以继续进行下一次构建。

在 ClearNetsAndPick 类中:

public void myProgram()
{
    Jeroo kim = new Jeroo(NORTH, 5);
    this.addObject(kim, 4, 1);

    // --- Turn around ---
    kim.turnAround();                 // new code

    // --- Disable nets ---

    // --- Get the flower ---

    // --- Go back to (4, 1) and turn around ---

}

在 ClearingJeroo 类中:

// ----------------------------------------------------------
/**
 * Turn the jeroo around 180 degrees so it faces the opposite
 * direction.
 *
 * @precondition  None.
 *
 * @postcondition The Jeroo has turned 180 degrees.
 * @postcondition The Jeroo is at the same location.
 */
public void turnAround()
{
    this.turn(LEFT);                  // new code
    this.turn(LEFT);                  // new code
}


// ----------------------------------------------------------
/**
 * Disable a net and move to the newly cleared location.
 *
 * @precondition  There is a net ahead.
 * @precondition  The Jeroo has at least one flower.
 *
 * @postcondition The net has been disabled.
 * @postcondition The Jeroo has one less flower.
 * @postcondition The Jeroo is at the location originally occupied by the net.
 * @postcondition The Jeroo has not changed direction.
 */
public void tossAndHop()
{
}

第三次构建 """""""""""

此构建完成了 tossAndHop() 方法,并在 myProgram() 方法中使用了它。我们的重点是销毁这两个网。

在 ClearNetsAndPick 类中:

public void myProgram()
{
    Jeroo kim = new Jeroo(NORTH, 5);
    this.addObject(kim, 4, 1);

    // --- Turn around ---
    kim.turnAround();

    // --- Disable nets ---
    kim.tossAndHop();                 // new code
    kim.tossAndHop();                 // new code

    // --- Get the flower ---

    // --- Go back to (4, 1) and turn around ---

}

在 ClearingJeroo 类中:

// ----------------------------------------------------------
/**
 * Turn the jeroo around 180 degrees so it faces the opposite
 * direction.
 *
 * @precondition  None.
 *
 * @postcondition The Jeroo has turned 180 degrees.
 * @postcondition The Jeroo is at the same location.
 */
public void turnAround()
{
    this.turn(LEFT);
    this.turn(LEFT);
}


// ----------------------------------------------------------
/**
 * Disable a net and move to the newly cleared location.
 *
 * @precondition  There is a net ahead.
 * @precondition  The Jeroo has at least one flower.
 *
 * @postcondition The net has been disabled.
 * @postcondition The Jeroo has one less flower.
 * @postcondition The Jeroo is at the location originally occupied by the net.
 * @postcondition The Jeroo has not changed direction.
 */
public void tossAndHop()
{
    this.toss();          // new code
    this.hop();           // new code
}

第四次构建(最终版)""""""""""""""""""""

此构建完成了 myProgram() 方法。我们需要检查 Kim 在最后是否拥有正确数量的花朵。

在 ClearNetsAndPick 类中:

public void myProgram()
{
    Jeroo kim = new Jeroo(NORTH, 5);
    this.addObject(kim, 4, 1);

    // --- Turn around ---
    kim.turnAround();

    // --- Disable nets ---
    kim.tossAndHop();
    kim.tossAndHop();

    // --- Get the flower ---
    kim.hop();            // new code
    kim.pick();           // new code

    // --- Go back to (4, 1) and turn around ---
    kim.turnAround();     // new code
    kim.hop(3);           // new code
    kim.turnAround();     // new code
}

6.1.6. 什么是多态?

所有 Jeroo 都理解同一组消息——也就是说,它们响应相同的方法调用。但既然你已经了解了如何定义自定义方法,子类也可以 重新定义 某个方法以执行更多操作或改变其行为。事实证明,不同的 Jeroo 可能会以不同方式响应 相同的方法调用,具体取决于它们所属的子类。我们使用术语 接收者 来指代被调用方法的对象。每次调用方法时,接收者都会决定如何响应,因此确切的行为取决于用于创建该接收者的特定子类中该方法是如何定义的。

多态 意味着不同的接收者可以以不同的方式响应相同的方法调用。多态不仅仅是一个理论概念;它是编写清晰、灵活且可维护代码的强大工具。本质上,它允许单个接口表示多种底层形式。例如,如果你有一个 Vehicle 超类和诸如 Car 、 Bicycle 和 Truck 之类的子类,那么一个将 Vehicle 作为参数的函数可以与任何这些子类一起工作,而无需在编译时知道它们的具体类型。这对于构建可扩展系统非常有用。你可以添加一个新的子类,比如 Motorcycle ,而你现有的处理 Vehicle 对象 的代码仍然可以正常运行,无需任何更改。这种 单一接口,多种实现 的概念是多态在实际应用中的核心优势。

6.1.7. 编程练习 6

   «  5.1. 字段、封装与二维像素网格   ::   目录   ::   7.1. 面向对象继承、重写与接口  »

关闭窗口