MengBridgeCourse 中文版

第 3 章 变量与 for-each 循环

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

3.1. 变量、字段和参数

3.1.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 ,编译器使用它们来分析程序的结构。你可以在网上找到 完整关键字列表,但不必死记硬背。大多数编程编辑器提供“语法高亮”,使程序的不同部分以不同颜色显示。

3.1.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;

3.1.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。

3.1.1.3. 算术运算符

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

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

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

在此程序中, hour * 60 + minutes 是一个 表达式,表示要计算的单个值。运算符是 * 和 + ,分别表示乘法和除法。参与运算的对象称为 操作数 (这是对参数的花哨称呼)。当程序运行时,每个变量都会被其当前值替换,然后应用运算符。

因此,即使你编写了 hour * 60 + minutes ,Java 也会将其计算为 11 * 60 + 59 ,并将值 719 赋给 timeAsMinutes 。表达式通常是数字、变量和运算符的组合。在编译和执行时,它们会被求值以产生单个值。同样重要的是要注意,运算顺序在 Java 中与在数学中一样适用。由于乘法的优先级高于加法, 11 * 60 会在 59 相加之前先被计算。

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

3.1.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。结果再次向下取整,但至少现在是近似正确的。

3.1.1.5. 一种新类型:Double

虽然上述示例都使用了数据类型 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。

3.1.1.6. 使用布尔值作为变量

在处理前两个模块中的条件时,你可能记得经常听到关于布尔值的内容。就像 int 和 double 一样, boolean 是 Java 提供的另一种数据类型。与 int 或 double 不同, boolean 值只能是 true 或 false。

我们在使用 while 循环或 if 语句时会用到这些,例如:

if (this.hasFlower())
{
    this.hop();
}

在此代码中,如果 Jeroo 有花,它将跳跃。 hasFlower() 是一个方法,将根据 Jeroo 是否有花返回 true 或 false 。

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

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;

3.1.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 语句的真分支内声明,因此 x 局部于该语句块,不能在声明它的那对花括号之外使用。

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

3.1.2. 检查你的理解:变量

3.1.3. 字段与局部变量

编写面向对象程序主要是设计类并用 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)。

3.1.4. 修改私有变量:访问方法

回想一下, Rectangle 的字段是私有的。我们可以在 Rectangle 类内部 anywhere 访问它们,但不能在外部访问。

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 类中的字段——例如,将其移动到新位置。与其允许直接访问字段,面向对象编程的一种惯例是:当类的程序设计需要时,提供公共方法来设置和获取对象某些属性的值。

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

类的设计者需要根据该类所代表的抽象,确定哪些属性需要访问器和/或修改器方法。有些字段可能纯粹供内部使用,并不代表对外可见的属性。另一些属性可能存储在字段中,但更适合只读使用,此时只提供访问器方法。还有一些情况下,类可能同时支持读取和修改属性,访问器和修改器都适用。

例如,如果你正在设计一个 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 作为前缀,并遵循复合名称的大小写约定。

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

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

这种模式使用得非常频繁,以至于当程序员提到“设置方法”或说“为其添加一个设置方法”时,几乎总是指这种模式。

3.1.4.1. 关于作用域的说明

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

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

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

public class Rectangle
{
    private int length;

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

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

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

请记住,在方法主体内声明的局部变量,其作用域从声明点开始,延伸至其所在代码块的末尾。

总之:

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

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

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

3.1.5. 检查你的理解:作用域

3.1.6. 访问器方法

当相应属性存储在字段中时,创建访问器方法也遵循一个非常简单的模式,尽管有一个我们之前未曾涉及的变化。

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

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

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

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

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

  • 此方法的命名方式是将 get 作为前缀添加到它所访问的属性名称之前,并遵循复合名称的大小写约定。

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

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

这种模式使用得非常频繁,以至于当程序员提到“获取方法”或说“为其添加一个获取方法”时,几乎总是指这种模式。

不同的是返回类型,而非 void 以及这个新的 return 关键字,我们将在下一节讨论它。目前最重要的是注意,方法的返回类型 必须 与字段的类型匹配。因此,由于 length 被定义为 int ,此 getter 方法的返回类型也必须是 int 。

3.1.7. 检查你的理解:字段、获取器和设置器

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

3.1.9. return 关键字

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

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

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

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

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

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

编译时你会看到语法错误,提示"missing return statement"。这是因为在关键字 public 之后使用 boolean 指定了该方法 将 返回一个 boolean 结果,但方法体并未说明产生什么值。要修复此问题,我们需要返回某个值。

public boolean isRaining()
{
    return false;
}

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

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

3.1.9.1. 返回访问器方法

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

public class Rectangle
{
    private int length;

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

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

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

3.1.9.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 语句。

3.1.9.3. 使用方法的结果

当你调用一个 void 方法时,该调用通常单独占一行。例如,这里是一个用于 Jeroos 的简单 hop 和 pick 方法。

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 也使用这一原则,但由于存在许多不同的数据类型,情况可能会变得稍微复杂一些。

3.1.10. 检查你的理解:带类型的方法与返回语句

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

3.1.12. 在测试中使用字段

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

然而,如果你有一系列测试都需要相同的初始条件该怎么办?为了使测试更加 streamlined,你可以声明 字段 而不是局部变量来保存你在大多数或所有测试中使用的公共对象。当你这样做时,与其在每个测试方法中反复将它们声明为局部变量,不如将它们声明为字段,这样它们对所有 测试方法 都可见,因此只需声明一次。

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

要了解其工作原理,请考虑软件测试模块中的此示例测试方法:

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() 的特殊方法(注意大小写!)。该方法会在该类中的每个测试之前自动执行。明确地说,这意味着如果您有 3 个名为 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 ,只能对字段使用,但我们对字段 总是 使用它。

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

3.1.13. 编程练习 5a

3.1.14. 编程练习 5b

   «  2.4. 单元 2 实验 2 占位符   ::   目录   ::   3.2. 单元 3 实验 1 占位符  »

关闭窗口