MengBridgeCourse 中文版

第 1 章 继承与多态

| 关于   英文版   «  1.2. 单元 1 实验 1 占位符   ::   目录   ::   1.4. 单元 1 实验 2 占位符  »

1.3. 继承与多态:子类与方法

“计算机科学并非编程。相反,编程是我们艺术的媒介,正如写作是英语和其他专业的媒介一样。首先,我们教你如何通过这种媒介表达你的问题解决能力,然后你在许多令人愉悦的分支学科中练习富有艺术性的问题解决。在这门课程中,你会感觉像是在用一种全新的方式思考和写作。学习编程具有变革性和赋能作用。” -- Todd Neller

1.3.1. 桑通岛的 Jeroos 对象

在异岛上野生动物模拟的背景下,进入基础 Java 编程。

_images/jeroo.png

1.3.1.1. 欢迎来到三同岛

三同岛是南太平洋上的一块无人居住的小陆地。在 20 世纪 80 年代末,自然学家杰西卡·通发现了两个此前未知的物种,这两个物种是该岛独有的。其中一种是 杰鲁,一种稀有的哺乳动物,类似于澳大利亚的负鼠。另一种是大型的 温森花,它是杰鲁的主要食物来源。

像它遥远的澳大利亚亲戚 jerboas 一样,jerboas 通过跳跃移动,但它的移动方式在自然界中是独一无二的。每个 jerboas 只在四个主要罗盘方向之一中移动,并且只在 90 度增量下转向。这种异常行为继续让 Jessica 和少数其他在野外观察到 jerboas 的研究人员感到困惑。有些人认为这种行为与地磁力量以某种方式相关,但其他人认为这仅仅是一种奇怪的习得行为。

每只杰鲁都有育儿袋,但雄性的育儿袋略小一些。雌性使用育儿袋来保护和哺育后代,而两性都会使用育儿袋来携带它们采摘的温苏姆花。

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

直到最近,杰罗布才免受人类干扰,因为它们仅为一小群研究人员所知,而且三同岛非常小、非常偏远,且未出现在大多数地图上。2001 年,布莱恩抵达该岛,中断了杰罗布如诗如画的生存世界。布莱恩是一位著名的收藏家,受雇于密苏里州的 C. A. Baret 动物园,目的是捕捉一只杰罗布用于极端动物展览。在研究了杰罗布独特的移动模式后,布莱恩定期在杰罗布可能造访的位置设置网具。幸运的是,敏感的杰罗布能够察觉这些网具,并学会将一种名为 winsum 的花抛到网上,从而使网具失效,让该位置在未来通行变得安全。布莱恩只能捕捉那些在观察之前就跳跃的粗心杰罗布。

1.3.1.2. 一个 Jeroo 模拟器

在刚从桑通岛(Santong island)旅行归来后,Jessica 请她的同事 Deanna 开发一个模拟器,用于演示 jeroos 及其与岛屿的关系。Deanna 的工作成果是一个用 Java 编写的计算机程序,旨在帮助 Jessica 进行研究。该程序提供了桑通岛的微观世界表示,并展示了岛上一个或多个 jeroos 的行为。

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

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

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

