OpenDSA 完整目录

Chapter 0 Introduction to Software Design

| 关于   «  2. 面向对象继承、重写与接口   ::   目录   ::   4. 软件测试基础与测试驱动开发  »

3. 方法、选择与重复动作

“计算机科学不是编程。相反,编程是我们艺术的媒介,正如写作是英语及其他专业的媒介一样。而且,像所有写作一样,它通过重写得以精炼。诀窍在于在手指敲击键盘之前,先在脑海中完成重写。”——Nick Parlante

在上一章中,你学习了如何创建现有类的实例并调用它们的方法。在本章中,你将迈作为软件设计师的下一步:创建你自己的自定义子类,定义新方法来赋予角色专门的行为,使用传感器查询配合 if 和 if-else 语句做出决策,以及使用 while 循环来重复动作。

3.1. 创建更聪明的 Jeroo(子类基础)

3.1.1. 创建子类

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

public class ChessPiece
{

}

创建 King 类则看起来像这样:

public class King
    extends ChessPiece
{

}

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

3.1.2. 创建更聪明的 Jeroo

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

作为一个例子,假设我们希望我们的 jeroo 知道如何同时跳跃和种植花——也就是说,每次它向前跳跃一步时,它也种一朵花(如果它有的话)。我们可以通过添加一个我们自己的 hopAndPlant() 方法来做到这一点。但要添加一个方法,我们需要一个自己的类来编写它。

自己试试 (Try It Yourself)

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

3.2. 总结:什么是继承?

3.3. 创建和使用 Jeroo 方法

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

3.3.1. 创建和使用 Jeroo 方法

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

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

3.3.1.1. 定义行为

我们必须问的第一个问题是"我如何决定一个好的行为?"这个问题没有固定答案,但有一些准则可以遵循。

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

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

这些准则作为起点,但经验是好老师。检查你自己和他人的程序。一个好的行为有非常清晰的定义,并在程序中使用多次。

3.3.1.2. 编写 Jeroo 方法

Jeroo 方法包含描述一个任意 Jeroo 需要做什么来执行相应行为的源代码。Jeroo 方法的形式是:

_images/method_structure1.png

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

备注

方法的名称应该是 动词 或描述方法功能的简短 动词短语 。

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

3.3.1.3. 示例:转身

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

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

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

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

