Java中的多重继承
今天我们将探讨Java中的多重继承。前段时间,我在Java中写了几篇关于继承、接口和组合的文章。在这篇文章中,我们将探讨Java的多重继承,然后比较组合和继承。
Java中的多重继承
在Java中,多重继承是指创建一个具有多个超类的单一类的能力。与C++等一些其他流行的面向对象编程语言不同,Java在类中不支持多重继承。Java不支持类中的多继承,因为它会导致钻石问题,而且与其提供一些复杂的解决方法,我们可以通过其他更好的方式来实现与多继承相同的结果。
Java中的钻石问题
为了更容易理解钻石问题,让我们假设Java支持多继承。在这种情况下,我们可以有一个像下面图片所示的类层次结构。假设SuperClass是一个声明了一些方法的抽象类,而ClassA和ClassB是具体类。SuperClass.java
package com.Olivia.inheritance;
public abstract class SuperClass {
public abstract void doSomething();
}
ClassA.java (中文版本)
package com.Olivia.inheritance;
public class ClassA extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of A");
}
//ClassA own method
public void methodA(){
}
}
B类.java
package com.Olivia.inheritance;
public class ClassB extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of B");
}
//ClassB specific method
public void methodB(){
}
}
现在假设ClassC的实现方法如下,并且它同时扩展了ClassA和ClassB。ClassC.java
package com.Olivia.inheritance;
// this is just an assumption to explain the diamond problem
//this code won't compile
public class ClassC extends ClassA, ClassB{
public void test(){
//calling super class method
doSomething();
}
}
注意到test()方法正在调用父类doSomething()方法。这会导致模棱两可,因为编译器无法确定应该执行哪个父类方法。由于菱形类图的存在,这在Java中被称为钻石问题。Java中的钻石问题是Java不支持类的多重继承的主要原因。请注意,多重类继承的上述问题也可以出现在只有三个类的情况下,其中所有类至少有一个共同的方法。
Java接口中的多重继承
你可能已经注意到,我总是说类不支持多继承,但接口支持多继承。一个接口可以继承多个接口,下面是一个简单的例子。InterfaceA.java
package com.Olivia.inheritance;
public interface InterfaceA {
public void doSomething();
}
B接口
package com.Olivia.inheritance;
public interface InterfaceB {
public void doSomething();
}
请注意,这两个接口都声明了相同的方法,现在我们可以通过下面的方式将一个接口扩展为同时包含这两个接口。InterfaceC.java
package com.Olivia.inheritance;
public interface InterfaceC extends InterfaceA, InterfaceB {
//same method is declared in InterfaceA and InterfaceB both
public void doSomething();
}
这是完全可以的,因为接口只是声明方法,实际的实现将由实现接口的具体类进行。因此,在Java接口中不存在多重继承的任何歧义可能性。这就是为什么Java类可以实现多个接口的原因,就像下面的示例InterfacesImpl.java一样。
package com.Olivia.inheritance;
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();
//all the method calls below are going to same concrete implementation
objA.doSomething();
objB.doSomething();
objC.doSomething();
}
}
你有没有注意到,每次我重写超类方法或实现接口方法时,我都使用了@覆盖注释。覆盖注释是Java内置的三个注释之一,我们在重写任何方法时都应该使用覆盖注释。
救援的作文
如果我们想在ClassC中使用ClassA的方法methodA()和ClassB的方法methodB(),该怎么办?解决方案就在于使用组合。下面是一个使用组合来利用两个类的方法,并且使用其中一个对象的doSomething()方法的重构版ClassC。
package com.Olivia.inheritance;
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();
}
}
组合与继承
Java编程中最佳实践之一是“优先选择组合而非继承”。我们将探讨一些支持这种方法的方面。
-
- 假设我们有一个超类和子类如下所示:ClassC.java
-
- package com.Olivia.inheritance;
public class ClassC{
public void methodC(){
}
}
ClassD.java
package com.Olivia.inheritance;
public class ClassD extends ClassC{
public int test(){
return 0;
}
}
上面的代码编译并正常工作,但如果更改ClassC的实现如下所示,会发生什么情况:ClassC.java
package com.Olivia.inheritance;
public class ClassC{
public void methodC(){
}
public void test(){
}
}
请注意,test()方法在子类中已经存在,但返回类型不同。现在ClassD将无法编译,如果您使用任何IDE,它将建议您在超类或子类中更改返回类型。现在想象一下有多级类继承并且超类不受我们控制的情况。我们别无选择,只能更改子类方法的签名或名称以删除编译错误。此外,我们还必须在所有调用子类方法的地方进行更改,因此继承使我们的代码变得脆弱。这个问题在组合中永远不会发生,这使得组合比继承更可取。
继承的另一个问题是我们向客户端公开了所有超类方法,如果我们的超类设计不当且存在安全漏洞,那么即使我们在实现我们的类时采取了完全的预防措施,我们也会受到超类的糟糕实现的影响。组合帮助我们对超类的方法提供受控访问,而继承不提供对超类方法的任何控制,这也是组合优于继承的主要优势之一。
组合的另一个好处是它提供了方法调用的灵活性。我们上面的ClassC实现不是最优的,并且提供了与将要调用的方法的编译时绑定,通过最小的更改,我们可以使方法调用灵活并使其动态化。ClassC.java
package com.Olivia.inheritance;
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
继承中不存在方法调用的这种灵活性,这增加了有利于组合的最佳实践。
在组合中进行单元测试很容易,因为我们知道从超类中使用了哪些方法,并且我们可以为测试进行模拟,而在继承中,我们严重依赖超类,并且不知道将使用超类的哪些方法,因此我们需要测试所有超类的方法,这是额外的工作,我们需要做很多不必要的工作,因为继承。
关于Java的多重继承和组合的概述到此为止。