Java 中的多重继承
今天我们将研究 Java 中的多重继承。不久前,我写了几篇关于Java 中的继承、接口和组合的文章。在这篇文章中,我们将研究 Java 多重继承,然后比较组合和继承。
Java 中的多重继承
Java 中的多重继承是指创建具有多个超类的单个类的能力。与其他一些流行的面向对象编程语言(如 C++)不同,Java 不支持类中的多重继承。Java 不支持类中的多重继承,因为它会导致菱形问题,与其提供一些复杂的解决方法,不如通过更好的方法实现与多重继承相同的结果。
Java中的钻石问题
为了更容易理解钻石问题,我们假设 Java 支持多重继承。在这种情况下,我们可以有一个如下图所示的类层次结构。假设 SuperClass 是一个声明某些方法的抽象类,而 ClassA、ClassB 是具体类。SuperClass.java
package com.journaldev.inheritance;
public abstract class SuperClass {
public abstract void doSomething();
}
ClassA.java
package com.journaldev.inheritance;
public class ClassA extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of A");
}
//ClassA own method
public void methodA(){
}
}
ClassB.java
package com.journaldev.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.journaldev.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.journaldev.inheritance;
public interface InterfaceA {
public void doSomething();
}
InterfaceB.java
package com.journaldev.inheritance;
public interface InterfaceB {
public void doSomething();
}
请注意,两个接口都声明了相同的方法,现在我们可以有一个接口来扩展这两个接口,如下所示。InterfaceC.java
package com.journaldev.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.journaldev.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();
}
}
Did you noticed that every time I am overriding any superclass method or implementing any interface method, I am using @Override annotation. Override annotation is one of the three built-in java annotations and we should always use override annotation when overriding any method.
Composition for the rescue
So what to do if we want to utilize ClassA
function methodA()
and ClassB
function methodB()
in ClassC
. The solution lies in using composition. Here is a refactored version of ClassC that is using composition to utilize both classes methods and also using doSomething() method from one of the objects. ClassC.java
package com.journaldev.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();
}
}
Composition vs Inheritance
One of the best practices of Java programming is to “favor composition over inheritance”. We will look into some of the aspects favoring this approach.
-
Suppose we have a superclass and subclass as follows:
ClassC.java
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } }
ClassD.java
package com.journaldev.inheritance; public class ClassD extends ClassC{ public int test(){ return 0; } }
The above code compiles and works fine but what if ClassC implementation is changed like below:
ClassC.java
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } public void test(){ } }
Notice that
test()
method already exists in the subclass but the return type is different. Now the ClassD won’t compile and if you are using any IDE, it will suggest you change the return type in either superclass or subclass. Now imagine the situation where we have multiple levels of class inheritance and superclass is not controlled by us. We will have no choice but to change our subclass method signature or its name to remove the compilation error. Also, we will have to make a change in all the places where our subclass method was getting invoked, so inheritance makes our code fragile. The above problem will never occur with composition and that makes it more favorable over inheritance. -
Another problem with inheritance is that we are exposing all the superclass methods to the client and if our superclass is not properly designed and there are security holes, then even though we take complete care in implementing our class, we get affected by the poor implementation of the superclass. Composition helps us in providing controlled access to the superclass methods whereas inheritance doesn’t provide any control of the superclass methods, this is also one of the major advantages of composition over inheritance.
-
Another benefit with composition is that it provides flexibility in the invocation of methods. Our above implementation of
ClassC
is not optimal and provides compile-time binding with the method that will be invoked, with minimal change we can make the method invocation flexible and make it dynamic.ClassC.java
package com.journaldev.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(); } }
Output of above program is:
doSomething implementation of A doSomething implementation of B
This flexibility in method invocation is not available in inheritance and boosts the best practice to favor composition over inheritance.
-
单元测试在组合上很容易,因为我们知道从超类中使用了哪些方法,并且可以模拟测试,而在继承中,我们严重依赖超类,并且不知道将使用超类的所有方法,所以我们需要测试超类的所有方法,这是一项额外的工作,由于继承,我们需要不必要地这样做。
这就是有关 Java 中的多重继承以及组合的简要介绍。