// ----------------------------------------------------------
/**
 * 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();
}

3.3.1.5. 使用 Jeroo 方法

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

作为一个例子,假设我们创建了自己的 Jeroo 子类 PlantingJeroo,并向其添加了 plantFour() 和 plantRowsOfFour() 方法。然后在我们的岛屿子类中,我们可以让一个名为 Ali 的新 Jeroo 在 (5, 5) 的南方和东方种植两行花:

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

    ali.plantRowsOfFour();
}

3.3.2. 前置条件和后置条件

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

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

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

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

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

以上面的特征列表为指南,我们可以使用以下问题作为编写前置条件和后置条件的基础。在使用 Jeroo 时,我们需要考虑一个方法如何改变执行该方法的 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 ---
}

3.3.3. 示例:清除网并采摘

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

3.3.3.1. 问题陈述(步骤 1)

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

开始

_images/5.1-start.png

结束

_images/5.1-finish.png

3.3.3.2. 问题分析(步骤 2)

  1. Jeroo 必须转身以找到第一个网

  2. 每个网都在前一个的正南方

  3. 第一个网在 Jeroo 的正南方

  4. 花在位置 (4, 4)

  5. Jeroo 必须在位置 (4, 1) 朝南结束

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

3.3.3.3. 详细算法(步骤 3 和 4)

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

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

投掷
跳跃一次 // 现在在 (4, 2) 朝南
投掷
跳跃一次 // 现在在 (4, 3) 朝南

获取花朵

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

返回 (4, 1) 并转身

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

3.3.3.4. 检查算法(步骤 5)

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

  2. 我们在示例 4.2 中使用了"转身"步骤。我们可以在同样的逻辑在这里。

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

3.3.3.5. 可能的行为

  1. "转身"被使用了三次

  2. "投掷、跳跃"序列在"禁用网"步骤中被使用两次。

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

备注

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

无

后置条件:

Jeroo 已转身 180 度
Jeroo 在同一位置

备注

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

前方有一个网
Jeroo 至少有一朵花

后置条件:

网已被禁用
Jeroo 少了一朵花
Jeroo 位于网原先占据的位置
Jeroo 没有改变方向

tossAndHop() 方法的最后一个后置条件只是说 Jeroo 面朝方法开始时的方向。只要 Jeroo 在方法结束时恢复到原始方向,它并不禁止 Jeroo 在方法执行过程中改变方向。

3.3.3.6. "清除网并采摘"的 Java 代码

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

3.3.3.7. 第一次构建

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

  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()
{
}

3.3.3.8. 第二次构建

这次构建完成了 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()
{
}

3.3.3.9. 第三次构建

这次构建完成了 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
}

3.3.3.10. 第四次构建(最终)

这次构建完成了 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
}

3.4. 选择

到目前为止,我们编写的方法都有一个共同特征——顺序执行。顺序执行意味着语句按照它们在源代码中出现的顺序一条接一条地执行。在本章中,我们将学习如何创建一个语句块,我们可以选择执行或跳过它,以及如何选择执行多个替代语句块中的一个。

控制结构 是编程语言的一个特征,它决定语句将按什么顺序执行。控制结构有三类:(1)顺序结构,(2)选择结构,和(3)重复结构(也称为循环)。

顺序结构是大多数面向对象编程语言使用的默认结构:将一条语句放在另一条之后,按顺序排列。除非我们另有说明,否则语句将按它们在源代码中出现的顺序执行,并且每条语句将恰好执行一次。到目前为止我们看到的每个程序都只使用顺序结构。

条件 是选择结构和重复结构的关键部分。条件是任何可以为真或假的表达式。

备注

条件 是任何可以为真或假的表达式。

每个 选择结构 定义了通过源代码的两条或多条替代路径。有三种重要的选择结构:if-then-else 结构、if-then 结构和多路分支结构。

if-then-else 结构(有时称为 if-else 结构)是最基本的选择结构,因为它可以用来形成任何可能的选择模式。if-then-else 结构定义了两个不同的语句块,其中只有一个会被执行。

if-then 结构(有时简称为 if 结构)是 if-then-else 的一种特殊形式,只提供一个语句块,第二个语句块被省略。因此,if-then 结构定义了一个可选的语句块,该语句块要么被执行,要么被跳过。

多路 if 结构(有时称为多路分支结构)定义了多个不同的语句块,其中只有一个会被执行。Java 还有一个 switch 结构来创建多路分支结构,但我们稍后会学习它。

3.5. 使用传感器方法的条件

Jeroo 类提供了许多 传感器查询方法,可用于询问 Jeroo 关于其周围环境的情况。每个传感器查询方法的结果要么为真,要么为假。任何产生真或假结果的方法都被称为 布尔方法 (也称为 谓词 )。更一般地说,任何为真或假的表达式都被称为 boolean 表达式(以 George Boole 命名)。这意味着在各种控制结构中使用的条件实际上是 boolean 表达式。

备注

任何产生真或假结果的方法都被称为 布尔方法,也称为 谓词。

对于 Jeroo,传感器查询方法是创建条件的基本构建块。创建条件最简单的方法是调用传感器查询方法。下表列出了 Jeroo 类提供的所有传感器查询方法。这些方法只能用于构造条件。由于它们是方法,因此通过向 Jeroo 对象发送消息来调用它们。

Sensor Methods

方法

用途

示例

hasFlower()

这只 Jeroo 有花吗?

dean.hasFlower()

isClear(relativeDirection)

指定方向有空位吗?空位是指没有花、没有网、没有水且没有 Jeroo 的位置。[isClear(HERE) 无意义]

dean.isClear(LEFT)

isFacing(compassDirection)

这只 Jeroo 面朝指定方向吗?

dean.isFacing(NORTH)

seesFlower(relativeDirection)

指定方向有花吗?

dean.seesFlower(HERE)

seesJeroo(relativeDirection)

指定方向有其他 Jeroo 吗?[seesJeroo(HERE) 无意义]

dean.seesJeroo(AHEAD)

seesNet(relativeDirection)

指定方向有网吗?[seesNet(HERE) 无意义]

dean.seesJeroo(RIGHT)

seesWater(relativeDirection)

指定方向有水吗?[seesWater(HERE) 无意义]

dean.seesWater(AHEAD)

值得注意的是,当你看到 relativeDirection 时,你的方向选择包括 HERE、LEFT、RIGHT 和 AHEAD。当你看到 compassDirection 时,你的方向选择是:NORTH、SOUTH、EAST 和 WEST。

3.6. 条件语句概述

3.7. Java 中 if-then-else 结构的语法

_images/ifThenElseDiagram2.png

此图展示了 Java 中 if-then-else 结构的语法。关于语法有四个重要事项需要注意。

  1. 条件 必须 在圆括号中。

  2. 圆括号后 没有分号。

  3. 关键字 else 后没有分号。

  4. if-then-else 结构不是方法,这意味着我们不会将其作为消息发送给 Jeroo 对象。

关于编码风格有三个重要事项需要注意。

  1. 花括号用于定义真分支和假分支的开始和结束。始终包含它们。

  2. 花括号与 if 和 else 单词的开头对齐。

  3. 花括号之间的语句应缩进(我们使用 4 个空格)。

3.7.1. 示例(if-then-else 结构)

让名为 Timmy 的 Jeroo 检查正前方是否有网。如果有一个,让他禁用它并转身。如果正前方没有网,Timmy 应该向右转。在他禁用网并转身或只是右转之后,Timmy 必须向前移动一格。

if (timmy.seesNet(AHEAD))
{
    timmy.toss();
    timmy.turn(LEFT);
    timmy.turn(LEFT);
}
else
{
    timmy.turn(RIGHT);
}

timmy.hop();

3.8. 使用 If-then 创建可选语句

if-then 结构(有时称为 if 结构)定义了一个可选的语句块。if-then 结构就是一个 if-then-else 结构,其中第二个语句块(else 部分)被省略,因此选择变成了执行第一个语句块 还是跳过它。

if 结构有两个部分:选择条件和真分支。真分支包含一个只有在选择条件为真时才会执行的语句块。

_images/ifThenDiagram.png

上图展示了一个通用的 if-then 结构,并使用箭头展示语句将按什么顺序执行。if 结构定义了可选代码,该代码要么被跳过,要么只执行一次。

3.9. Java 中 if-then 结构的语法

_images/ifThenDiagram2.png

此图展示了 Java 中 if-then 结构的语法。关于语法有三个重要事项需要注意。

  1. 条件 必须 在圆括号中。

  2. 圆括号后 没有分号。

  3. if-then 结构不是方法,这意味着我们不会将其作为消息发送给 Jeroo 对象。

关于编码风格有三个重要事项需要注意。

  1. 花括号用于定义真分支的开始和结束。始终包含它们。

  2. 花括号与 if 单词的开头对齐。

  3. 花括号之间的语句应缩进(我们使用 4 个空格)。

3.9.1. 示例(if-then 结构)

让名为 Jessica 的 Jeroo 检查右侧是否有网。如果有一个,让她禁用它并恢复到当前状态。无论她是否禁用了网,Jessica 都应向前跳跃一格。

if (jessica.seesNet(RIGHT))
{
    jessica.turn(RIGHT);
    jessica.toss();
    jessica.turn(LEFT);
}

jessica.hop();

3.10. Java 中多向选择结构的语法(级联 if)

_images/multiWayIfDiagram.png

此图展示了在 Java 中编写多向选择结构的常用技术。严格来说,此结构由一系列嵌套的 if-then-else 语句组成,但编码风格掩盖了这一事实,使多向选择逻辑更加可见。这种特定结构通常称为 级联 if。

关于此结构有五个重要事项需要注意。

  1. 条件 必须 在圆括号中。

  2. 圆括号后 没有分号。

  3. else-if 块的数量没有限制。

  4. 最后的 else 分支是可选的。

  5. 此结构不是方法,这意味着我们不会将其作为消息发送给 Jeroo 对象。

关于编码风格有三个重要事项需要注意。

  1. 花括号用于定义真分支的开始和结束。始终包含它们。

  2. 花括号与 if 和 else 单词的开头对齐。

  3. 花括号之间的语句应缩进(我们使用 4 个空格)。

3.10.1. 示例(多向选择结构)

假设一只名为 Louisa 的 Jeroo 至少携带一朵花。让她检查前方的单元格。如果该单元格包含一朵花,就采摘它。如果该单元格包含一个网,就禁用它。如果该单元格包含水,就在当前位置种一朵花。如果该单元格包含另一只 Jeroo,就给那只 Jeroo 一朵花。最后,如果该单元格中什么都没有,让她跳跃一次并向左转。

if (louisa.seesFlower(AHEAD))
{
    louisa.hop();
    louisa.pick();
}
else if (louisa.seesNet(AHEAD))
{
    louisa.toss();
}
else if (louisa.seesWater(AHEAD))
{
    louisa.plant();
}
else if (louisa.seesJeroo(AHEAD))
{
    louisa.give(AHEAD);
}
else
{
    louisa.hop();
    louisa.turn(LEFT);
}

3.11. 你对课程评分政策的看法

请完成以下调查。调查包含关于你对本课程评分政策的看法以及你如何在本课程中学习的问题。我们将利用这些信息来更好地了解这些政策对你的影响,从而改进课程。

在调查快结束时,你将被问及是否同意将你的回答和课程数据用于关于评分政策的研究。如果你同意将你的数据用于研究,你不需要做任何额外的工作。完成调查应该不到 30 分钟。你的课程导师不会知道你是否同意参与,直到你完成课程并提交了最终成绩。无论你是否同意参与研究或拒绝回答部分或全部问题,提交此调查都将获得作业学分。

CS 1114 Grading Policies Survey 1

请在下面回答,你的调查提交将被验证以获得学分。

3.12. 重复动作

在前面的章节中,我们学习了如何使用 if-then-else 或 if-then 结构来决定执行哪个动作。在本节中,我们将学习如何创建一个可以连续多次执行的语句块。我们使用重复结构(也称为循环)来做到这一点,它是大多数命令式和面向对象编程语言支持的基本控制结构之一。

重复结构 (或 循环 )允许一组语句连续执行多次。有三种重要的重复结构:对对象集合中的每个对象重复一个动作的循环、由程序中对象的状态控制的循环,以及由计数器(通常是数字)控制的循环。在本章中,我们将只关注一种循环——由程序中对象的状态控制的循环。这恰好是许多编程语言中最通用、最基本的重复结构。

3.13. 通用重复结构

每个重复结构有两个主要部分:循环体和控制条件。这两个部分提供了分类循环的方式。

可以重复执行的语句块称为 循环体 。每次执行循环体中的语句称为一次循环 遍历 (或 迭代 ),循环体被执行的次数称为 遍历计数 。

控制条件 是用于确定是继续执行循环体还是终止循环的条件。控制条件在每次遍历循环体后都会被重新检查。

分类循环的一个标准是基于控制条件相对于第一次遍历循环体的检查时机。在 前测循环 中,控制条件总是在循环体第一次执行之前被检查。在 后测循环 中,控制条件直到第一次遍历循环体之后才被检查。在任一情况下,条件在每次遍历循环体后都被检查,以确定是否进行另一次遍历。

分类循环的第二个标准是基于真条件还是假条件导致遍历循环体。在 while 循环 中,真条件导致遍历循环体,而假条件终止循环。在 until 循环 中,真条件终止循环,而假条件导致遍历循环体。while 和 until 循环之间的区别总结在此表中:

_images/whileVsUntil.png

结合这两个标准,我们可以定义四大类循环:前测 while、前测 until、后测 while 和后测 until。很少有编程语言提供所有这四种(大多数只提供两种,甚至一种!),但在几乎所有命令式和面向对象编程语言中支持的最常见的形式是前测 while 循环。在本章的剩余部分,我们将专门关注 前测 while 循环。

_images/whileVsUntil2.png

由于前测 while 循环是命令式和面向对象语言中最常见的重复结构,我们将对它进行更仔细的考察。

_images/whileDiagram.png

上图展示了一个通用的前测 while 循环,并使用箭头展示语句执行和条件检查的顺序。

3.14. Java 中 while 循环的语法

_images/whileDiagram2.png

上图展示了 Java 中前测 while 循环的 Java 语法。关于语法有三个重要事项需要注意。

  1. 条件 必须 在圆括号中。

  2. 包含条件的圆括号后或关键字 else 后 没有分号。

  3. while 结构不是方法,这意味着我们不会将其作为消息发送给 Jeroo 对象。

关于编码风格有三个重要事项需要注意。

  1. 花括号用于定义 while 语句体的开始和结束。始终包含它们。

  2. 花括号与 while 单词的开头对齐。

  3. 花括号之间的语句应缩进(我们使用 4 个空格)。

3.14.1. 示例(前测 while 结构)

假设一只名为 Kim 的 Jeroo 没有站在花上,但前方有一排花。让 Kim 摘完所有这些花,一旦前方没有花就停下来。摘完所有花后,Kim 应该向左转。

while (kim.seesFlower(AHEAD))
{
    kim.hop();
    kim.pick();
}

kim.turn(LEFT);

3.15. 初尝测试:验证 Jeroo 行为

当你编写程序或为 Jeroo 创建新方法时,你怎么知道它确实有效?

到目前为止,你可能已经在 BlueJ 中可视化地运行了你的程序,看着 Jeroo 跳过岛屿网格,并检查它是否最终到达了正确的位置。虽然可视化检查在入门时很有帮助,但它有严重的局限性:

  1. 它很 慢,每次更改代码都需要手动操作。

  2. 它 容易遗漏细微的 bug (比如多转了一次或提前停了一个单元格)。

  3. 随着程序变大,你无法手动检查每个可能的场景。

在专业的软件开发中,程序员编写 自动化单元测试——小型、专门的代码片段,其唯一工作就是执行特定的方法或动作,并验证结果是否符合预期。

3.15.1. 测试类的结构

在 Java 中,单元测试被组织在一个扩展 student.TestCase 的单独类中。按照惯例,如果你正在测试一个名为 NetRemover 的类,你的测试类就叫 NetRemoverTest。

这是一个完整、可工作的测试类示例:

import student.TestCase;
import static org.assertj.core.api.Assertions.*;

// -------------------------------------------------------------------------
/**
 * Unit tests for the NetRemover class.
 */
