文章目录
- 一、为什么Java不支持多重继承?
- 1.1 Java不支持多重继承
- 1.2 为什么Java不支持多重继承?
- 1.2.1 简单
- 1.2.2 很少使用
- 二、如何给女朋友解释为什么Java不支持多继承?
- 2.1 继承
- 2.2 多继承
- 2.3 Java不支持多继承
- 2.4 Java 8支持多继承
- 三、实现多重继承
- 3.1 接口
- 3.2 内部类
- 四、知乎帖子
- 4.1-RednaxelaFX
- 4.2-CharlieW
- 4.3-解牛
- 最近在学习NDK和JNI相关,讲到了C++多继承问题,想的Java是不支持多继承的,就顺带看看这些资料,做个记录。
- 做股票软件,画K线图的时候看源码,又发现了他们定义的接口extends了多个父类……
public interface IKLine extends ICandle, IMACD, IKDJ, IRSI, IVolume, IWR {
}
一、为什么Java不支持多重继承?
James Gosling在1995年2月发表了一篇名为”Java概览”的Java白皮书,文章解释了Java不支持多重继承的原因。
Java去除了一些C++中很少用、而且被经常误解而错用的功能,如操作符的重载(operator overloading)(尽管Java仍旧保留方法的重载),多重继承(multiple inheritance),以及广泛的自动强迫同型(extensive automatic coercions)。
没有谁比James Gosling更有资格来谈论这个这个话题了。这篇文章将为你介绍他对于Java不支持多重继承的看法。
1.1 Java不支持多重继承
首先我们要搞清楚,Java是否支持多重继承。有的人会说,Java中因为一个类可以实现多个接口,所以支持多重继承。错!Java不支持多重继承。如果你不相信我说的,那么再回到上面,重新读读Java之父的原话吧。
对于Java通过接口来实现多重继承的这个错误观点是有的程序员胡编乱造的。接口相对于具体的类赋予了我们更多的灵活性。我们可以一个类实现多个接口。这是约定俗成的,我们可以用两个’框架蓝图’来创造一个类。
我们做的只是实现多重接口,这里我们不是扩展(继续)什么类,而是实现一个类,在其中添加一些属性和行为。这不是从父类中直接获得一些行为和属性。在这里,我再次强调,Java不支持多重继承。
多重继承是一个子类从多个父类中继承属性和方法。C++, Common Lisp是时下支持多重继承的流行语言。
1.2 为什么Java不支持多重继承?
现在我们清楚了Java不支持多重继承了,那么Java为什么要这么做呢?这是Java创造者们的决定,最好的理由是因为简单,以及我们很少会用到它。
1.2.1 简单
我想在这里分享一下James Gosling对于Java的定义。
Java: 一种简单的,面向对象的,分布式的,解释型的(译者注:Java既不是纯解释型也不是纯编译型的语言),健壮的,安全的,架构中立的,可移植的,高性能的,支持多线程的,动态语言。
看看定义的美妙之处吧。现代程序语言应该有这样的特性。我们看到,定义第一个特性是什么?是简单。
为了强化简单这个特点,这就是我们去除多重继承的原因。下面来看个例子,多重继承的菱形继承问题。
有两个类B和C继承自A。假设B和C都继承了A的方法并且进行了覆盖,编写了自己的实现。假设D通过多重继承继承了B和C,那么D应该继承B和C的重载方法,那么它应该继承哪个的呢?是B的还是C的呢?
C++中经常会掉入这个陷阱,虽然它也提出了替代的方法来解决这个问题。我们在Java中就不会出现这个问题。就算两个接口拥有同样的方法,实现的类只会有一个方法,这个方法由实现的类编写。动态的加载类会让多重继承的实现变得困难。
1.2.2 很少使用
我们使用Java已经很长时间了,我们有多少次因为缺少多重继承而面临困难呢?我个人的经验是一次都没有。因为多重继承很少有机会被用到,所以更安全的做法是去掉它而保持简单性。
就算是碰到需要多重继承的情景,我们也可以找到替代方法。
我的观点是,去掉对多重继承的支持不是Java的缺陷,对开发者来说是件好事。
二、如何给女朋友解释为什么Java不支持多继承?
要提到多继承,首先要从继承开始说起。
2.1 继承
面向对象的编程语言有三个重要的基本特性:封装、继承和多态。而很多人认为继承是Java面向对象编程技术的一块基石。
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法;或子类从父类继承方法,使得子类具有父类相同的行为。
Java继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。
加入,我们已经定义了一个Car类,这个Car中包含了轮胎、发动机、底盘、方向盘等属性,还具有行走、加油、开窗等行为。而如果我们想要定义一辆Bus,想要复用这些属性和行为,就可以通过继承来实现。通过使用继承,我们使得Bus类和Car类之间存在了一定的关系,而我们通常称呼Car是Bus的父类,Bus是Car的子类。在Java中,使用extends关键字来实现继承。
如上面Car与Bus,当写继承语句时,class Bus extends Car{ }其中Bus类是子类,Car类是父类。
2.2 多继承
上面我们提到的Bus和Car之间的关系其实是一种单继承,指的是一个类只继承自一个父类。在软件开发中,还有一种多继承(多重继承)的情况,顾名思义,就是一个类同时继承自多个父类。比如维基百科中关于多继承举了一个例子:
例如,可以创造一个“哺乳类动物”类别,拥有进食、繁殖等的功能;然后定义一个子类型“猫”,它可以从父类继承上述功能,不需重新编写程序,同时增加属于自己的新功能,例如“追赶老鼠”。
但是,"猫"还可以作为"宠物"的子类,拥有一些宠物独有的能力。
作为面向对象语言,C++是支持多重继承的。
但是,多年以来,多重继承一直都是一个敏感的话题,反对者指它增加了程序的复杂性与含糊性。
2.3 Java不支持多继承
很多人知道,Java是不支持多重继承的,这里要提一下,这里的继承特指的是使用extends关键字的这种继承行为。
那么为什么Java不支持多重继承呢?
关于这个问题,Java的创始人James Gosling曾经回答过,他表示: “Java之所以不支持一个类继承多个类,主要是因为在设计之初我们听取了来自C++和Objective-C登阵营的人的意见。因为多继承会产生很多歧义问题。”
Gosling老人家提到的歧义问题,其实是C++因为支持多继承之后带来的菱形继承问题。
假设我们有类B和类C,它们都继承了相同的类A。另外我们还有类D,类D通过多重继承机制继承了类B和类C。
这时候,因为D同时继承了B和C,并且B和C又同时继承了A,那么,D中就会因为多重继承,继承到两份来自A中的属性和方法。在使用D的时候,如果想要调用一个定义在A中的方法时,就会出现歧义。因为这样的继承关系的形状类似于菱形,因此这个问题被形象地称为菱形继承问题。
而C++为了解决菱形继承问题,又引入了虚继承 。
因为支持多继承,引入了菱形继承问题,又因为要解决菱形继承问题,引入了虚继承。而经过分析,人们发现我们其实真正想要使用多继承的情况并不多。
所以,在 Java 中,不允许“实现多继承”,即一个类不允许继承多个父类。但是 Java 允许“声明多继承”,即一个类可以实现多个接口,一个接口也可以继承多个父接口。由于接口只允许有方法声明而不允许有方法实现(Java 8之前),这就避免了 C++ 中多继承的歧义问题。
2.4 Java 8支持多继承
Java不支持多继承,但是是支持多实现的,也就是说,同一个类可以同时实现多个类。
我们知道,在Java 8以前,接口中是不能有方法的实现的。所以一个类同时实现多个接口的话,也不会出现C++中的歧义问题。因为所有方法都没有方法体,真正的实现还是在子类中的。
那么问题来了。
Java 8中支持了默认函数(default method ),即接口中可以定义一个有方法体的方法了。
public interface Pet {
public default void eat(){
System.out.println("Pet Is Eating");
}
}
而又因为Java支持同时实现多个接口,这就相当于通过implements就可以从多个接口中继承到多个方法了,这不就是变相支持了多继承么。
那么,Java是怎么解决菱形继承问题的呢?我们再定义一个哺乳动物接口,也定义一个eat方法。
public interface Mammal {
public default void eat(){
System.out.println("Mammal Is Eating");
}
}
然后定义一个Cat,让他分别实现两个接口:
public class Cat implements Pet,Mammal {
}
这时候,编译期会报错:
error: class Cat inherits unrelated defaults for eat() from types Mammal and Pet
这时候,就要求Cat类中,必须重写eat()方法。
public class Cat implements Pet,Mammal {
@Override
public void eat() {
System.out.println("Cat Is Eating");
}
}
所以可以看到,Java并没有帮我们解决多继承的歧义问题,而是把这个问题留给开发人员,通过重写方法的方式自己解决。
三、实现多重继承
多重继承指的是一个类可以同时从多于一个的父类那里继承行为和特征,然而我们知道Java为了保证数据安全,它只允许单继承。有些时候我们会认为如果系统中需要使用多重继承往往都是糟糕的设计,这个时候我们往往需要思考的不是怎么使用多重继承,而是您的设计是否存在问题。但有时候我们确实是需要实现多重继承,而且现实生活中也真正地存在这样的情况,比如遗传:我们即继承了父亲的行为和特征也继承了母亲的行为和特征。可幸的是Java是非常和善和理解我们的,它提供了两种方式让我们曲折来实现多重继承:接口和内部类。
3.1 接口
在介绍接口和抽象类的时候了解到子类只能继承一个父类,也就是说只能存在单一继承,但是却可以实现多个接口,这就为我们实现多重继承做了铺垫。
对于接口而已,有时候它所表现的不仅仅只是一个更纯粹的抽象类,接口是没有任何具体实现的,也就是说,没有任何与接口相关的存储,因此也就无法阻止多个接口的组合了。
interface CanFight {
void fight();
}
interface CanSwim {
void swim();
}
interface CanFly {
void fly();
}
public class ActionCharacter {
public void fight(){
}
}
public class Hero extends ActionCharacter implements CanFight,CanFly,CanSwim{
public void fly() {
}
public void swim() {
}
/**
* 对于fight()方法,继承父类的,所以不需要显示声明
*/
}
3.2 内部类
上面使用接口实现多重继承是一种比较可行和普遍的方式,在介绍内部类的时候谈到内部类使的多继承的实现变得更加完美了,同时也明确了如果父类为抽象类或者具体类,那么我就仅能通过内部类来实现多重继承了。如何利用内部类实现多重继承,请看下面实例:儿子是如何利用多重继承来继承父亲和母亲的优良基因。
首先是父亲Father和母亲Mother:
public class Father {
public int strong(){
return 9;
}
}
public class Mother {
public int kind(){
return 8;
}
}
重头戏在这里,儿子类Son:
public class Son {
/**
* 内部类继承Father类
*/
class Father_1 extends Father{
public int strong(){
return super.strong() + 1;
}
}
class Mother_1 extends Mother{
public int kind(){
return super.kind() - 2;
}
}
public int getStrong(){
return new Father_1().strong();
}
public int getKind(){
return new Mother_1().kind();
}
}
测试程序:
public class Test1 {
public static void main(String[] args) {
Son son = new Son();
System.out.println("Son 的Strong:" + son.getStrong());
System.out.println("Son 的kind:" + son.getKind());
}
}
----------------------------------------
Output:
Son 的Strong:10
Son 的kind:6
儿子继承了父亲,变得比父亲更加强壮,同时也继承了母亲,只不过温柔指数下降了。这里定义了两个内部类,他们分别继承父亲Father类、母亲类Mother类,且都可以非常自然地获取各自父类的行为,这是内部类一个重要的特性:内部类可以继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,多重继承才会成为可能。
有关于更多接口和内部类的详情,请参考这里:
- 接口: Java提高篇-----抽象类与接口
- 内部类: Java提高篇----详解内部类
四、知乎帖子
最后发现一个大宝库——知乎,有相关的讨论:
Java 为什么不支持多继承?
- java为什么不支持多继承?
- 有了接口为什么还要abstract class?
4.1-RednaxelaFX
先从Java 8之前说起。要区分“声明多继承”与“实现多继承”。Java是不允许“实现多继承”,简称不允许“多继承”。但是Java支持“声明多继承”——Java的接口的多继承——一个类可以实现多个接口(“继承”了多个接口上的方法声明),而一个接口可以继承多个接口(同样是“继承”了多个接口上的方法声明)。接口只允许有方法声明而不允许有实现,因而不会出现像C++那样的实现多继承的决议问题;抽象类可以有方法实现,但要遵循Java类的单继承限制,也避免了实现多继承的问题。这是早期Java为了与C++区分开的一个决定。
然后,从Java 8开始,接口允许为方法提供“默认实现”了——默认方法(default method)。因而实质上Java 8的接口多继承其实也会涉及到实现多继承,并且语言层面有专门规定去解决实现多继承时选择哪个版本的问题——哪个都不选择,而是在发现会继承多个默认方法实现并且没有override时报错,逼使用户显式override可能冲突的方法。这使得Java 8开始接口可以当作traits来使用,达到实现多继承的目的。
4.2-CharlieW
先举一个多重继承的例子,我们定义一个动物(类)既是狗(父类1)也是猫(父类2),两个父类都有“叫”这个方法。那么当我们调用“叫”这个方法时,它就不知道是狗叫还是猫叫了,这就是多重继承的冲突。而java对此的解决方法是,一个物体的本质只能有一个。一个动物只能是狗或只能是猫,如果你想创造一个会玩毛线球会玩激光(被激光玩?)的狗,那么只需要创造一个描述这类行为的接口(就叫玩耍吧),然后在自己的类里面实现“玩耍”接口,具体实现这些玩的行为,最终你同样会得到一个既像狗又像猫的动物。如果你想让这个动物叫起来像猫而不是狗,那么使用覆写(override)机制,子类里重新定义“叫”这个行为即可。但是无论如何,这样得到的类是绝对不会有多重继承的冲突的。
再来说说abstract class和interface的区别。
abstract class的核心在于,我知道一类物体的部分行为(和属性),但是不清楚另一部分的行为(和属性),所以我不能自己实例化。还是刚才那个例子,如果你有个abstract class叫哺乳动物,那么你可以定义他们胎生,恒定体温等共同的行为,但是具体“叫”这个行为时,你得留着让非abstract的狗和猫等等子类具体实现。
interface的核心在于,我只知道这个物体能干什么,具体是什么不需要遵从类的继承关系。比如上述的“玩耍”interface,狗有狗的玩法,猫有猫的玩法,妖魔鬼怪机器人都可以玩耍,只要你告诉我这个物体有玩耍接口,我就能让它玩起来(๑•̀ㅂ•́) ✧
所以abstract class和interface是不能互相替代的,interface不能定义(它只做了声明)共同的行为,事实上它也不能定义“非常量”的变量。而abstract class只是一种分类的抽象,它不能横跨类别来描述一类行为,它使得针对“别的分类方式”的抽象变得无法实现(所以需要接口来帮忙)。
而多重继承不但会造成冲突,还让一个类变得不伦不类,看不出这个类的本质,所以java毅然舍弃掉了这个祸害。
4.3-解牛
Java支持接口的多继承,不支持类的多继承,Java不支持类的多继承的一个原因是避免实现方法的版本冲突,看下面例子:
class Person{
public void say(){
System.out.println("Person say。");
}
}
class People{
public void say(){
System.out.println("People say。");
}
}
class Student extends Person,People{
public static void main(String[] args) {
this.say();//不知道该执行哪个程序。
}
}
如上代码清单,编译器不知道哪个版本的say方法是正确。而单一继承则解决了上述的问题。另外,关于有了接口为什么还需要抽象类,可以从接口和抽象类的不同点看出一些端倪来。接口里的方法都是public abstract的(无论你是否显示的用这两个修饰符修饰),而抽象类中则可以有非抽象方法,我们在设计程序的时候,可以把方法声明在一个接口里,然后用一个抽象类实现这个接口,把其中公用的方法实现了,而把更具体的方法留给抽象类的子类去实现。还是举个例子吧:
interface People{
public abstract void sayHello();
public abstract void work();
}
abstract class Chinese implements People{
@Override
public void sayHello(){
System.out.println("Chinese people say hello in Chinese.");
}
public abstract void work();
}
class Student extends Chinese{
@Override
public void work() {
System.out.println("My work is study Confucianism.");
}
}
class ChineseTeacher extends Chinese{
@Override
public void work() {
System.out.println("My work is teach Chinese and Confucianism.");
}
}
class Test{
public static void main(String[] args) {
People student = new Student();
People chineseTeacher = new ChineseTeacher();
student.sayHello();//Chinese people say hello in Chinese.
chineseTeacher.sayHello();//Chinese people say hello in Chinese.
student.work();//My work is study Confucianism.
chineseTeacher.work();//My work is teach Chinese and Confucianism.
}
}
看上面的例子,Chinese类型的实例(注意这里指Chinese类的子类的实例,因为Chinese是抽象类不能实例化)sayHello都是用中文,所以可以把这个方法在抽象类中实现,它的子类就可以直接使用而不用每次都实现该方法。但是work方法不同子类型的实现可能不同,因此Chinese抽象类不打算实现该方法,而是留给自己的子类去实现。所以Student类和ChineseTeacher类拥有共同的sayHello方法以及各自的work方法。这样实现了代码的重用。