OpenDSA 完整目录

Chapter 0 Introduction to Software Design

| 关于   «  4. 软件测试基础与测试驱动开发   ::   目录   ::   6. 第 4 周  »

5. 变量、字段和参数

5.1. 变量

编程语言最强大的特性之一是定义和操作变量的能力。 变量 是一个命名的位置,用于存储一个值。值可以是数字、文本、图像、声音以及其他类型的数据。要存储一个值,首先必须声明一个变量。

int x = 7;

每个变量都有一个类型,该类型决定了它可以存储的值的种类。此语句是一个 声明,因为它声明了名为 x 、类型为 int 的变量,并为其赋予初始值 7。

请注意, x 是变量的任意名称。一般来说,应使用能表明变量所代表含义的名称——无论是其持有的值,还是其扮演的角色。例如,如果你看到以下声明,很可能就能猜出将存储的值:

int ageInYears = 21;
int studentID = 1234;

本例声明了两个变量,每个变量的类型均为 int。当变量名包含多个单词时,如 ageInYears ,通常将除首词外每个单词的首字母大写,这与方法命名方式一致。这种命名约定称为 驼峰命名法 (有时写作 CamelCase),其中大写字母在名称中形成周期性的“凸起”。在 Java 中,如同大多数编程语言,名称是区分大小写的,因此 ageInYears 与 ageinyears 或 AgeInYears 并不相同。请记住,变量名始终以小写字母开头,而首字母大写则保留用于类名和接口名。

你可以为变量使用任意名称,但有大约 50 个保留字,称为关键字,不允许用作变量名。这些词语包括 public 、 class 、 static 、 void 和 int ,它们被编译器用来分析程序的结构。你可以在 complete list of keywords 上找到完整的关键字列表,但无需死记硬背。大多数编程编辑器提供“语法高亮”功能,使程序的不同部分以不同颜色显示。

5.1.1. 变量赋值

现在我们已经声明了变量,接下来想要使用它们来存储值。我们通过赋值语句来实现这一点

int ageInYears = 29; // 29 is the initial value for ageInYears
ageInYears = 21;  // set ageInYears to now store 21

此示例展示了一个变量声明,后接一条赋值语句。在 Java 中,所有局部变量都必须赋予初始值,我们使用相同的 "=" 字符来表示变量声明中存储的值。然而,在声明一个变量之后,我们可以随时 更改 其值,这通过 赋值语句 实现。此处,我们先写出变量本身,接着是一个等号( = ),再后跟希望该变量存储的新值。赋值语句 会改变变量中存储的值,因此下次使用该变量时,其对应值将为新值。请记住,“变量”这一名称反映了其值可在程序运行过程中 随时间变化(或变动) 的特性,而赋值正是改变该值的方法。

程序员可以使用诸如“设置 ageInYears 为 21”、“赋值 21 到 ageInYears ”或“修改 ageInYears 为 21”的短语。这里的词汇可能令人困惑,但其概念却很简单:

  • 当你声明一个变量时,你创建了一个命名的存储位置,并为其赋予一个初始值。

  • 当您向变量赋值时,会更新其值,替换掉它之前持有的任何值。

通常情况下,变量的类型必须与所赋值的类型相同。例如,您无法将字符串存入 ageInYears ,因为它只能存储整数。我们之后会看到一些看似违反此规则的例子,但稍后会解释原因。

变量在声明时必须始终被初始化(首次赋值),你可以在同一行上写出:

int ageInYears = 29;

5.1.2. 关于等号 = 的说明

让我们来看一下下面这个代码示例:

int a = 5;
int b = 7;
a = b;

因为 Java 使用 = 符号表示赋值,因此很容易将赋值语句 a = b 解释为等式陈述。它并非如此!

相等关系具有对称性,而赋值不具有。例如,在数学中若 a = 7 则 7 = a 。在 Java 中 a = 7; 是合法的赋值语句,但 7 = a; 不是。这是因为赋值语句 是一种动作,它通过将左侧变量的值设置为右侧 = 符号所出现的值来改变左侧的变量。因此,赋值语句的左侧必须是一个存储位置,如变量。

此外,在数学中,等式陈述在所有时间都成立。如果 a = b 现在成立,那么 a 一直等于 b。在 Java 中,赋值语句可以使两个变量相等,但它们不必一直保持相等:

