Java组合与继承

在本小节中,我们将学习java多重继承,比较组合和继承。

java中的多重继承是创建具有多个超类的单个类的功能。与其他一些流行的面向对象编程语言(如C++)不同,java不支持类中的多重继承。

Java不支持一个类中的多个继承,因为它可能产生一些问题(钻石问题),Java有更好的方法可以实现与多重继承相同的结果。

1. Java中的钻石问题

为了方便理解钻石问题,首先假设java中支持多个继承。在这种情况下,可以有一个类层次结构,如下图所示。

假设SuperClass是一个抽象类,声明了一些方法。而ClassAClassB是继承了SuperClass类的具体类。

文件:SuperClass.java

public abstract class SuperClass {

    public abstract void doSomething();
}

文件:ClassA.java

public class ClassA extends SuperClass{

    @Override
    public void doSomething(){
        System.out.println("doSomething implementation of A");
    }

    //ClassA 自己的方法
    public void methodA(){

    }
}

文件:ClassB.java

public class ClassB extends SuperClass{

    @Override
    public void doSomething(){
        System.out.println("doSomething implementation of B");
    }

    //ClassB 自己的方法
    public void methodB(){

    }
}

现在假设ClassC实现类似于下面的内容,它同时扩展了ClassAClassB,这里实例多重继承。

文件:ClassC.java

//  这只是为了解释钻石问题的假设
// 这段代码不能编译通过
public class ClassC extends ClassA, ClassB{

    public void test(){
        //调用父类的方法
        doSomething();
    }

}

请注意,test()方法中调用超类doSomething()方法。这就产生了歧义,因为编译器不知道要执行哪个超类方法。由于钻石类图,在java中称为钻石问题。Java中的钻石问题是java不支持类中多重继承的主要原因。

2. Java接口多重继承

前面我们说过类中不支持多继承,但是使用接口可以实现。单个接口可以扩展多个接口,下面是一个简单的例子。

文件:InterfaceA.java

public interface InterfaceA {

    public void doSomething();
}

文件:InterfaceB.java

public interface InterfaceB {

    public void doSomething();
}

请注意,两个接口都声明了相同的方法,现在可以使用一个接口来扩展这两个接口,如下所示。

文件:InterfaceC.java

public interface InterfaceC extends InterfaceA, InterfaceB {

    //same method is declared in InterfaceA and InterfaceB both
    public void doSomething();

}

这非常好,因为接口只声明方法,实际的实现将由实现接口的具体类完成。因此,Java接口中的多重继承中不存在任何歧义。

这就是java类可以实现多个接口的原因,如下例所示。

文件:InterfacesImpl.java

public class InterfacesImpl implements InterfaceA, InterfaceB, InterfaceC {

    @Override
    public void doSomething() {
        System.out.println("doSomething implementation of concrete class");
    }

    public static void main(String[] args) {
        InterfaceA objA = new InterfacesImpl();
        InterfaceB objB = new InterfacesImpl();
        InterfaceC objC = new InterfacesImpl();

        //下面的所有方法调用都将进行相同的具体实现
        objA.doSomething();
        objB.doSomething();
        objC.doSomething();
    }

}

是否注意到每次覆盖任何超类方法或实现任何接口方法时,都使用@Override注释。@Override注释是三个内置java注释之一,我们应该在覆盖任何方法时始终使用@Override注释。

2. Java组合

那么如果想在ClassC中使用ClassA函数methodA()ClassB函数methodB()方法,该怎么办? 可使用组合方案解决。下面是ClassC的重构版本,它使用组合来使用两个类中方法,并使用其中一个对象的doSomething()方法。

文件:ClassC.java

public class ClassC{

    ClassA objA = new ClassA();
    ClassB objB = new ClassB();

    public void test(){
        objA.doSomething();
    }

    public void methodA(){
        objA.methodA();
    }

    public void methodB(){
        objB.methodB();
    }
}

3. 组合与继承

Java编程的最佳实践之一是“赞成组合而不是继承”。下面将研究一些有利于这种方法的应用。

  1. 假设有一个超类和子类如下:
    文件:ClassC.java
public class ClassC{

    public void methodC(){
    }
}

文件:ClassD.java

public class ClassD extends ClassC{

    public int test(){
        return 0;
    }
}

上面的代码编译并且工作正常,但是如果ClassC实现如下改变了怎么办:

文件:ClassC.java

public class ClassC{

    public void methodC(){
    }

    public void test(){
    }
}

请注意,test()方法已存在于子类中,但返回类型不同。现在ClassD将无法编译,如果使用任何IDE,它将建议您更改超类或子类中的返回类型。

现在想象一下,有多级类继承和超类的情况不受控制。别无选择,只能更改子类方法签名或其名称以删除编译错误。此外,将不得不在调用子类方法的所有地方进行更改,因此继承会使代码变得脆弱。

上述问题永远不会出现在组合中,这使得它比继承更有利。

  1. 继承的另一个问题是将所有超类方法暴露给客户端,如果超类没有正确设计并且存在安全漏洞,那么即使完全注意实现类,也会受到糟糕实现的影响。
    组合有助于提供对超类方法的受控访问,而继承不提供对超类方法的任何控制,这也是组合优于继承的主要优点之一。

  2. 组合的另一个好处是它提供了调用方法的灵活性。上面的ClassC实现并不是最优的,它提供了与将被调用的方法的编译时绑定,只需极少的更改,就可以使方法调用灵活并使其动态化。

文件:ClassC.java

public class ClassC{

    SuperClass obj = null;

    public ClassC(SuperClass o){
        this.obj = o;
    }
    public void test(){
        obj.doSomething();
    }

    public static void main(String args[]){
        ClassC obj1 = new ClassC(new ClassA());
        ClassC obj2 = new ClassC(new ClassB());

        obj1.test();
        obj2.test();
    }
}

执行上面示例代码,得到以下结果 -

doSomething implementation of A
doSomething implementation of B

方法调用的这种灵活性在继承中不可用,并且提升了最佳实践以支持组合而不是继承。

  1. 单元测试中很容易组合,因为我们知道在超类中使用的所有方法,可以模拟它进行测试,而在继承中,很大程度上依赖于超类而不知道所有超类的方法将要使用,所以需要测试超类的所有方法。

上一篇: java中方法重载和方法重写的区别 下一篇:无