在本小节中,我们将学习java多重继承,比较组合和继承。
java中的多重继承是创建具有多个超类的单个类的功能。与其他一些流行的面向对象编程语言(如C++)不同,java不支持类中的多重继承。
Java不支持一个类中的多个继承,因为它可能产生一些问题(钻石问题),Java有更好的方法可以实现与多重继承相同的结果。
1. Java中的钻石问题
为了方便理解钻石问题,首先假设java中支持多个继承。在这种情况下,可以有一个类层次结构,如下图所示。
假设SuperClass
是一个抽象类,声明了一些方法。而ClassA
,ClassB
是继承了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
实现类似于下面的内容,它同时扩展了ClassA
和ClassB
,这里实例多重继承。
文件: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编程的最佳实践之一是“赞成组合而不是继承”。下面将研究一些有利于这种方法的应用。
- 假设有一个超类和子类如下:
文件: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,它将建议您更改超类或子类中的返回类型。
现在想象一下,有多级类继承和超类的情况不受控制。别无选择,只能更改子类方法签名或其名称以删除编译错误。此外,将不得不在调用子类方法的所有地方进行更改,因此继承会使代码变得脆弱。
上述问题永远不会出现在组合中,这使得它比继承更有利。
继承的另一个问题是将所有超类方法暴露给客户端,如果超类没有正确设计并且存在安全漏洞,那么即使完全注意实现类,也会受到糟糕实现的影响。
组合有助于提供对超类方法的受控访问,而继承不提供对超类方法的任何控制,这也是组合优于继承的主要优点之一。组合的另一个好处是它提供了调用方法的灵活性。上面的
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
方法调用的这种灵活性在继承中不可用,并且提升了最佳实践以支持组合而不是继承。
- 单元测试中很容易组合,因为我们知道在超类中使用的所有方法,可以模拟它进行测试,而在继承中,很大程度上依赖于超类而不知道所有超类的方法将要使用,所以需要测试超类的所有方法。