int a = 5;
int b = a; //a and b are now equal
a = 3;     //a and b are no longer equal!

第一行声明变量 a 并将其初始化为值 5。第二行声明变量 b 并将其初始化为 a 的值,此时该值为 5。第三行将 a 的值更改为 3,但不会更改 b 的值,因此它们不再相等。 a 现在的值为 3,而 b 的值为 5。

5.1.3. 算术运算符

运算符是代表简单计算的符号。例如,加法运算符是 + ,减法是 - ,乘法是 * ,除法是 / 。

下列程序将一天中的时间转换为分钟数:

int hour = 11;
int minutes = 59;
int timeAsMinutes = hour * 60 + minutes;

在本程序中, hour * 60 + minutes 是一个 表达式,表示需要计算的单一值。运算符为 * 和 + ,分别代表乘法与除法。参与运算的变量称为 操作数 (这是参数的另一种说法)。当程序运行时,每个变量将被其当前值替换,随后应用运算符。

因此,尽管你输入 hour * 60 + minutes ,Java 仍会将其计算为 11 * 60 + 59 ,并将 timeAsMinutes 的值设为 719。表达式通常由数字、变量和运算符组合而成。在编译并执行时,它们会被求值以生成单一数值。还应注意的是,Java 中的运算顺序与数学中的规则一致。由于乘法的优先级高于加法,因此 11 * 60 会在 59 相加前先被计算。

虽然加法(+)、减法(-)、乘法(*)和除法(/)都是编程中可使用的数学运算符,但 Java 还支持两个你可能未曾见过的单目运算符。它们是 ++ 和 -- 。这两个运算符分别用于加一或减一,常用于在计算过程中逐步增加或减少变量中存储的值。

5.1.4. 关于整数与除法的一点说明

加法、减法和乘法的结果符合你的预期,但除法的结果可能会让你感到意外。例如,以下代码片段试图计算已过去的时间占一小时的分数:

int minutes = 59;
int fractionOfHour = minutes / 60;

fractionOfHour 在此情况下将实际被赋值为 0!这一结果常令人困惑。 minutes 的值是 59,而 59 除以 60 应为 0.98333,而非 0。当操作数为整数时,Java 执行“整数除法”,如本例所示。根据设计,整数除法始终向零舍入,即使在下一个整数非常接近的情况下也是如此。此外, fractionOfHour 被声明为一个 int 变量,因此它只能存储整数值,而不能存储小数部分。

作为一种替代方案,我们可以计算百分比而非分数:

int minute = 59;
int fractionOfHour = (minute * 100) / 60;

现在, fractionOfHour 被赋值为 98。结果再次向下取整,但至少此时已大致正确。

5.1.5. 新类型:双精度

虽然上述示例均使用了数据类型 int ,我们也可以使用变量来存储不仅仅是整数值的内容。

创建准确的 fractionOfHour 变量的一种更通用的方法是使用一种称为 double (双精度)的数据类型,该类型不仅能表示整数,还能表示分数。double 值实际上是数学意义上的 实数 的数字近似表示。您可以使用与其它类型相同的语法创建 double 变量并为其赋值:

double pi = 3.14159;

当一个或多个操作数为 double 值时,Java 执行“浮点除法”。因此我们可以解决上一节中遇到的问题:

double minutes = 59.0;
double fractionOfHour = minutes / 60;

现在, fractionOfHour 被设置为值 0.9833333333333333。

尽管浮点数很有用,但它们可能带来混淆。例如,Java 将整数值 1 与浮点数值 1.0 区分开来,尽管它们看起来是同一个数字。它们属于不同的数据类型,严格来说,不允许在不同类型之间进行赋值。

以下语句非法,因为被声明的变量是 int 类型,而右侧的初始值是 double 类型:

int x = 1.1; // compiler error

很容易忘记这条规则,因为在许多情况下 Java 会自动将一种类型转换为另一种类型:

double y = 1; // legal, but bad style

上述示例应属非法,但 Java 允许通过自动将 int 值 1 转换为 double 值 1.0 来处理。这种宽松性虽方便,却常给初学者带来困扰。例如:

double y = 1 / 3; // common mistake