方向 """""""""

每个对象都知晓四个相对方向与四个罗盘方向,如下表所示。每个方向必须用大写字母书写(在 Java 中,永不改变的常量值通常以此方式书写)。相对方向 HERE 指向对象的当前位置,因此对于上一节中的动作方法并无用处(我们稍后将看到 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.3.1.4. 在岛屿上创建 Jeroos 对象

Island 类的一个实例相当普通:

_images/island.png

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

public class NewClassName
    extends ExistingClassName
{
    ...
}

关键字 public class 表示我们正在引入一个全新的 类,该 类 可以在我们的代码中的任何位置使用(即,它公开地可供其他地方使用)。NewClassName 是我们正在定义的新 类 的名称,而 ExistingClassName 是我们想要在其基础上构建的 类 的名称。关键字 extends 表示我们的新 类 是一个 子类。每当使用 extends 时,您都在声明您正在定义的 类 继承 了另一个现有类的所有功能。

备注

一个 子类 是一个 继承 另一个类的所有方法和属性,该另一个类被称为它的 父类。

备注

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

备注

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

在 Eclipse 中,我们可以通过在 Package Explorer 视图中右键单击项目的名称,并选择 New -> Class 菜单命令,从 Island 类创建一个新的子类。在生成的新类对话框中,输入新类的名称。要指定父类,请查找 Superclass: 字段,并单击该字段右侧的 Browse 按钮。在搜索框中开始输入 "island",然后选择 Island (来自 student.micro.jeroo)。单击 "OK" 按钮以创建您的新类。此操作将为您在适当的源文件中创建一个新类,并填充一些初始内容(包括一些注释和占位符)。

备注

在 Java 中,给定类的源代码存储在一个纯文本文件中,该文件的名称 与类名相同,并添加了 ".java" 后缀。文件名的大小写必须与类名完全一致。

当您在 Eclipse 中创建一个新的子类时,新类将需要您添加一个 构造函数。构造函数是一种特殊的方法,仅用于初始化全新的对象。

备注

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

在任何新创建的对象中,你总是可以以一个简单的构造函数开始,以便稍后添加信息。例如,如果你创建了一个新的 MyIsland 子类,可以这样提供一个最小构造函数:

// ----------------------------------------------------------
/**
 * Initializes a newly created MyIsland object.
 */
public MyIsland()
{
    super();
}

在我们的岛屿子类的构造函数中,您可以定义岛屿的尺寸,并指定在创建岛屿时要执行的操作——例如在其中填充一些花朵,或在新位置放置水。

我们也可以通过添加一个 myProgram() 方法来描述按下 运行按钮 时我们希望发生什么。您可以将此方法添加到您的岛屿子类中:

// ----------------------------------------------------------
/**
 * Describe what your code does here.
 */
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 从 (3, 4) 到 (5, 4) 种植一行三朵花的 jessica 指令。

1.3.1.5. 自我检测

1.3.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 子类继承。当然,每个 ChessPiece 对象中的 row 和 column 属性具有不同的值。

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

最后,请注意,在国际象棋中,国王具有某些特殊属性和动作。因此,只有国王可以被 将军。这意味着国王受到攻击并处于被捕获的危险之中,从而结束游戏。同样,只有国王有能力进行王车易位。这是一种国王在特定条件下可以与其中一个车一起进行的特殊走法。因此,我们在 King 类中显示 inCheck 属性和 castle() 动作的原因是因为这些是特定于 King 对象的特征。

通过这种方式,类层次结构表示从顶部到底部对类的 特化。最一般的类,ChessPiece,位于层次结构的顶部。它的属性和方法被传递给(由)其子类继承。然而,除了从它们的超类继承的属性和方法之外,子类还定义它们自己的特殊属性和方法。每个子类,Pawn,Bishop 等等,都表示超类某种形式的特化。在这个例子中,每个子类都有其独特的移动方式。而 King 子类具有独特的属性和操作(inCheck 和 castle())。

1.3.2.1. 创建子类

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

public class ChessPiece
{

}

创建 King 类将如下所示:

public class King
    extends ChessPiece
{

}

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

1.3.2.2. 自我检测

1.3.2.3. 创建更智能的 Jeroos

除了创建 Island 的子类以提供不同的地图外,我们还可以创建 Jeroo 自己的子类。如果你创建 Jeroo 的一个子类,你可以向其中添加新方法,这样你个人的 jeroo 就能理解更多的动作词汇。然后你可以使用这些新方法来解决各种问题。

例如,假设我们希望我们的跳鼠能够同时执行跳跃和种植花朵的操作——也就是说,每次它向前跳跃时,如果它拥有花朵,就同时种下一朵。我们可以通过添加一个 hopAndPlant() 方法来实现这一功能。但是,若要添加一个方法,我们需要创建一个属于我们自己的类来编写它。

Try It Yourself

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

1.3.3. 总结:什么是继承?

1.3.4. 语法练习 2a:Jeroo 方法

1.3.5. 问题求解与算法

学习开发问题解决方案的基本流程。本章中没有任何内容仅限于使用计算机解决问题。该流程可用于解决各种各样的问题,包括与计算机无关的问题。

1.3.5.1. 问题、解决方案和工具

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

既然知道凯姨欣赏富有创意和独特的事物,我决定雇佣一名歌唱信使来传达我的谢意。在此情境下,信使是一种工具,但需要我的指令。我必须告诉信使凯姨住在哪里、我希望何时送达信息以及希望唱什么歌词。计算机程序类似于我对信使的指令。

Kay 阿姨的故事利用一个熟悉的背景,为关于计算机和计算机程序的一个有用的观点设定了舞台。以下列表总结了该观点的关键方面。

备注

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

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

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

  • 人必须设计算法。

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

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

1.3.5.2. 算法开发过程

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

备注

算法 是解决问题的计划。

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

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

步骤 1:获取问题描述

这一步比看起来要困难得多。在随后的讨论中,单词 client 指想要找到问题解决方案的人,单词 developer 指找到解决问题方法的人。开发者必须创建一个算法来解决客户的问题。

客户端负责创建问题的描述,但这通常是该过程中最薄弱的环节。问题描述经常遭受以下一种或多种类型的缺陷:(1) 描述依赖于未陈述的假设,(2) 描述含糊不清,(3) 描述不完整,或 (4) 描述存在内部矛盾。这些缺陷很少是由于客户端的粗心大意造成的。相反,它们是由于自然语言(英语、法语、韩语等)相当不精确这一事实造成的。开发人员职责的一部分是识别问题描述中的缺陷,并与客户端合作以纠正这些缺陷。

步骤 2:分析问题

本步骤的目的是确定解决问题的起点和终点。这个过程类似于数学家确定已知条件和需要证明的内容。一个好的问题描述使得执行此步骤变得更加容易。

在确定起点时,我们应该首先寻求以下问题的答案:

  • 有哪些可用数据?

  • 这些数据在哪里?

  • 哪些公式适用于该问题?

  • 处理数据有哪些规则?

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

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

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

  • 哪些项将发生变化?

  • 对这些项将做出哪些更改?

  • 哪些事物将不再存在?

步骤 3:开发一个高级算法

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

问题: 我需要给我弟弟 Mark 寄一张生日贺卡。
分析: 我手头没有贺卡,我宁愿买一张贺卡也不愿自己制作。
高层算法:

前往售卖贺卡的商店
选择一张贺卡
购买一张贺卡
邮寄贺卡

该算法对于日常使用是令人满意的,但缺乏计算机执行解决方案时必须添加的细节。这些细节包括对如下问题的回答:

  • "我将访问哪家商店?"

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

  • "马克喜欢哪种卡片:幽默的、感性的还是大胆的?"

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

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

高级算法展示了解决问题所需遵循的主要步骤。现在我们需要为这些步骤添加细节,但我们应该添加多少细节呢?不幸的是,这个问题的答案取决于具体情况。我们必须考虑谁(或什么)将实现该算法,以及这个人(或事物)已经知道多少。如果某人将代表我购买马克的生日贺卡,我的指示必须根据该人是否熟悉社区中的商店以及购买者对我哥哥贺卡喜好的了解程度进行调整。

当我们的目标是开发将导致计算机程序的算法时,我们需要考虑计算机的能力,并提供足够的细节,以便其他人可以使用我们的算法编写遵循我们算法步骤的计算机程序。就像生日贺卡问题一样,我们需要调整细节的级别以匹配程序员的能力。当有疑问或当你正在学习时,拥有过多的细节比拥有过少的细节更好。

我们的大多数示例将在单个步骤中从高级算法过渡到详细算法,但这并不总是合理的。对于更大、更复杂的问题,通常需要经过多次这样的过程,在过程中逐步开发中级算法。每次,我们都会为之前的算法添加更多细节,直到进一步细化不再有益时停止。这种从高级算法逐步过渡到详细算法的技术通常被称为 stepwise refinement。

备注

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

步骤 5:回顾算法

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

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

  • 该算法是否可以 简化?计算矩形周长的一种公式为:

    length + width + length + width

一个更简单的公式是:

2.0 * (length + width)

  • 该解决方案是否 相似 于另一个问题的解决方案?它们有哪些相似之处?有哪些不同之处?例如,考虑以下两个公式:

矩形面积 = 长 * 宽
三角形面积 = 0.5 * 底 * 高

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

1.3.5.3. 示例:挑选与种植

本节包含一个扩展示例,展示了算法开发过程。为了完成该算法,我们需要知道每只 Jeroo 都可以向前跳跃、向左和向右转向、从当前位置拾取一朵花,并在当前位置种植一朵花。

问题陈述(步骤 1)

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

开始

_images/4.1-start.png

Finish

_images/4.1-finish.png

问题分析(步骤 2)

  1. 花朵正好位于跳鼠前方三个空格处。

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

  3. 跳鼠需面向东方,停在已种植花朵正东方一个空格处。

  4. 无需担心任何网。

高级算法(第 3 步)

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

获取花朵
放置花朵
向东跳跃

详细算法(第 4 步)

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

获取对象

跳跃 3 次
拾取花朵

将花放入

向右转
跳跃 2 次
种下一朵花

向东

向左转
跳跃一次

回顾算法(步骤 5)

  1. 高级算法将问题划分为三个相对简单的子问题。这似乎是一种很好的技术。

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

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

"Pick and Plant" 的 Java 代码

优秀的程序员不会一次性编写完整个程序。相反,程序员会分阶段编写和测试程序。每个构建(build)都会在前一个构建的基础上增加功能。高级算法将指导我们完成这一过程。

备注

优秀的程序员会 增量式地,一次添加一小块代码并不断检查迄今为止的工作。

首先构建对象

要查看此解决方案的运行效果,请创建一个新 Eclipse 项目,并使用"New -> Class"菜单命令创建一个名为您自选的 Island 子类。该子类将存放您的新代码。

推荐的第一个构建包含三个东西:

  1. 主方法(在此处 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() =====

通过花点时间运行到目前为止的工作,您可以确认此步骤在计划中的算法中是否按预期工作。

第三次构建

此构建添加了“放置花朵”的逻辑。新代码由此处提供的注释指示,用于标记添加内容。

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() =====

1.3.5.4. 示例:将 Net 替换为 Flower

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

问题陈述(步骤 1)

有两个 Jeroos。一个 Jeroo 从 (0, 0) 出发,面朝 North,口袋里有一朵花。第二个从 (0, 2) 出发,面朝 East,口袋里有一朵花。在位置 (3, 2) 有一个网。编写一个程序,指挥第一个 Jeroo 将其花交给第二个。收到花后,第二个 Jeroo 必须禁用该网,并在其位置种下一朵花。种下花后,Jeroo 必须转身面朝 South。岛上没有其他网、花或 Jeroos。

开始

_images/4.2-start.png

Finish

_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 结束时,其口袋里将没有花。一朵花用于禁用网,另一朵被种植了。

高级算法(第 3 步)

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

Ann 应执行以下操作:

找到 Andy(但不要与他相撞)|br| 给 Andy 一朵花(他会径直向前)

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

查找网络(但不得跳上它)|br| 禁用网络|br| 在网络所在位置种植花朵|br| 面向南方

详细算法(第 4 步)

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

Ann 应执行以下操作:

查找 Andy

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

给 Andy 一朵花

提前给出

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

找到净对象

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

禁用网络

抛掷

在网路位置|br|处种下一朵花

Hop (to location (3, 2))
种花

面向南方

向右转

回顾算法(步骤 5)

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

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

Java 代码用于“将网络替换为花朵”

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

首先构建对象

本构建创建了主方法,实例化了 Jeroos,并概述了高层算法。在此示例中,主方法将位于 myProgram() 中,该方法是 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 定位并禁用该网络(net)的逻辑。

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() =====

1.3.5.5. 自我检测

1.3.6. 创建和使用 Jeroo 对象的方法

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

1.3.6.1. 创建和使用 Jeroo 方法

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

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

定义行为

我们必须提出的第一个问题是“我该如何决定一个好的行为?”这个问题没有固定的答案,但有一些指导原则需要遵循。

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

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

这些准则仅作为起点,但经验是良师。检查你自己的程序以及他人的程序。良好的行为具有非常明确的定义,并且在程序中多次使用。

编写一个 Jeroo 方法

Jeroo 方法包含描述任意 Jeroo 需要执行什么操作以完成相应行为的源代码。Jeroo 方法的格式如下:

_images/method_structure1.png

第一行(标题行)上的 methodIdentifier 是程序员为方法选择的名字。该名字应指示相应的行为。创建方法标识符的规则与第 Module 1--但请记住,我们总是从方法名以小写 字母 开头。在每个方法中,我们应该缩进打开和关闭大括号之间的每一行。

备注

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

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

示例:掉头

如果我们想添加一个方法让 Jeroo 掉头,我们需要一个类来放置它。我们必须创建自己的 Jeroo 子类来容纳我们的代码。在 Eclipse 中,您可以使用"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 执行与方法关联的任务。

例如,假设我们创建了一个名为 PlantingJeroo 的 Jeroo 子类,并为其添加了 plantFour() 和 plantRowsOfFour() 方法。那么,在我们的岛屿子类中,我们可以有一个名为 Ali 的新跳蚤,在 (5, 5) 的南方和东方种植两排花:

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

    ali.plantRowsOfFour();
}

关于构造函数的说明

我们知道,当我们创建一个 子类 时,它会 继承 父类的所有方法和属性 extends。 如果你创建一个名为 PlantingJeroo 的 Jeroo 子类,那么任何 PlantingJeroo 对象都可以执行任何 Jeroo 知道的所有方法——因为一个 PlantingJeroo 是一种特殊的 Jeroo。PlantingJeroo 类从 Jeroo 类继承了所有方法和属性,并且也理解你编写的新方法,例如 platRowsOfFour() 方法。计算机科学家有时将这种关系称为 is-a 关系,因为每个 PlantingJeroo 对象 is a 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。

1.3.6.2. 前置条件与后置条件

在编写对应方法代码之前,我们应始终仔细定义行为。行为的完整定义必须包含前置条件和后置条件的陈述。方法的一个 前置条件 是指在该方法被调用之前假设成立的内容。调用该方法的那部分代码负责确保在方法被调用之前所有前置条件均得到满足。方法的一个 后置条件 是指在该方法执行之后成立的内容。方法内部的代码负责确保所有后置条件均得到满足。确定良好的前置条件和后置条件的过程可能很困难,但如果我们记住一些关于对象和方法的特征,这一过程会更容易。

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

  2. 恰好有一个对象响应消息并执行方法。

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

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

使用上一组特性作为指南,我们可以将以下问题作为编写前置条件和后置条件的基础。在使用 Jeroos 时,我们需要考虑方法如何改变执行该方法的 Jeroo 对象的属性。在某些情况下,Jeroo 的动作,如 pick()、plant() 和 toss(),可以通过添加或删除对象来改变世界的属性,尽管我们通常不直接向这些其他对象发送消息。在幕后,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 ---
}

1.3.6.3. 示例:清除网络并选择

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

问题陈述(步骤 1)

一个 Jeroo 对象从 (4, 1) 出发,面向 North,口袋里有 5 朵花。在 Jeroo 的 South 方向紧邻处有两个网,位置分别是 (4, 2) 和 (4, 3)。第二个网的 South 方向有一朵花。编写一个程序,指导 Jeroo 对象关闭网并采摘花朵。采摘花朵后,Jeroo 对象应返回其起始位置并面向 South。

开始

_images/5.1-start.png

Finish

_images/5.1-finish.png

问题分析(步骤 2)

  1. 对象必须转身以定位第一个网

  2. 每个网都直接位于前一个网的南方

  3. 第一个网直接位于对象的南方

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

  5. 对象必须在位置 (4, 1) 以南面向南方结束

  6. 对象应以 5 - 2 + 1 = 4 朵花结束

详细算法(步骤 3 和 4)

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

转身 // 现在位于 (4, 1) 面向南|br| 连续禁用两个网

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

获取对象

跳一次 // 现在位于 (4, 4) 的花朵上,面向南
拾取

返回到 (4, 1) 并掉头

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

回顾算法(步骤 5)

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

  2. 我们在示例 4.2 中使用了“掉头”步骤。我们可以在此使用相同的逻辑。

  3. 掉头这一行为出现在高级算法的步骤中,并且是“返回到 (4, 1) 并掉头”步骤的一部分。有趣!

可能的行为

  1. "Turn around" 被使用了三次

  2. 序列"Toss, Hop"在"Disable nets"步骤中被使用了两次。

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

备注

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

无

后置条件:

蚂蚁已旋转 180 度
蚂蚁位于同一位置

备注

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

前方有一张网
该 Jeroo 至少有一朵花

后置条件:

网络已禁用
杰罗虫少了一朵花
杰罗虫位于网络原先所在的位置
杰罗虫未改变方向

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

Java 代码用于“清除网络并选择”

同前,我们应将该代码开发为一连串的版本构建过程。为启动此过程,创建一个新项目,然后为本次示例创建一个名为 ClearNetsAndPick 的 Island 子类。此外,创建一个名为 ClearingJeroo 的 Jeroo 子类以存放你的 Jeroo 方法。一旦创建了这些类,请确保它们已编译。编辑你在 ClearNetsAndPick 类中提供的构造函数,以在岛屿上适当的位置创建并添加花朵和网。然后右键点击你的 ClearNetsAndPick 类并创建其实例,该实例将填充世界视图。

首先构建对象

推荐的第一个构建包含三个东西:

  1. 您在岛屿子类中创建并向 Jeroo 发送消息的 myProgram() 方法。

  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
}

1.3.6.4. 自我检测

1.3.7. 什么是多态性?

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

多态性 意味着不同的接收者可以以不同的方式响应相同的方法调用以不同的方式调用。

1.3.8. 语法练习 2b:子类构造函数

1.3.9. 语法练习 2c:更多子类构造函数

1.3.10. 编程练习 2

1.3.11. 检查您的理解

   «  1.2. 单元 1 实验 1 占位符   ::   目录   ::   1.4. 单元 1 实验 2 占位符  »

关闭窗口