6. 第 4 周¶
本周,我们将回顾一些以前见过的主题。
6.1. NamedJeroo 类(改编自 Think Java 的 Time 类)¶
定义一个类的一个常见原因,是把相关的数据封装在一个可作为一个整体处理的对象中。 这样一来,我们就可以把对象用作参数和返回值,而不是传递和返回多个值。 这种设计原则被称为 数据封装 (data encapsulation)。
例如,在本模块中,我们将实现一个 TrackingJeroo 类,它表示一只跟踪自己采了多少朵花、
破坏了多少张网、跳了多少次的 Jeroo。因为每个 TrackingJeroo 对象都包含这些数据,
所以我们定义属性来存放它们。
6.1.1. 实例变量¶
属性也被称为 实例变量 (instance variables),因为每个实例都有自己专属的变量。
实例变量在类定义的开头声明,位于任何方法之外。单独来看,这段代码片段就是一个合法的类定义:
public class TrackingJeroo {
private int numFlowersPicked;
private int numNetsDisabled;
private int numHops;
}
TrackingJeroo 类是 public 的,这意味着它可以在其他类中使用。但是实例变量是 private 的,
这意味着它们只能从 TrackingJeroo 类的内部访问。如果试图从另一个类中读取或写入它们,就会得到一个编译错误。
私有实例变量有助于让各个类彼此隔离,这样对一个类的修改就不会要求修改其他类。 它还简化了其他程序员为了使用你的类而需要理解的内容。这种隔离被称为 信息隐藏 (information hiding)。
6.1.2. 构造函数¶
声明完实例变量之后,下一步是定义 构造函数 (constructor),它是一个用于初始化实例变量的特殊方法。
下面是 TrackingJeroo 类的一个构造函数示例:
public TrackingJeroo(){
this.numFlowersPicked = 0;
this.numNetsDisabled = 0;
this.numHops = 0;
}
这个构造函数不接收任何参数。每一行都把一个实例变量初始化为 0。
名称 this 是一个关键字,指代我们正在创建的对象。你可以像使用任何其他对象的名称一样使用 this。例如,你可以读取和写入 this 的实例变量,也可以将 this 作为参数传递给其他方法。但你不能声明 this,也不能对它进行赋值。
要创建一个 TrackingJeroo 对象,和 Jeroo 类一样,你必须使用 new 关键字:
TrackingJeroo tom = new TrackingJeroo();
当你调用 new 时,Java 会创建对象并调用你的构造函数来初始化实例变量。构造函数完成后,
new 会返回对新对象的引用。在这个例子中,这个引用被赋给了变量 tom 。
初学者有时会犯在构造函数内部调用 new 的错误。你没有必要这样做,也不应该这样做。 在这个例子中,在构造函数里调用 new TrackingJeroo() 会导致错误:
public TrackingJeroo() {
new TrackingJeroo(); // wrong!
this.numFlowersPicked = 0;
this.numNetsDisabled = 0;
this.numHops = 0;
}
和其他方法一样,构造函数可以重载(overload),也就是说你可以编写多个参数不同的构造函数。 Java 通过把你提供的参数与构造函数的参数进行匹配,来确定要调用哪个构造函数。
有时编写一个不带参数的构造函数(比如上面那个)会很有用,再写一个像这样的构造函数:
public TrackingJeroo(int flowers, int nets, int hops) {
this.numFlowersPicked = flowers;
this.numNetsDisabled = nets;
this.numHops = hops;
}
这个构造函数所做的只是把参数的值复制到实例变量中。 如果我们
要调用这个构造函数,就必须在 new 运算符之后提供参数。
这个例子创建了一个已经跳了 7 次、采了 3 朵花的 TrackingJeroo :
Time time = new Time(3, 0, 7);
编写多个构造函数提供了灵活性:既可以先创建对象再填充属性,也可以在创建对象本身之前收集全部信息。
一旦熟练了,编写构造函数就会变得很乏味。你只需看一眼实例变量列表就能很快把它们写出来。
把所有内容结合起来,这是目前为止完整的类定义:
public class TrackingJeroo {
private int numFlowersPicked;
private int numNetsDisabled;
public int numHops;
public TrackingJeroo(){
this.numFlowersPicked = 0;
this.numNetsDisabled = 0;
this.numHops = 0;
}
public TrackingJeroo(int flowers, int nets, int hops) {
this.numFlowersPicked = flowers;
this.numNetsDisabled = nets;
this.numHops = hops;
}
}
6.2. 方法¶
方法 (method)对应一个动作或一种行为,是一段命名的代码,可以被调用或 唤起 (invoke)来执行一组预先定义的动作。
方法定义由两部分组成:方法头和方法体。一般来说,方法头采用下面的形式,其中某些部分是可选的:
修饰符 可选 返回类型 方法名 ( 参数列表 可选)
把各部分组合起来,一个方法定义看起来可能像这样:
public int addHops()
上面,这个方法以访问修饰符 public 开头,用来声明这个方法可以被其他类访问或引用。
方法头的下一部分是方法的返回类型。这是方法返回的值(如果有的话)的类型。
在上面的代码中,我们指定期望返回一个 int 。到目前为止,当我们编写方法时,我们这样写:
public void pickFlowersAndDisableNets()
这里我们看到的不是 int,而是关键字 void ,它指定我们不会返回任何东西。我们稍后会更多地讨论返回类型。
方法的名字跟在方法的返回类型之后。这是调用方法时使用的名字。我们可以随便叫它什么,但不能包含空格。
方法名之后是方法的 参数列表 (parameter list),我们将在下一节讨论。
6.3. 使用参数传递信息(来自 ThinkJava)¶
我们使用过的一些方法需要参数(arguments),即你在调用方法时提供的值。例如,一只 Jeroo 有两个名为 'hop' 的方法。
调用 hop() 会让 Jeroo 向前跳一格。但是,如果你在括号里指定一个数字,比如 hop(4) ,Jeroo 就会向前跳四格。
使用方法时,你提供参数。当 编写 方法时,你要为参数命名。参数列表说明需要哪些参数。
例如:
public void turnAndDisable(RelativeDirection direction) {
this.turn(direction);
this.toss();
}
要调用这个方法,我们必须提供一个相对方向作为参数:
RelativeDirection dir = RIGHT;
turnAndDisable(dir);
这会让 Jeroo 向右转,并破坏一张网。
6.3.1. 使用多个参数¶
下面是接收两个参数的方法示例:
public void turnThenHop(RelativeDirection direction, int hops) {
this.turn(direction);
this.hop(numHops);
}
要调用这个方法,我们必须提供一个整数和一个相对方向作为参数:
int number = 7;
RelativeDirection dir = RIGHT;
turnThenHop(number, dir);
这会让 Jeroo 向右转,然后跳七次。
6.4. 编写条件语句的良好习惯¶
和加注释一样,可读性是编写条件语句时的重要因素。
6.4.1. 逻辑非与 if-else 结构¶
有一点需要考虑:在编写 if/else 语句时,以 ! 开头通常会让代码更难阅读。
if(!this.isClear(AHEAD))
{
this.toss();
}
else
{
this.hop();
}
上面的 ! 很容易被忽略,从而导致对这个条件语句的误读。相反,更好的做法是像这样改写同一个条件:
if(this.isClear(AHEAD))
{
this.hop();
}
else
{
this.toss();
}
可以看到,从逻辑上讲,这两个 if-then-else 结构达到了同样的效果,但其中一个更容易阅读。
备注
请记住,你不一定总能不使用 ! 运算符就写出正确的条件。特别是当你有 else 子句时,
你也许仍然要用到它,但只要有可能,尽可能回避它仍然是一个好习惯。
6.4.2. 过多的条件语句¶
另一件要记住的事情是不要编写过多的条件。在解决复杂问题时,很容易忍不住为遇到的每一种新情况不断添加新条件。 然而,这既更难阅读,也可能在你的代码中引入以后很难发现的 bug。
例如:
if(this.isClear(AHEAD))
{
this.hop();
}
else if (!this.isClear(AHEAD))
{
this.toss();
}
else
{
this.turn(RIGHT);
}
从逻辑上讲,这个条件语句的 if 分支和 else-if 分支与我们上面看到的效果相同。然而,这里的第三个分支永远不会执行。
这是因为 aaron 前方的区域要么是空的,要么不是空的。代码总能找到一个要执行的分支,也总会跳过 else 分支。
如果你不完全确定两个布尔语句是否等价,写出真值表会很有帮助。例如,我们在下面可以看到,编写 b 和 !!b 是等价的。
|
|
|
|---|---|---|
True |
False |
True |
False |
True |
False |
无论 b 的值是什么,我们都可以看到 !!b 与它匹配!
6.4.3. 空条件分支¶
不在代码中留下空条件也是一个好习惯。
if(this.isClear(AHEAD))
{
// do nothing
}
else
{
this.turn(RIGHT);
}
人们总是更喜欢只用一个 if 语句,而不是一个空的 if-else。
if(this.isClear(AHEAD))
{
// do nothing
}
else
{
this.turn(RIGHT);
}
这里,比起空条件,更偏好使用 ! 运算符:
if(!this.isClear(AHEAD))
{
this.turn(RIGHT);
}
6.4.4. 多个条件与复合条件¶
看一下下面这段代码片段:
if(this.isClear(AHEAD))
{
if(this.seesNet(RIGHT)){
this.turn(RIGHT);
}
}
这里我们看到一个条件嵌套在另一个条件中。通常,更好的做法是把同样的条件改写成这样:
if(this.isClear(AHEAD) && this.seesNet(RIGHT))
{
this.turn(RIGHT);
}
6.5. 更复杂的条件语句¶
有时你想检查相关的条件,并从几个动作中选择一个。一种方法是使用我们的级联 if 结构:
if(molly.isFacing(NORTH))
{
molly.hop();
}
else if (molly.isFacing(SOUTH))
{
molly.hop(2);
}
else if (molly.isFacing(EAST))
{
molly.hop(3);
}
else
{
molly.hop();
molly.toss();
}
这些链可以任意长,尽管如果长得过分会难以阅读。
你还可以把一个条件语句嵌套在另一个条件语句中,从而做出复杂的决策。我们可以把前面的例子写成:
if(molly.isFacing(WEST))
{
molly.hop();
molly.toss();
}
else{
if (molly.isFacing(NORTH)){
molly.hop();
}
else if (molly.isFacing(SOUTH))
{
molly.hop(2);
}
else{
molly.hop(3);
}
}
外部条件语句有两个分支。第一个分支让 Jeroo 跳一次再投掷,第二个分支包含另一个条件语句,它自己有三个分支。
这类结构很常见,但很快就会变得难以阅读。良好的缩进对于让读者看清结构(或预期的结构)至关重要。
6.6. 一种不同类型的复杂 if 语句¶
if 语句变得复杂的另一种方式,是创建更长的复合条件。
例如,
if((caroline.isFacing(NORTH) && caroline.hasFlower()) || caroline.seesNet(AHEAD))
这条语句可以概括为 if(A || B) ,其中:
A = caroline.isFacing(NORTH) && caroline.hasFlower()B = caroline.seesNet(AHEAD)
如果 Jeroo 面朝北方时持有花朵,或者看到前方有网,此 if 语句将触发。值得注意的是,如果 Jeroo 仅持有花朵,逻辑 AND
将强制语句 caroline.isFacing(NORTH) && caroline.hasFlower() 为假。因此,Jeroo 必须看到前方有网,此 if 语句才会触发。
逻辑非也可以否定一个复合语句。
if(!(caroline.isFacing(NORTH) && caroline.hasFlower()))
记住,要得到 caroline.isFacing(NORTH) && caroline.hasFlower() 为 true,Jeroo 必须有花并且面向北方。
只要括号内的复合条件为 false,编写
!(caroline.isFacing(NORTH) && caroline.hasFlower()) 就会为 true。
看这类复杂运算时,很容易被搞混。在考虑取反的复合条件时,按照 德摩根定律 (De Morgan's laws)改写它们对你可能有帮助:
!(A && B)与!A || !B相同!(A || B)与!A && !B相同
利用这一点,我们不写
if(!(caroline.isFacing(NORTH) && caroline.hasFlower()))
而是写一个逻辑上等价的式子:
if(!caroline.isFacing(NORTH) || !caroline.hasFlower())
同样,如果我们使用真值表,可以看到这两列是匹配的:
|
|
|
|
|
|
|
|---|---|---|---|---|---|---|
True |
True |
True |
False |
False |
False |
False |
True |
False |
False |
True |
False |
True |
True |
False |
True |
False |
True |
True |
False |
True |
False |
False |
False |
True |
True |
True |
True |
6.7. 短路求值(Think Java)¶
布尔运算符的另一个重要特性是它们使用一种称为短路求值的求值形式。在 短路求值 (short-circuit evaluation)中, 布尔表达式从左到右求值,一旦表达式的值可以确定,求值就立即停止,无论其中是否还包含其他运算符和操作数。例如,在表达式
basil.isFacing(WEST) && basil.seesNet(AHEAD)
中,如果 basil.isFacing(WEST) 为 false,那么 AND 表达式必定为 false,所以 Java 不会求值 basil.seesNet(AHEAD)。
类似地,在表达式
basil.isFacing(NORTH) || basil.seesNet(AHEAD)
中,如果 basil.isFacing(WEST) 为 true,那么 Java 不会求值 basil.seesNet(AHEAD) ,因为我们知道 OR 表达式已经是 true。
6.8. 语法回顾¶
6.9. 语法练习¶
CodeWorkout 练习
6.10. changePointeeDataDirect¶
莫莉正在练习添加一道 CodeWorkout 风格的题目(还在进行中)