public class NetRemoverTest extends TestCase
{
    // 1. Declare fields to hold the objects you will test
    private Island island;
    private NetRemover jeroo;

    // ----------------------------------------------------------
    /**
     * The setUp() method runs automatically BEFORE every single test method.
     * It creates a fresh island and actor so each test starts from a clean slate.
     */
    public void setUp()
    {
        island = new Island();
        jeroo = new NetRemover();
        island.addObject(jeroo, 3, 1);
    }

    // ----------------------------------------------------------
    /**
     * Test that turnAround() correctly faces the Jeroo in the opposite direction
     * without changing its (x, y) coordinates.
     */
    public void testTurnAround()
    {
        // 1. Execute the action to test
        jeroo.turnAround();

        // 2. Assert (verify) that the postconditions match expectations
        assertThat(jeroo.getX()).isEqualTo(3);
        assertThat(jeroo.getY()).isEqualTo(1);
        assertThat(jeroo.getHeading()).isEqualTo(WEST);
    }

    // ----------------------------------------------------------
    /**
     * Test that a jeroo correctly picks a flower and clears a net.
     */
    public void testPickAndClear()
    {
        // Place a flower on the island for testing
        island.addObject(new Flower(), 4, 1);

        // Execute the action
        jeroo.hop();
        jeroo.pick();

        // Assert that the jeroo now holds a flower in its pouch
        assertThat(jeroo.hasFlower()).isTrue();
        assertThat(jeroo.getX()).isEqualTo(4);
    }
}