你可能会期望变量 y 获得值 0.333333,这确实是 double 类型的一个合法值。但实际它获得的值是 0.0。右侧的表达式进行的是两个整数的除法,因此 Java 执行整数除法,结果为 int 值 0。该 int 值随后被隐式转换为 double ,以便用作 y 的初始值,因此赋给 y 的值为 0.0。

5.1.6. 使用布尔值作为变量

在前两个模块学习条件语句时,你可能记得频繁提到布尔值。与 int 和 double 一样, boolean 是 Java 提供的另一种数据类型。与 int 或 double 不同, boolean 值只能是 true 或 false 。

我们使用这些结构来处理 while 循环或 if 语句,例如:

if (this.hasFlower())
{
    this.hop();
}
  • 如果该 jeroo 拥有花朵,则它将跳跃。 hasFlower() 是一个方法,其返回值为 true 或 false ,具体取决于该 jeroo 是否拥有花朵。

我们可以将该布尔值存储在变量中,如下所示:

boolean pocketIsFull = this.hasFlower();

这里我们声明了一个布尔型变量,命名为 pocketIsFull ,并将其赋值为 hasFlower() 方法返回的结果。

如果我们愿意,就可以在条件语句中使用该变量:

boolean pocketIsFull = this.hasFlower();
if (pocketIsFull)
{
    this.hop();
}

这是写出相同 if 语句的另一种方式!

此外,我们可以将 boolean 变量赋值给复合条件表达式的计算结果:

boolean shouldToss = this.hasFlower() && this.seesNet(AHEAD);
if (shouldToss)
{
    this.toss();
}

之前我们声明并初始化了一个由数学表达式计算得到的 int 变量。这里的情况相同,Java 将会计算 this.hasFlower() && this.seesNet(AHEAD) ,该复合表达式将产生一个值,该值要么是 true ,要么是 false ,然后这个值将作为变量 shouldToss 的初始值使用。

最后,如果你只需要一个 boolean 值,可以使用关键字 true 或 false 来初始化一个 boolean 变量。

boolean x = true;
boolean y = false;

5.1.7. 局部变量的作用域

局部变量可以是非常有用的工具。你应该在 需要多次引用同一值时 使用局部变量,因为局部变量允许你为一个值命名,从而可以在多个位置使用该名称。如果你只需要一次使用某个值,命名可能并没有太大意义。

然而,局部变量用于表示仅在单个方法内(或甚至仅在方法内的少数几条语句中)使用的本地名称。编程语言使用术语 作用域 来指代名称可被使用的程序文本区域。对于局部变量而言,每个变量声明都出现在某对大括号( { } )之内,例如包围方法主体的那对大括号。这对大括号构成了程序语句块的起始与结束。该局部变量的作用域是其声明位置与包含该声明的语句块末尾的大括号( } )之间的所有语句。该名称在该块外部将不会被识别。最重要的是,这意味着该变量声明在其他方法中不可见(无法使用)。

public void doSomeStuff()
{
    int x = 17;  // declared here
    ...
    if (jeroo.getX() == 17)    // OK to use here, inside scope
    {
        ...
    }

    // scope of x ends at closing brace
}

public void doOtherStuff()
{
   if (x == 17)  // compiler error! x can't be referenced
   {
       ...
   }
}

上述代码中,局部变量 x 在方法 doSomeStuff() 的开始处声明,因此该变量可在该方法内的任何位置使用。然而, x 的作用域在 doSomeStuff() 结束时终止,即方法末尾的右大括号所标记的位置。

这就是为什么 局部变量 被称为 局部 的原因:它们只能在程序文本的一个小范围、局部区域内使用,仅限于下一个右大括号之前。作为进一步的例子:

public void doSomeStuff()
{
    ...
    if (jeroo.hasFlower())
    {
        int x = 17;  // declared here
        ...
        if (jeroo.getX() == 17)    // OK to use here, inside scope
        {
            ...
        }

        // scope of x ends at closing brace
    }

    x = 4;  // compiler error! x can't be referenced
}

局部变量可以在任意一对用于标记一系列语句的花括号内声明。然而,它们始终仅对该语句块 局部有效,不能在该语句块外部使用。此处, x 在第一个 if 语句的 true 分支内声明,因此 x 局限于该语句块内,无法在声明它的花括号对之外使用。

变量在作用域之外不可见,也无法使用。其作用域是指变量可见的文本区域。在 Java 中,作用域通常由一对花括号( { } )标记。

