James Gosling在1995年2月发表的题为“ Java:概述”的白皮书中提出了一个关于Java中为什么不支持多重继承的想法。
JAVA省略了许多C ++很少使用,理解不清,令人困惑的功能,以我们的经验,这些功能带来的痛苦多于好处。这主要包括运算符重载(尽管它确实有方法重载),多重继承和广泛的自动强制。
谁能比詹姆斯·高斯林博士更好呢?本段为我们提供了概述,他谈到了不支持多重继承的主题。
Java不支持多重继承
首先让我们牢记这一点。无论Java是否支持多重继承,这本身都是一个讨论点。有人说,它支持使用接口。不。在Java中不支持多重继承。如果您不相信我的话,请再次阅读以上段落,这些都是Java之父的话。
这个使用接口支持多重继承的故事是我们开发人员编写的。接口提供了比具体类更大的灵活性,并且我们可以选择使用单个类来实现多个接口。根据协议,这是我们遵循的两个创建类的蓝图。
这正试图接近多重继承。我们要做的是实现多个接口,这里我们不扩展(继承)任何东西。实现类是将添加属性和行为的类。它不能使实现脱离父类。我会简单地说,在Java中不支持多重继承。
多重继承
多重继承是我们将多个类的属性和行为继承到单个类的地方。C ++,Common Lisp,是一些支持多重继承的流行语言。
为什么Java不支持多重继承?
现在我们确定在Java中不支持多重继承。但为什么?这是java的创建者做出的设计决定。关键字是简单性和罕见的用法。
简单
我想分享James Gosling给出的Java定义。
JAVA:一种简单的,面向对象的,分布式的,解释的,健壮的,安全的,与体系结构无关的,可移植的,高性能,多线程,动态语言。
看看这个定义对于Java的美丽。这应该是现代软件语言的定义。语言定义的第一个特点是什么?很简单。
为了实现简单性,应该是省略多重继承的主要原因。例如,我们可以考虑多重继承的钻石问题。
我们有两个类B和C从A继承。假设B和C覆盖了继承的方法,并且它们提供了自己的实现。现在D继承了B和C的多重继承。D应该继承该覆盖方法,将使用哪个覆盖方法?是来自B还是来自C?在这里,我们有歧义。
在C ++中,尽管它提供了替代方法来解决此问题,但仍有可能进入该陷阱。在Java中,由于没有多重继承,因此永远不会发生。即使两个接口将具有相同的方法,实现类也将仅具有一个方法,并且实现者也将完成该方法。动态加载类使实现多重继承变得困难。
很少使用
我们使用Java已有很长时间了。由于缺少对Java多重继承的支持,我们面对过几次被困和面对困境的情况?以我的亲身经历,我甚至不记得一次。由于很少需要,考虑到实现的复杂性,可以安全地省略多重继承。这不值得麻烦,而是选择了简单的方法。
即使需要,也可以用替代设计代替。因此,有可能没有多重继承而没有任何问题地生活,这也是原因之一。
我对此的看法是,在Java中省略对多重继承的支持并不是一个缺陷,并且对实现者有好处。
C++解决多继承歧义的方法
多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个。
多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承,如下图所示:
图1:菱形继承
类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A-->B-->D 这条路径,另一份来自 A-->C-->D 这条路径。
在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B-->D 这条路径,还是来自 A-->C-->D 这条路径。下面是菱形继承的具体实现:
- //间接基类A
- class A{
- protected:
- int m_a;
- };
- //直接基类B
- class B: public A{
- protected:
- int m_b;
- };
- //直接基类C
- class C: public A{
- protected:
- int m_c;
- };
- //派生类D
- class D: public B, public C{
- public:
- void seta(int a){ m_a = a; } //命名冲突
- void setb(int b){ m_b = b; } //正确
- void setc(int c){ m_c = c; } //正确
- void setd(int d){ m_d = d; } //正确
- private:
- int m_d;
- };
- int main(){
- D d;
- return 0;
- }
这段代码实现了上图所示的菱形继承,第 25 行代码试图直接访问成员变量 m_a,结果发生了错误,因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。
为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:
- void seta(int a){ B::m_a = a; }
这样表示使用 B 类的 m_a。当然也可以使用 C 类的:
- void seta(int a){ C::m_a = a; }
虚继承(Virtual Inheritance)
为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
在继承方式前面加上 virtual 关键字就是虚继承,请看下面的例子:
- //间接基类A
- class A{
- protected:
- int m_a;
- };
- //直接基类B
- class B: virtual public A{ //虚继承
- protected:
- int m_b;
- };
- //直接基类C
- class C: virtual public A{ //虚继承
- protected:
- int m_c;
- };
- //派生类D
- class D: public B, public C{
- public:
- void seta(int a){ m_a = a; } //正确
- void setb(int b){ m_b = b; } //正确
- void setc(int c){ m_c = c; } //正确
- void setd(int d){ m_d = d; } //正确
- private:
- int m_d;
- };
- int main(){
- D d;
- return 0;
- }
这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
现在让我们重新梳理一下本例的继承关系,如下图所示:
图2:使用虚继承解决菱形继承中的命名冲突问题
观察这个新的继承体系,我们会发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。在上图中,当定义 D 类时才出现了对虚派生的需求,但是如果 B 类和 C 类不是从 A 类虚派生得到的,那么 D 类还是会保留 A 类的两份成员。
换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。
在实际开发中,位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。通常情况下,使用虚继承的类层次是由一个人或者一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个类是虚基类,况且新类的开发者也无法改变已经存在的类体系。
C++标准库中的 iostream 类就是一个虚继承的实际应用案例。iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。
图3:虚继承在C++标准库中的实际应用
虚基类成员的可见性
因为在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,那么仍然可以直接访问这个被覆盖的成员。但是如果该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪个类。
以图2中的菱形继承为例,假设 A 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:
- 如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 A 的成员,此时不存在二义性。
- 如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高。
- 如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。
可以看到,使用多继承经常会出现二义性问题,必须十分小心。上面的例子是简单的,如果继承的层次再多一些,关系更复杂一些,程序员就很容易陷人迷魂阵,程序的编写、调试和维护工作都会变得更加困难,因此我不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。也正是由于这个原因,C++ 之后的很多面向对象的编程语言,例如 Java、C#、PHP 等,都不支持多继承。
来源:Why Multiple Inheritance is Not Supported in Java
来源:C++虚继承和虚基类详解