3.15.2. 理解关键部分

  1. 测试夹具(setUp): setUp() 方法很特殊。在 JUnit 运行 任何 测试方法之前,它先执行 setUp()。这保证了每个测试方法都使用一个全新的、独立的岛屿和角色来运行,防止一个测试干扰另一个测试。

  2. 测试方法(public void test...()): 每个测试方法必须是 public void,且其名称 必须以 test 开头 (如 testTurnAround 或 testPickAndClear)。当你在 BlueJ 中运行测试类时,JUnit 自动找到并运行每个以 test 开头的方法。

  3. 使用 assertThat(...) 进行断言: 断言 是检查条件是否为真的语句。如果断言成功,测试通过(显示绿色条)。如果断言失败(例如,如果 jeroo.getX() 是 2 而不是 3),测试立即停止并报告错误(显示红色条)。

3.15.3. Jeroo 的常用断言

在测试 Jeroo 方法时,你将最频繁地使用三类断言:

Common Jeroo Assertions

断言语法

检查内容

assertThat(jeroo.getX()).isEqualTo(expectedX);

验证 Jeroo 的水平 x 坐标。

assertThat(jeroo.getY()).isEqualTo(expectedY);

验证 Jeroo 的垂直 y 坐标。

assertThat(jeroo.getHeading()).isEqualTo(EAST);

验证 Jeroo 的罗盘方向(EAST、SOUTH、WEST 或 NORTH)。

assertThat(jeroo.hasFlower()).isTrue();

验证 Jeroo 持有至少一朵花。

assertThat(jeroo.isWater(AHEAD)).isFalse();

验证正前方没有水。

在第 3 章中,你将更深入地探索测试——学习如何设计全面的测试套件、测试 if-else 语句和循环的所有分支,以及实践测试驱动开发(TDD)。但现在,你已经具备了为你的 Jeroo 方法编写自动化检查的能力!

3.16. 编程练习 2

 

   «  2. 面向对象继承、重写与接口   ::   目录   ::   4. 软件测试基础与测试驱动开发  »

关闭窗口