5.2. 字段与局部变量

编写面向对象程序在很大程度上是设计类,并在 Java 中为这些类编写定义。设计一个类是明确该类型对象所具有的所有属性和行为的过程。

到目前为止,我们一直使用属性和动作这两个术语来描述对象的特征。然而,在讨论编程语言时,更常见的做法是通过字段和方法来描述对象的特征。 字段 是在类开始处定义的一种变量,帮助我们描述类的特性。

例如,假设我们正在编写一个绘图程序。程序中需要的一种对象是矩形。一个 Rectangle 对象具有两个关键属性:其长度和宽度。在类内部,我们可以声明两个字段来存储这些属性的值:一个 length 和一个 width 。有了这两个字段,我们就可以定义矩形的典型操作,例如计算其面积以及绘制自身的能力。识别对象的字段与操作正是开发面向对象程序时所涉及的设计活动。

我们可以声明类 Rectangle 中的字段:

public class Rectangle
{
    private int length;
    private int width;
    private int xCoord;
    private int yCoord;

    // other parts of the class go here ...
}

我们的矩形有四个属性。前两个, length 和 width ,存储矩形的尺寸。后两个, xCoord 和 yCoord ,存储绘制矩形时的位置信息,这与我们确定岛屿上 jeroo 位置的方式类似。请注意,这些属性均未赋值。这是因为该类代表的是一般类型的矩形,而每个具体的矩形将为其属性赋予各自的值。声明仅指定了所有矩形共有的特征,而不代表任何一个特定的矩形。如同用于制作饼干的模具,类给出了对象的一般形状,而任何具体对象的特定值并不包含在内。

显著的是,这些字段也使用关键字 private ,表示它们只能在此类内部被访问和操作。按照惯例,我们 始终 包含关键字 private ,以确保只有类内部的代码才能访问或修改这些字段。这贯彻了一种软件工程设计原则,称为 信息隐藏,即仅由类自身管理其负责的信息,类外部的任何代码均不得干扰或更改此类内部信息。

一个字段在不同对象中将具有不同的值——也就是说,类的不同实例。例如,各个 Rectangle 实例的 length 、 width 、 xCoord 和 yCoord 字段将具有不同的值。例如,我们可以创建两个不同的 Rectangle 对象如下:

_images/RectangleObjects.png

要创建两个不同的 Rectangle 对象,我们需要一个构造函数。回想一下,构造函数的主要职责之一是初始化对象中存储的数据(即其字段中的数据)。由于我们的 Rectangle 类使用字段来存储属性值,因此构造函数需要初始化这些值。有时,你可能会将字段初始化为所有对象的固定值,但通常希望创建对象的代码能够指定初始值。如果此处我们希望如此,则需要一个 Rectangle 构造函数,该构造函数接受 4 个整数作为参数:

public class Rectangle
{
    private int length;
    private int width;
    private int xCoord;
    private int yCoord;

    public Rectangle(int myLength, int myWidth, int initialX, int initialY)
    {
        this.length = myLength;
        this.width = myWidth;
        this.xCoord = initialX;
        this.yCoord = initialY;
    }

    // other parts of the class go here ...
}

然后,如果我们想要调用这个构造函数,可以说:

Rectangle rectangle1 = new Rectangle(2, 5, 0, 0);

将 length 设为 2, width 设为 5,并将坐标设为 (0, 0)。

5.3. 修改私有变量:修改方法

回忆一下, Rectangle 的字段是私有的。我们可以在 Rectangle 类内部任何位置访问它们,但不能在类外部访问。

public class Runner
{
    public void myProgram()
    {
        Rectangle rectangle1 = new Rectangle(0, 0, 2, 5);
        int w = rectangle1.width;  // compiler error! Can't access field
    }
}

由于 length 是私有的,上述代码在编译时将导致错误。这是一种防止字段被意外修改或在不希望修改时修改的便捷方式,相当于强制的信息隐藏。信息隐藏有助于限制意外引入错误的可能性,也有助于在错误发生时缩小排查范围。此外,如果我们的 Rectangle 类将来发生变化,上述代码很可能也需要随之更改,这意味着该 Runner 类 依赖 于 Rectangle 类的内部细节。这会使未来的修改工作更加复杂。因此,信息隐藏 减少了引入错误的机会,也 减少了排查错误时需要检查的代码量,还 降低了未来修改的成本。这就是我们在此类中始终将字段声明为 private 的原因。最终,你将学习到关于字段的其他选项,但请记住,放宽信息隐藏会带来显著代价。

