一、5.1.3 利用导入改变行为
Java 已取消的一种特性是C 的“条件编译”,它允许我们改变参数,获得不同的行为,同时不改变其他任何代码。Java 之所以抛弃了这一特性,可能是由于该特性经常在 C 里用于解决跨平台问题:代码的不同部分根据具体的平台进行编译,否则不能在特定的平台上运行。由于 Java 的设计思想是成为一种自动跨平台的语言,所以这种特性是没有必要的。
然而,条件编译还有另一些非常有价值的用途。一种很常见的用途就是调试代码。调试特性可在开发过程中使用,但在发行的产品中却无此功能。Alen Holub(www.holub.com)提出了利用包(package)来模仿条件编译的概念。根据这一概念,它创建了C“断定机制”一个非常有用的 Java 版本。之所以叫作“断定机
129
制”,是由于我们可以说“它应该为真”或者“它应该为假”。如果语句不同意你的断定,就可以发现相关的情况。这种工具在调试过程中是特别有用的。
可用下面这个类进行程序调试:
//: Assert.java
// Assertion tool for debugging
package com.bruceeckel.tools.debug;
public class Assert {
private static void perr(String msg) {
System.err.println(msg);
}
public final static void is_true(boolean exp) {
if(!exp) perr("Assertion failed");
}
public final static void is_false(boolean exp){
if(exp) perr("Assertion failed");
}
public final static void
is_true(boolean exp, String msg) {
if(!exp) perr("Assertion failed: " + msg);
}
public final static void
is_false(boolean exp, String msg) {
if(exp) perr("Assertion failed: " + msg);
}
} ///:~
这个类只是简单地封装了布尔测试。如果失败,就显示出出错消息。在第 9 章,大家还会学习一个更高级的错误控制工具,名为“违例控制”。但在目前这种情况下,perr()方法已经可以很好地工作。
如果想使用这个类,可在自己的程序中加入下面这一行:
import com.bruceeckel.tools.debug.*;
如欲清除断定机制,以便自己能发行最终的代码,我们创建了第二个 Assert 类,但却是在一个不同的包里://: Assert.java
// Turning off the assertion output
// so you can ship the program.
package com.bruceeckel.tools;
public class Assert {
public final static void is_true(boolean exp){}
public final static void is_false(boolean exp){}
public final static void
is_true(boolean exp, String msg) {}
public final static void
is_false(boolean exp, String msg) {}
} ///:~
现在,假如将前一个import 语句变成下面这个样子:
import com.bruceeckel.tools.*;
程序便不再显示出断言。下面是个例子:
//: TestAssert.java
// Demonstrating the assertion tool
package c05;
// Comment the following, and uncomment the
// subsequent line to change assertion behavior:
import com.bruceeckel.tools.debug.*;
// import com.bruceeckel.tools.*;
public class TestAssert {
public static void main(String[] args) {
Assert.is_true((2 + 2) == 5);
Assert.is_false((1 + 1) == 2);
Assert.is_true((2 + 2) == 5, "2 + 2 == 5");
Assert.is_false((1 + 1) == 2, "1 +1 != 2");
}
} ///:~
通过改变导入的package,我们可将自己的代码从调试版本变成最终的发行版本。这种技术可应用于任何种类的条件代码。
-
取消条件编译的原因:Java取消了类似C语言中的条件编译特性,这是因为Java的设计目标之一是实现自动跨平台,而条件编译通常是为了解决跨平台问题而存在的。由于Java本身就具有跨平台的特性,因此条件编译在Java中变得没有必要。
-
利用包模拟条件编译:虽然Java取消了条件编译,但是可以利用包来模拟类似的功能。作者提出了一个叫做"断言机制"的工具,通过创建不同的包来实现调试和最终发布版本之间的切换。具体地,作者创建了两个不同的Assert类,一个用于调试版本,一个用于最终发布版本,它们分别位于不同的包中。
-
断言机制的实现:作者提供的Assert类封装了一些布尔测试方法,用于在代码中插入断言。如果断言失败,会输出相应的错误消息。通过在代码中导入不同的Assert类,可以选择性地启用或禁用断言功能,从而实现调试版本和最终发布版本之间的切换。
-
实例演示:最后,作者提供了一个示例程序TestAssert,展示了如何在代码中使用Assert类进行断言。通过切换导入的包,可以将程序从调试版本转换为最终发布版本。
综合来看,这段话介绍了如何利用Java的包来模拟条件编译的功能,以实现在调试和最终发布版本之间的切换。这种方法可以帮助开发人员在开发过程中轻松地进行调试,同时在发布时去除调试信息,使得代码更加干净和高效。
二、5.1.4 包的停用
大家应注意这样一个问题:每次创建一个包后,都在为包取名时间接地指定了一个目录结构。这个包必须存在(驻留)于由它的名字规定的目录内。而且这个目录必须能从CLASSPATH 开始搜索并发现。最开始的时候,package 关键字的运用可能会令人迷惑,因为除非坚持遵守根据目录路径指定包名的规则,否则就会在运行期获得大量莫名其妙的消息,指出找不到一个特定的类——即使那个类明明就在相同的目录中。若得到象这样的一条消息,请试着将 package 语句作为注释标记出去。如果这样做行得通,就可知道问题到底出在哪儿。
-
包的命名与目录结构的关系:在Java中,包是用来组织类的命名空间的一种机制。每次创建一个包时,都会为这个包指定一个目录结构。这意味着包名决定了类文件存放的目录位置,而这个目录必须在CLASSPATH中能够被搜索到。
-
包名与目录结构必须对应:为了确保Java能正确地找到类文件,包名与目录结构必须保持一致。如果不遵循这个规则,就会在运行时出现类找不到的错误。这种错误可能会让人感到困惑,因为类明明存在于相同的目录中,但却找不到。
-
遇到问题时的解决方法:如果遇到类找不到的错误,可以尝试将包语句(即以
package
关键字开头的语句)注释掉。如果注释掉之后程序能够正常运行,那么问题可能就出在包的命名或目录结构上。
综合起来,这段话提醒程序员在使用Java包时要注意包名与目录结构的一致性,以避免在运行时出现类找不到的问题。如果出现这种问题,可以尝试注释掉包语句来排除问题。
三、到底选择合成还是继承
无论合成还是继承,都允许我们将子对象置于自己的新类中。大家或许会奇怪两者间的差异,以及到底该如何选择。
如果想利用新类内部一个现有类的特性,而不想使用它的接口,通常应选择合成。也就是说,我们可嵌入一个对象,使自己能用它实现新类的特性。但新类的用户会看到我们已定义的接口,而不是来自嵌入对象的接口。考虑到这种效果,我们需在新类里嵌入现有类的private 对象。
有些时候,我们想让类用户直接访问新类的合成。也就是说,需要将成员对象的属性变为public。成员对象会将自身隐藏起来,所以这是一种安全的做法。而且在用户知道我们准备合成一系列组件时,接口就更容易理解。car(汽车)对象便是一个很好的例子:
//: Car.java
// Composition with public objects
class Engine {
public void start() {}
public void rev() {}
public void stop() {}
}
class Wheel {
public void inflate(int psi) {}
}
class Window {
public void rollup() {}
public void rolldown() {}
}
class Door {
public Window window = new Window();
public void open() {}
public void close() {}
}
public class Car {
public Engine engine = new Engine();
public Wheel[] wheel = new Wheel[4];
public Door left = new Door(),
right = new Door(); // 2-door
Car() {
for(int i = 0; i < 4; i++)
wheel[i] = new Wheel();
}
public static void main(String[] args) {
Car car = new Car();
car.left.window.rollup();
car.wheel[0].inflate(72);
}
} ///:~
由于汽车的装配是故障分析时需要考虑的一项因素(并非只是基础设计简单的一部分),所以有助于客户程序员理解如何使用类,而且类创建者的编程复杂程度也会大幅度降低。
如选择继承,就需要取得一个现成的类,并制作它的一个特殊版本。通常,这意味着我们准备使用一个常规用途的类,并根据特定的需求对其进行定制。只需稍加想象,就知道自己不能用一个车辆对象来合成一辆汽车——汽车并不“包含”车辆;相反,它“属于”车辆的一种类别。“属于”关系是用继承来表达的,而“包含”关系是用合成来表达的。
这段话主要讨论了在Java中使用合成(Composition)和继承(Inheritance)的区别以及如何选择合适的方法。让我解释一下:
1. **合成与继承的区别**:
- 合成指的是在一个新类中包含其他类的实例作为其成员。这样做的好处是可以利用现有类的功能,但不继承其接口,从而避免暴露不必要的接口给外部用户。合成通常通过将对象私有化来实现。
- 继承则是创建一个新类,基于现有类的定义,并且继承其接口和行为。通过继承,子类可以重用父类的功能,并且可以添加新的功能或修改现有功能。
2. **合成的使用场景**:
- 当想要利用现有类的特性,但不想继承其接口时,通常应选择合成。这种方式允许将现有类的对象嵌入到新类中,并且可以在新类中定义自己的接口。这样做有助于提高代码的复用性和安全性,同时也降低了编程的复杂度。
- 汽车的例子展示了合成的使用,通过将引擎、轮子、门等对象嵌入到汽车类中,实现了汽车的功能,但隐藏了这些对象的具体接口,使得汽车类的接口更加清晰。
3. **继承的使用场景**:
- 当需要基于现有类创建一个特殊版本,或者需要扩展现有类的功能时,通常应选择继承。继承允许子类直接使用父类的接口和功能,并且可以在子类中添加新的功能或修改现有功能。
- 比如,如果需要创建一种特殊类型的汽车,可以通过继承汽车类并添加新的功能来实现。
4. **结论**:
- 合成与继承各有其适用的场景,需要根据具体的需求和设计来选择合适的方式。
- 合成适用于希望利用现有类的功能,但不想继承其接口的情况;而继承适用于需要创建特殊版本或扩展现有类功能的情况。
综合来看,这段话强调了合成和继承的区别以及如何根据需求选择合适的方法,以实现代码的复用和扩展。