在JAVA中,组合与继承都允许在新的类中放置子对象,不同的是,组合是显示的这样做,而继承是隐式的这样做。那么在实际的编程中,我们到底该如何在二者当中进行选择来使我们的程序更符合实际的效果呢?
通常来讲,组合会应用在想在新类中使用现有类的功能而并非它的接口的情形之下。也就是说,在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是新类中定义的接口而并非所嵌入对象的接口,为实现此效果,需要在新类中嵌入现有类的private对象。
但是某些时候,允许类的用户直接访问新类中的对象和组合成分也是很有必要的,那么这个时候可以将成员对象声明为public,如果成员对象自身都隐藏了具体实现的方法,那么毫无疑问,这种做法是十分安全的。当用户了解到我们正在组装一组部件时,会使得端口更易于理解。下面的代码将很好的解释端口:
package access;
import java.util.*;
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 ecgine = 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) {
// TODO Auto-generated method stub
Car car = new Car();
car.left.window.rollup();
car.wheel[0].inflate(72);
}
}
在这个例子当中,Car的组合也是问题分析的一部分,不仅仅是底层设计的一部分,所以成员均为public将有助于客户端程序员了解如何使用类,并且同时降低了类开发者所面临的代码复杂度,但这只是一个特例,一般情况下,都应该使域成为private。
在继承的时候,使用某个现有类,并开发一个它的特殊版本,通常这意味着我们在使用一个通用类,并未某种特殊需要而将其特殊化。
举个例子来说,用一个“交通工具”对象来构成一部“车子”是毫无意义的,因为“车子”并不包含“交通工具”,它仅仅是一种交通工具(is-a关系)。
“is-a”关系是用继承来表达的,而“has-a”关系是用组合来表达的。在实际的使用过程中应该多注意二者的区别已及关系,将二者巧妙的结合起来会有助于我们的开发工作。
在面向对象编程的过程中,生成和使用程序代码最有可能采用的方法为直接将数据和方法包装到一个类中,并使用该类的对象。也可以运用组合技术使用现有的类进行开发新的类,而继承技术实际上是不太常用的,对于继承,我们应当慎重使用,其使用场合仅限于我们确信使用该技术确实有效的情况下。到底该使用继承还是组合,一个最清晰的判断方法为问一问自己是否需要从新类向基类进行向上转型,如果必须进行向上转型,那么继承的使用是很有必要的;但如果不需要,那么需要仔细考虑是否该使用继承。
下面给出一个结合使用继承与组合的例子:
package access;
import java.util.*;
class Plate{
Plate(int i){
System.out.println("Plate constructor");
}
}
class DinnerPlate extends Plate{
DinnerPlate(int i){
super(i);
System.out.println("DinnerPlate constructor");
}
}
class Utensil{
Utensil(int i){
System.out.println("Utensil constructor");
}
}
class Spoon extends Utensil{
Spoon(int i){
super(i);
System.out.println("Utensil constructor");
}
}
class Fork extends Utensil{
Fork(int i){
super(i);
System.out.println("Fork constructor");
}
}
class Knife extends Utensil{
Knife(int i){
super(i);
System.out.println("Knife constructor");
}
}
class Custom{
Custom(int i){
System.out.println("Custom constructor");
}
}
public class PlaceSetting extends Custom{
private Spoon sp;
private Fork frk;
private Knife kn;
private DinnerPlate pl;
public PlaceSetting(int i){
super(i + 1);
sp = new Spoon(i + 2);
frk = new Fork(i + 3);
kn = new Knife(i + 4);
pl = new DinnerPlate(i + 5);
System.out.println("PlaceSetting constructor");
}
public static void main(String[] args) {
// TODO Auto-generated method stub
PlaceSetting x = new PlaceSetting(9);
}
}
此程序的运行结果为:
虽然编译器在强制我们进行初始化基类操作,并要求我们在构建器起始处就要这样做,但编译器并不监督我们必须将成员对象也进行初始化操作。
另外,初始化时是从基类到导出类逐步进行,但清除时(C++中的析构)是从导出类到基类逐步进行。下面给出一个清除的例子:
package access;
import java.util.*;
class Shape{
Shape(int i){
System.out.println("Shape constructor");
}
void dispose(){
System.out.println("Shape dispose");
}
}
class Circle extends Shape{
Circle(int i){
super(i);
System.out.println("Drawing Circle");
}
void dispose(){
System.out.println("Drawing Circle");
super.dispose();
}
}
class Triangle extends Shape{
Triangle(int i){
super(i);
System.out.println("Drawing Triangle");
}
void dispose(){
System.out.println("Erasing Triangle");
super.dispose();
}
}
class Line extends Shape{
private int start,end;
Line(int start,int end){
super(start);
this.start = start;
this.end = end;
System.out.println("Drawing line: "+ start +","+end);
}
void dispose(){
System.out.println("Erasing Line: "+ start +","+end);
super.dispose();
}
}
public class CADSystem extends Shape{
private Circle c;
private Triangle t;
private Line[] lines = new Line[3];
public CADSystem(int i){
super(i+1);
for(int j = 0;j < lines.length;j++)
lines[j] = new Line(j,j*j);
c = new Circle(1);
t = new Triangle(1);
System.out.println("Combined constructor");
}
public void dispose(){
System.out.println("CADSystem.dispose()");
t.dispose();
c.dispose();
for(int i = lines.length -1;i >=0;i--)
lines[i].dispose();
super.dispose();
}
public static void main(String[] args) {
// TODO Auto-generated method stub
CADSystem x = new CADSystem(47);
try{
}finally{
x.dispose();
}
}
}
此程序运行结果为:
Java中没有C++中析构函数的概念,原因是在JAVA中,我们习惯于只是忘掉而并非销毁对象,并且让垃圾回收器在必要时帮助我们自动释放内存。通常这样做是好事,但有时需要在类的生命周期内执行一些必要的清理工作,正如以前博客中提到的我们并不知道垃圾回收器会何时调用或是否被调用,所以必须显示编写一些方法做这些事情。另外在程序的结尾处运用了“try-finally”,以便程序无论在何时都会调用finally中的清除语句。并始终牢记,最好编写自己的清理方法,不要使用finalize()方法。有关finalize(),之前的博客中有提到过。
当我们使用现有类来建立新的类时,如果首先考虑继承技术,反倒会加重我们的负担,使事情变得复杂起来,更好的方式是选择组合,因为组合不会强制我们进入继承的层次结构当中,组合更加灵活,可以动态选择类型,而继承则需要在编译时明确知道类型,如下代码很好的说明了这一点:
package access;
class Actor{
public void act(){}
}
class HappyActor extends Actor{
public void act(){
System.out.println("HappyActor");
}
}
class SadActor extends Actor{
public void act(){
System.out.println("SadActor");
}
}
class Stage{
private Actor actor = new HappyActor();
public void change(){
actor = new SadActor();
}
public void performPlay(){
actor.act();
}
}
public class Transmogrify {
public static void main(String[] args) {
// TODO Auto-generated method stub
Stage stage = new Stage();
stage.performPlay();
stage.change();
stage.performPlay();
}
}
此程序的运行结果为:
上述例子当中,Stage对象包含一个对Actor的引用,而Actor被初始化为HappyActor,意味着performPlay方法会产生某种特殊行为,而引用在运行时可以与另一个不同的对象重新动态绑定起来,那么SadActor对象的引用可以在actor中被替代,然后performPlay方法产生的行为也会随之改变,这样在运行时就获得了动态灵活性。
一条通用的准则是:用继承来表达行为间的差异,并用字段表达状态上的变化。在上述例子中,两者均使用到:通过继承得到两个不同的类,用于表达act方法的差异;而Stage通过运用组合使自己的状态发生变化,状态的改变引发行为的改变。