另一方面,这个 Runner 类可能需要访问并修改 Rectangle 类中的字段——例如,为了将其移动到新位置。为了避免直接访问字段,面向对象编程的一种惯例是,当符合类的设计需求时,提供公共方法来设置和获取对象某些属性的值。

获取或检索属性值的方法称为 访问器方法,或更通俗地称为 获取器 方法。将对象的属性设置为新值的方法称为 修改器方法,或更通俗地称为 设置器 方法。通常情况下,访问器和修改器方法会直接与对象的字段进行操作,如果属性是直接存储在字段中的。

由类的设计者根据该类所代表的抽象来决定哪些属性需要访问器方法和/或修改器方法。某些字段可能仅用于内部使用,不表示外部可见的属性。其他属性可能存储在字段中,但更适合只读使用,仅提供访问器方法。在其他情况下,类可能同时支持读取和修改属性,此时同时提供访问器和修改器方法是合适的。

如果你在设计一个 BankAccount 类,例如,你可能希望提供一个公共的 getAccountNumber() 方法,以便客户端能够获取其银行账户的信息,但你很可能不希望提供一个公共的 setAccountNumber() 来让客户端修改这些信息,或提供一个公共的 getAccountPassword() 用于应仅限内部使用的数据。

我们将在下面讨论访问器方法,但在那之前,先来看一下修改器方法。对于我们的 Rectangle 示例,假设我们希望其他类能够更改 xCoord 和 yCoord 属性,以移动 Rectangle 在屏幕上的位置。修改器方法可以简单如以下所示:

public class Rectangle
{
    // field declarations from above go here

    public void setXCoord(int newValue)
    {
        this.xCoord = newValue;
    }

    public void setYCoord(int newValue)
    {
        this.yCoord = newValue;
    }
}

此代码还展示了为直接存储在字段中的任何属性提供访问器方法的简单模式。该模式的关键要素是:

  • 变量修改方法是 public ,因为我们希望它可以在任何位置被访问。

  • 它的返回类型是 void ,因为此方法不会 return 任何内容。

  • 此方法通过在所更改属性名称前添加 set 作为前缀来命名,使用我们复合名称的大写规范。

  • 此方法接受一个与字段类型相同的参数。

  • 方法体使用赋值语句将字段设置为新值。

这种模式使用频率极高,几乎总是程序员在使用“设置方法”一词或说“为其添加一个设置方法”时所指的内容。

5.3.1. 作用域说明

变更方法的主体同时使用了字段和参数。需要注意的是,这两种变量的使用位置存在差异。请记住,名称或方法的 作用域 指的是它在程序中可以被使用的范围。

参数的作用域是其被声明的方法体。

例如考虑下面的 setLength() 方法:

public class Rectangle
{
    private int length;

    public void setLength(int len)
    {
        this.length = len;
    }
}

在此代码中,参数名 len 无法在 setLength() 方法之外被引用。它在包围方法体的花括号({ })之外完全不可见。

字段则在包围整个类的花括号( { } )内声明,因此其作用域为整个类——可以在类内的任何位置、任意方法中使用。例如,字段 length 是在 setLength() 方法之外声明的,可在类内的任何位置访问。

记住,声明在方法体内部的局部变量的作用域从其声明位置延伸到其声明所在代码块的末尾。

总而言之:

  • 字段在类内部声明,因此其作用域为整个类。可以在类中的任何方法内引用它们。

  • 参数在方法声明的参数列表中声明,因此其作用域为该整个方法。可以在该方法体内的任何位置引用它们,但无法在声明它们的方法外部访问。

  • 局部变量在由大括号包围的语句块( { } )中声明。可以从声明位置开始,到标记该代码块结束的大括号( } )为止,访问该块内的任何位置。

5.4. 访问方法

创建访问器方法时,如果对应的属性存储在字段中,同样遵循非常简单的模式,尽管有一个我们之前未涉及的细节。

public class Rectangle
{
    private int length;
    // other field declarations from above go here

