一、在组合与继承之间选择
组合和继承都允许在新的类中放置子对象,组合是显式地这样做,而继承则是隐式地做。你或许想知道二者之间的区别何在,以及怎样在二者之间做出选择。
组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即,在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非所嵌入对象的接口。为取得此效果,需要在新类中嵌入一个现有类的private对象。
有时,允许类的用户直接访问新类中的组合成分是极具意义的;也就是说,将成员对象声明为public。如果成员对象自身都隐藏了具体实现,那么这种做法是安全的。当用户能够了解到你正在组装一组部件时,会使得端口更加易于理解。car对象即为一个很好的例子:
/**
* 引擎类
*/
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();
public 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);
}
}
由于在这个例子中car的组合也是问题分析的一部分(而不仅仅是底层设计的一部分),所以使成员称为public将有助于客户端 程序员了解怎样去使用类,而且也降低了类开发者所面临的代码复杂度。但务必要记得这仅仅是一个特例,一般情况下应该使域成为private。
在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常,这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。略微思考一下就会发现,用一个“交通工具”对象来构成一部“车子”是毫无意义的,因为“车子”并不包含“交通工具”,它仅是一种交通工具(is-a关系)。“is-a”(是一个)的关系是用继承来表达的,而“has-a”(有一个)的关系则是用组合来表达的。
二、protected关键字
现在,我们已介绍完了继承,关键字protected最终具有了意义。在理想世界中,仅靠关键字private就已经足够了。但在实际项目中,经常会想要将某些事物尽可能对这个世界隐藏起来,但仍然允许导出类的成员访问它们。
关键字protected就是起这个作用。它指明“就类用户而言,这是private的,但对于任何继承于此类的导出类或其他任何位于同一个包内的类来说,它却是可以访问的。”(protected也提供了包内访问权限。)
尽管可以创建protected域,但是最好的方式还是将域保持为private;你应当一直保留“更改底层实现”的权利。然后通过protected方法来控制类的继承者的访问权限。
/**
* 罪犯类
*/
class Villain {
private String name;
protected void set(String nm) {
name = nm;
}
public Villain(String name) {
this.name = name;
}
public String toString() {
return "我是一个罪犯,我的名字是:" + name;
}
}
/**
* *类
*/
public class Orc extends Villain {
private int orcNumber;
public Orc(String name, int orcNumber) {
super(name);
this.orcNumber = orcNumber;
}
public void change(String name, int orcNumber) {
set(name);
this.orcNumber = orcNumber;
}
public String toString() {
return "Orc " + orcNumber + ":" + super.toString();
}
public static void main(String[] args) {
Orc orc = new Orc("Limburger", 12);
System.out.println(orc);
orc.change("Bob", 19);
System.out.println(orc);
}
}
可以发现,change()可以访问set(),这是因为它是protected的。还应注意Orc的toString()方法的定义方式,它依据toString()的基类版本而定义。
三、向上转型
“为新的类提供方法”并不是继承技术中最重要的方面,其最重要的方面是用来表现新类和基类之间的关系。这种关系可以用“新类是现有类的一种类型”这句话加以概括。
这个描述并非只是一种解释继承的华丽的方式,这直接是由语言所支撑的。例如,假设有一个称为Instrument的代表乐器的基类和一个称为Wind的导出类。由于继承可以确保基类中所有的方法在导出类中也同样有效,所以能够向基类发送的所有信息同样也可以向导出类发送。如果Instrument类具有一个play()方法,那么Wind乐器也将同样具备。这意味着我们可以准确地说Wind对象也是一种类型的Instrument。下面这个例子说明了编译器是怎样支持这一概念的:
class Instrument {
public void play() {
}
static void tune(Instrument i) {
i.play();
}
}
public class Wind extends Instrument {
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute);
}
}
在此例中,tune()方法可以接受Instrument引用,这是在是太有趣了。但在Wind的main()中,传递给tune()方法的是一个Wind引用。鉴于java对类型检查十分严格,接受某种类型的方法同样可以接受另外一种类型就会显得很奇怪,除非你认识到Wind对象同样也是一种Instrument对象,而且也不存在任何tune()方法是可以通过Instrument来调用的,同时又不存在于Wind之中。在tune()中,程序代码可以对Instrument和它所有的导出类起作用,这种将Wind引用转换为Instrument引用的动作,我们称之为向上转型。
四、为什么称为向上转型
该术语使用有其历史原因,并且是以传统的类继承图的绘制方法为基础的:将根置于页面的顶端,然后逐渐向下。于是,Wind.java的继承图就是:
由于导出类转型成为基类,在继承图上是向上移动的,因此一般称为向上转型。由于向上转型是从一个较专用类型向较通用类型转换,所以总是很安全的。也就是说,导出类是基类的一个超集。它可能比基类含有更多的方法,但它必须至少具备基类中所含有的方法。向上转型过程中,类接口中唯一可能发生的事情就是丢失方法,而不是获取它们。这就是为什么编译器在“未曾明确表示转型”或“未曾指定特殊标记”的情况下,仍然允许向上转型的原因。
五、组合与继承总结
在面向对象编程中,生成和使用程序代码最有可能采用的方法就是直接将数据和方法包装进一个类中,并使用该类的对象。也可以运用组合技术使用现有类来开发新的类;而继承技术其实不太常用。因此,尽管在教授OOP过程中我们多次强调继承,但这并不意味着要尽可能使用它。相反,应当慎用这一技术,其使用场合仅限于你确信使用该技术确实有效的情况。到底是该用组合还是用继承,一个最清晰的判断方法就是问一问自己是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的;如果不需要,则应当好好考虑自己是否需要继承。
游王子 发布了77 篇原创文章 · 获赞 2 · 访问量 1万+ 私信 关注