    public int getLength()
    {
        return this.length;
    }
}

此代码展示了为直接存储在字段中的任何属性提供访问器方法的简单模式。该模式的关键要素是:

  • 访问器方法是 public ,因为我们希望它可以在任何位置被访问。

  • 在关键字 public 之后,我们不再列出 void ,而是列出字段中存储的数据类型,这与该方法将返回的数据类型相同。

  • 此方法通过在所访问属性名称前添加 get 作为前缀来命名,使用我们复合名称的大写约定。

  • 此方法不接受任何参数。

  • 方法体使用 return 语句返回字段的值。

这种模式被使用得如此频繁,以至于程序员在提到“getter方法”或说“为它添加一个getter方法”时,几乎总是指这种模式。

不同之处在于返回类型不再是 void ,而是新增的 return 键,我们将在下一节中讨论。目前最重要的是要注意,方法的返回类型必须与字段类型一致。因此,由于 length 被定义为 int ,此访问器方法的返回类型也必须是 int 。

5.5. 语法练习 5a:字段与访问器

5.6. 返回关键字

在返回类型为 void 的方法之外,你在 Java 中编写的其他所有方法都必须指定一个返回类型——即该方法将返回的 答案 或 结果 的类型。到目前为止,我们主要使用了 void 方法,这类方法不返回任何结果——实际上, void 表示“无”,我们调用这些方法是关注其执行的动作,而非它们计算出的值。

用于为我们计算值的方法在被调用时“返回”这些值。当我们说“返回一个值”时,意思是“给我们一个答案”或“生成一个结果”。与 void 方法相比,返回值的方法在两方面有所不同:

  • 它们声明了返回值的类型(返回类型)。

  • 它们至少使用一条 return 语句来提供一个 返回值。

例如,如果你要编写该方法:

public boolean isRaining()
{
    // no code yet
}

你将在编译时看到语法错误,提示“缺少返回语句”。这是因为关键字 public 之后使用了 boolean ,表示该方法 将 返回一个 boolean 类型的值,但方法体中并未说明具体返回什么值。要解决此问题,我们需要返回一个值。

public boolean isRaining()
{
    return false;
}

return 语句用于指定方法被调用时所生成的值。在关键字 return 之后,您可以使用任意表达式,包括常量(如 false 所示)、变量、字段、数学表达式,甚至其他方法调用的组合。

语句 return 中表达式的类型必须与方法声明的返回类型相匹配。当你声明返回类型为 double 时,你实际上是在承诺该方法最终将产生一个 double 值。如果你尝试使用无表达式,或类型不正确的表达式进行 return ,编译器将生成错误。

5.6.1. 回溯至访问方法

为字段编写访问方法必须具有返回类型,因为所有字段都有类型。

public class Rectangle
{
    private int length;

    public Rectangle()
    {
        this.length = 4;
    }

    public int getLength()
    {
        return this.length;
    }
}

在此情况下,我们的 length 字段类型为 int ,因此该字段的访问方法也需返回一个 int 。

5.6.2. 编写带返回值的更复杂方法

有时你可能会觉得有必要编写多个 return 语句,例如在条件语句的每个分支中各写一个:

public double absoluteValue(double x)
{
    if (x < 0)
    {
        return -x;
    }
    else
    {
        return x;
    }
}

由于这些 return 语句位于条件语句中,因此只会执行其中一个。一旦其中任意一个被执行,方法立即终止,后续的任何语句都不会再执行。当然,这意味着在到达 return 语句之后,方法的其他部分将不会被执行。此外,由于返回值的方法必须 始终 使用 return 语句,无论方法以何种方式结束,因此你必须在每个分支中都包含 return 语句,而不仅仅是在第一个分支中。

由于这些限制,一些初学者发现始终编写一个返回语句并将其放在方法的最后一条语句会更容易——这能确保方法总是返回一个值,并且不会意外跳过任何重要步骤。随着技能的发展,你可能会在某些情况下发现使用多个返回语句更方便,但请始终记住它们需要更多的注意,且更容易出错,因此当你遇到困难时,应退回到使用单一返回语句的方式:

public double absoluteValue(double x)
{
    double answer = x;
    if (x < 0)
    {
        answer = -x;
    }
    return answer;
}

以下是一个示例: calculateArea 接受一个 double 类型的参数,并返回以该半径对应的圆的面积:

public double calculateArea(double radius)
{
   // Note that Math.PI is a constant with the value of pi
   double result = Math.PI * radius * radius;
   return result;
}

这最后一行是一个返回语句。该语句表示:“立即从本方法中退出,并使用后面的表达式作为返回值。” 另外,请注意使用了来自内置 Java 工具类 Math 的常量 PI 。Java Math 类提供了许多有用且常见的数学函数,以及常量 PI 和 E 的定义。在 Java 中,按照惯例,程序员将常量名称全部用大写字母书写,以便与变量或字段区分开来。

您提供的条目中包含未解析的占位符 return ,根据规则需保留原样放置的特殊符号占位符,不得增删。修正后重译如下:

public double calculateArea(double radius)
{
   return Math.PI * radius * radius;
}

出现在 return 语句之后(在同一块内)或任何永远无法执行的位置的代码称为 死代码。如果代码的一部分是死代码,编译器会给出“不可达语句”的错误提示。例如,此方法包含死代码:

public double absoluteValue(double x)
{
   if (x < 0)
   {
      return -x;
   }
   else
   {
      return x;
   }
   x = 5;    // can never be executed
 }

那最后一行, x = 5 永远不会被执行,因为方法在到达该行之前总会返回一个值(结束方法执行)。

如果你将 return 语句放在条件语句内部,就必须确保程序的每条可能执行路径都最终会到达一个 return 语句。如果情况并非如此,编译器会提示你。例如,以下方法是不完整的:

public double absoluteValue(double x)
{
   if (x < 0)
   {
      return -x;
   }
   else if (x > 0)
   {
      return x;
   }
   // syntax error
}

当 x 为 0 时,两个条件都不成立,因此该方法在未遇到 return 语句的情况下结束。此情况下的错误信息可能类似于“缺少 return 语句”,这令人困惑,因为已经存在两条 return 语句了。但希望你能理解其含义。

同样地,如果你遇到困难,通常一个简单的解决方法是调整方法的结构,使得方法末尾仅有一个 return 语句。

5.6.3. 使用方法的结果

调用一个无返回值的方法时,调用语句通常单独占一行。例如,以下是 Jeroo 的一个简单跳跃并拾取方法。

public void hopAndPick()
{
    this.hop();
    this.pick();
}

这就是它的调用方式:

this.hopAndPick();

另一方面,当你调用一个具有返回类型的函数时,必须对返回值进行处理。我们通常将其赋值给变量,或作为表达式的一部分使用。

以这个方法为例:

public int square(int x)
{
    return x * x;
}

此方法接收某个数 x ,并返回该数的平方值。

我们可以随后调用该方法并实例化一个新变量以保存结果:

int base = 3;
int raised = square(base);

在此情况下, raised 被设置为值 9(即计算 3 * 3 的结果)。

此外,我们还可以将一个方法的结果作为另一个方法调用的参数:

int base = 3;
int raisedAgain = square(square(base));

这个新变量 raisedAgain 将被设为 81。对 square() 的调用按“由内而外”顺序求值,括号内的调用最先执行。因此,在第一次调用中, base 被作为参数传递给 square() ,返回值为 9。第一次调用的返回值 9 被用作第二次调用 square() 的参数值,该调用随后返回 9 * 9 = 81。

这可能会让你想起在代数课程中处理函数的情形。在那些课程中,我们可能会看到同样的概念被这样写出:

假设 f(x) = x * x,f(3) 的值是多少?f(f(3)) 的值是多少?

Java 使用了同样的原理,尽管涉及多种不同类型的数据,事情可能会变得稍微复杂一些。

5.7. 语法练习 5b:修改器与返回语句

5.8. 测试中使用字段

上周,我们致力于为 Jeroo 创建一些简单的测试套件。你可能还记得,每次测试开始时都需要进行大量的设置工作,以确保 Jeroo 所在的位置完全符合预期,并且拥有正确数量的花朵。

然而,如果你连续有多个测试都需要相同的初始条件呢?为了使测试过程更加高效,你可以声明 字段 而不是局部变量,以保存在大多数或全部测试中使用的公共对象。这样做后,无需在每个测试方法中重复声明它们为局部变量,而是将它们声明为字段,使其对 所有 测试方法都可见,从而只需声明一次即可。

此外,与其在每个测试方法中重复编写设置初始条件的代码,不如只写一次,而不是在每个测试方法中重复。

为了了解其工作原理,请考虑来自软件测试模块的这个示例测试方法:

public void testPickFlowers()
{
    // 1. set up initial conditions
    Lab04Island island = new Lab04Island();
    FlowerPicker picker = new FlowerPicker();
    island.addObject(picker, 1, 2);

    // 2. call the method
    picker.pickFlowers();

    // 3. check expected results
    assertThat(picker.getX()).isEqualTo(6);
    assertThat(picker.getY()).isEqualTo(2);
    assertThat(picker.getFlowers()).isEqualTo(5);
    assertThat(picker.getHeading()).isEqualTo(EAST);
}

现在假设你正在编写一个包含多个测试的测试类:

public class FlowerPickerTest
    extends TestCase
{
    public void testPickFlowers()
    {
        // 1. set up initial conditions
        Lab04Island island = new Lab04Island();
        FlowerPicker picker = new FlowerPicker();
        island.addObject(picker, 1, 2);

        ...
    }

    public void testPickFlowersAgain()
    {
        // 1. set up initial conditions
        Lab04Island island = new Lab04Island();
        FlowerPicker picker = new FlowerPicker();
        island.addObject(picker, 1, 2);

        // turn picker south and move forward 1
        // for this test

        ...
    }
}

此处,两个测试均使用了一个 island ,且均使用了一个 picker 。它们都必须创建 island 、创建 picker ,并将 picker 放置在 island 上。这仅仅是重复的代码。

重复的代码是 代码异味 的一个例子,也就是设计可以改进的警示信号。有时你查看代码时,会本能地觉得有更好的实现方式——这就是一种 代码异味。如果你有这种感觉,几乎可以肯定存在更好的实现方式,修改代码将使其得到改善。

首先,与其在每个方法中使用名为 island 和 picker 的局部变量,不如将它们移作字段:

public class FlowerPickerTest
    extends TestCase
{
    private Lab04Island island;
    private FlowerPicker picker;

    public void testPickFlowers()
    {
        // 1. No extra setup needed
        ...
    }

    public void testPickFlowersAgain()
    {
        // 1. set up initial conditions
        // turn picker south and move forward 1
        // for this test

        ...
    }
}

但是,我们该把创建对象的初始化代码放在哪里呢?虽然看起来构造函数似乎是个合适的位置,但构造函数仅在创建对象时执行一次。而这段初始化代码需要在类中的每个测试方法中都执行一次,而不仅仅是一次。

在测试类中,我们使用一种特殊方法 setUp() 实现此目的(注意大小写!)。该方法将在类中的每个测试执行前自动运行。明确地说,这意味着如果你有三个名为 test1() 、 test2() 和 test3() 的测试,那么 setUp() 方法会在 test1() 执行前运行,且在 test2() 执行前运行,也在 test3() 执行前运行。它为类中 所有 测试方法提供通用的初始化步骤。

我们可以使用此 setUp() 方法来高效地设置初始条件。程序员通常将一个类中所有测试共用的一组初始条件称为 测试夹具。

public class FlowerPickerTest
    extends TestCase
{
    private Lab04Island island;
    private FlowerPicker picker;

    public void setUp()
    {
        this.island = new Lab04Island();
        this.picker = new FlowerPicker();
        this.island.addObject(this.picker, 1, 2);
    }

    public void testPickFlowers()
    {
        // 1. No extra setup needed
        ...
    }

    public void testPickFlowersAgain()
    {
        // 1. set up initial conditions
        // turn picker south and move forward 1
        // for this test

        ...
    }
}

注意,这些字段的写法与原始的局部变量声明略有不同。它们在前面包含了关键字 private ,因为我们的所有字段都是 private 的。你永远不能在方法内部的局部变量前使用 private ,只能用于字段,但我们 始终 在字段前使用它。

另外,在 setUp() 内部可以看到,每一行开头都没有类型。我们并未在该方法内部 声明 局部变量。每当在名称前指定类型时,实际上就是在以某种方式声明一个新名称。

5.9. Programming Practice 5a

5.10. 编程实践 5b

   «  4. 软件测试基础与测试驱动开发   ::   目录   ::   6. 第 4 周  »

关闭窗口