一篇文章,带你了解Java中的继承与多态特性

  • 继承
    • protected关键字
    • final关键字
    • 方法重写
    • super关键字
    • 继承小结
  • 多态
    • 向上转型
    • 动态绑定
    • 理解多态
    • 多态的好处

众所周知,面向对象编程的三大特性分别是:封装、继承、多态。在很多刚接触面向对象编程初学者来说,可能不太理解这三大特性是什么意思,那么本期博主就围绕三大特性中的继承与多态来介绍。关于封装,博主已经在前面的一篇博客中讲解了,有兴趣的同学可以去看看,链接: Java类和对象的学习,话不多说,正文开始。


包 (package) 是组织类的一种方式,简单来说就是一个文件夹。在一个包中可以创建于其他包中类名相同的类。

创建方式(IDEA)
一篇文章,带你了解Java中的继承与多态特性_第1张图片
但本章重点不是包,所以这里不做过多介绍,在本章只需要记住本章提及的包是个文件夹就OK了。

继承

  • 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

基本语法

class 子类 extends 父类 {
     

} 

规则

  • extends是继承的关键字
  • Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承).
  • 子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用.

代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法),有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联,例如以下代码

在src内创建一个包testdome,在testdome创建了三个不同的文件,每个文件代表一个类
一篇文章,带你了解Java中的继承与多态特性_第2张图片

一篇文章,带你了解Java中的继承与多态特性_第3张图片

在src包下创建一个Main类,用来测试代码。

这个代码我们发现其中存在了大量的冗余代码,仔细分析, 我们发现 Animal 和 Cat 以及 Bird 这几个类中存在一定的关联关系:

  1. 这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的
  2. .这三个类都具备一个相同的 name 属性, 而且意义是完全一样的.
  3. 从逻辑上讲, Cat 和 Bird 都是一种 Animal (is - a 语义),例如:猫是一个动物。

此时我们就可以让 Cat 和 Bird 分别继承 Animal 类, 来达到代码重用的效果,代码如下
一篇文章,带你了解Java中的继承与多态特性_第4张图片

  • 此时, Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类, 对于像 Cat 和 Bird 这样的类, 我们称为 子类, 派生类和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果.

两张图中的代码,都能达到代码效果
一篇文章,带你了解Java中的继承与多态特性_第5张图片

但图二相比图一来讲,通过使用继承特性达到代码的复用性,少写了许多冗余代码,使整个程序看上去更加简洁。

思考
要是我们将父类Animal中的属性namepublic改成private会怎样?
一篇文章,带你了解Java中的继承与多态特性_第6张图片

我们可以看到,改成private后不仅在Main文件中会报错,在子类Bird文件当中也会报错

一篇文章,带你了解Java中的继承与多态特性_第7张图片

因为private这个访问修饰符的特性是私有的,被private修饰的成员只能在本类中访问,也就是说,如果将父类Animal中的属性name改成private,此时name属性只能在父类中访问,而子类Brid就访问不到,所以会报错,由此得知

  • 子类会继承父类的所有 public 的字段和方法.
  • 对于父类的 private 的字段和方法, 子类中是无法访问的.

重点
子类在构造时, 一定要先帮助父类进行构造。在子类的构造方法中,我们要先调用父类的构造方法来帮助父类进行构造,不知道构造方法的同学可以先去我的这篇博客了解一下,链接: Java类和对象的学习.

如果父类当中是默认构造方法,那么子类当中的构造方法如果没主动调用父类构造方法的话,此时会显式调用父类默认的构造方法
一篇文章,带你了解Java中的继承与多态特性_第8张图片
如果父类实现了构造方法,那么此时子类在构造时就必须得使用super关键字调用父类的构造方法,不管子类当中有多少个构造方法,都必须在子类构造之前调用父类构造方法先帮助父类进行构造。
一篇文章,带你了解Java中的继承与多态特性_第9张图片
否则代码就会报错
一篇文章,带你了解Java中的继承与多态特性_第10张图片
super关键字后面有讲。

protected关键字

通过上面问题我们发现,使用public来修饰成员字段达不到"封装"的目的,而使用private子类又无法继承,那有没有两全其美的方法呢?

当然有,Java提供了一个protected访问权限,它的作用

  1. 实现了父类的 “封装” 性:对于类的调用者来说, protected 修饰的字段和方法是不能访问的
  2. 实现了子类的继承性:对于类的子类 和 同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的

我们将public改成protected
一篇文章,带你了解Java中的继承与多态特性_第11张图片

此时可以发现,在Main文件当中执行代码依然会报错,而在子类Brid当中却没报错了。
一篇文章,带你了解Java中的继承与多态特性_第12张图片
protected其实是一个访问权限修饰符,而在Java当中一共有4种访问权限修饰符,分别有着不同的权限范围,以下一幅图可做参考
一篇文章,带你了解Java中的继承与多态特性_第13张图片

所以,我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用public.

final关键字

final是Java中的一个关键字,被final修饰的变量表示常量 (不能被修改),而final其实还可以修饰类,当一个类被final修饰时,那么代表这个类不能被继承。

在刚才的继承学习当中我们知道,动物是一个类,而动物有很多种类,猫是动物的一种,但实际生活当中不管是猫还是鸟,其种类也是有很多种种类
一篇文章,带你了解Java中的继承与多态特性_第14张图片

一篇文章,带你了解Java中的继承与多态特性_第15张图片

此时子类可以进一步的再派生出新的子类,以此类推,将此叫做多层继承,但是即使如此, 我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了,防止继承层次,那么此时我们可以使用final关键字来进行限制。

示例

一篇文章,带你了解Java中的继承与多态特性_第16张图片
我们将ChineseGardenCat类使用final修饰,此时发现如果OrangeCat类去继承ChineseGardenCat则代码会报错。
一篇文章,带你了解Java中的继承与多态特性_第17张图片

方法重写

子类可继承父类中的方法,而不需要重新编写相同的方法。但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override),

构成重写的规则

  1. 方法名称相同
  2. 返回值相同
  3. 参数列表相同

先看以下代码
一篇文章,带你了解Java中的继承与多态特性_第18张图片

我们发现,在main方法中执行的b.func调用的是父类继承的方法
一篇文章,带你了解Java中的继承与多态特性_第19张图片
那么我们将子类B重写父类的func方法,会发生什么呢?
一篇文章,带你了解Java中的继承与多态特性_第20张图片
此时执行的是子类重写的方法。
一篇文章,带你了解Java中的继承与多态特性_第21张图片

针对重写的方法, 可以使用 @Override 注解来显式指定,有了这个注解能帮我们进行一些合法性校验.,例如不小心将方法名字写错了 (比如写成 fun), 那么此时编译器就会发现父类中没有 fun方法, 就会编译报错, 提示无法构成重写.一篇文章,带你了解Java中的继承与多态特性_第22张图片

重写的注意事项

  1. 普通方法可以重写, static 修饰的静态方法不能重写.
    一篇文章,带你了解Java中的继承与多态特性_第23张图片
  2. 重写中子类的方法的访问权限不能低于父类的方法访问权限.
    一篇文章,带你了解Java中的继承与多态特性_第24张图片
  3. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 协变类型除外).
    一篇文章,带你了解Java中的继承与多态特性_第25张图片
    如果重写方法返回值是继承关系,这种一般称之为协变类型,是可以构成重写的

super关键字

super 表示获取到父类实例的引用
super关键字的三种用法,类似于this关键字

  1. super.父类成员方法: 调用父类的成员方法
  2. super.父类成员变量: 调用父类的成员属性
  3. super(): 调用父类的构造方法

前面的代码中由于使用了重写机制, 调用到的是子类的方法. 如果需要在子类内部调用父类方法怎么办? 可以使用super 关键字。
一篇文章,带你了解Java中的继承与多态特性_第26张图片
此时调用子类的func方法的同时调用了父类的func方法
一篇文章,带你了解Java中的继承与多态特性_第27张图片
需要注意,如果子类不加super调用func方法,默认调用的是子类重写的func方法。

this关键字与super关键字的区别
尽管两个关键字的用法都差不多,但还是有本质区别的,可参考下图

继承小结

  1. 继承表示 is - a 语义,在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “是” 一种动物.
  2. 子类在构造时要先帮助父类进行构造(重点)
  3. 在使用的时候应该尽可能的使用比较严格的访问权限
  4. 使用final 关键字的限制类被继承,防止多层继承,代码重构
  5. 重写中子类的方法的访问权限不能低于父类的方法访问权限
  6. 被static修饰的方法不能被重写
  7. 注意 super 和 this 功能有些相似, 但是还是要注意其中的区别.

多态

多态是同一个行为具有多个不同表现形式或形态的能力,就是同一个接口,使用不同的实例而执行不同操作,多态是一种思想。

在了解多态前,我们先来了解一下两个重要知识点,向上转型与动态绑定

向上转型

在面向对象程序设计中, 针对一些复杂的场景(很多类, 很复杂的继承关系), 程序猿会画一种 UML 图的方式来表示类之间的关系. 此时父类通常画在子类的上方. 所以我们就称为 “向上转型” , 表示往父类的方向转.
一篇文章,带你了解Java中的继承与多态特性_第28张图片
在Java中使用父类引用引用子类对象,此时就会发生向上转型,发生向上转型的时机有三种

  1. 直接赋值
  2. 方法传参
  3. 方法返回

这里先放上代码图,方便后面讲解。
一篇文章,带你了解Java中的继承与多态特性_第29张图片

直接赋值
在代码中,如果使用父类定义的变量引用子类创建的对象,此时就会发生向上转型,例如:
一篇文章,带你了解Java中的继承与多态特性_第30张图片
方法传参
在传给方法的参数中也可以发生向上转型的,例如:
一篇文章,带你了解Java中的继承与多态特性_第31张图片
方法返回值
不仅方法的参数可以发生向上转型,方法的返回者也可以发生向上转型,例如:
一篇文章,带你了解Java中的继承与多态特性_第32张图片
注意
发生向上转型后,通过父类的引用,只能访问自己(父类)的方法或属性。
一篇文章,带你了解Java中的继承与多态特性_第33张图片

如果通过父类去引用子类的方法,此时代码会发生报错
一篇文章,带你了解Java中的继承与多态特性_第34张图片
那么向上转型有什么作用呢?这个在后续的多态思想中很重要。

动态绑定

动态绑定也叫运行时绑定,发生在向上转型调用子类重写的方法的时候。我们学习了前面的向上转型,知道了通过父类的引用只能调用到父类的属性及方法,但如果子类重写了父类的方法,此时编译时调用的是父类的方法,但实际运行中调用的是子类重写的方法。

发生动态绑定的前提:

  1. 要先向上转型
  2. 通过父类引用来调用父类和子类同名的覆盖(重写)方法

举个例子
一篇文章,带你了解Java中的继承与多态特性_第35张图片
上图代码,在子类当中,重写了父类方法,此时在编译时调用的是父类的eat方法,但在运行时调用的是子类重写的eat方法
一篇文章,带你了解Java中的继承与多态特性_第36张图片

我们来看以下一段有坑的代码。
一篇文章,带你了解Java中的继承与多态特性_第37张图片

从代码中,我们看到,通过new关键字,调用了D的构造方法创建了一个对象,而前面我们说过在子类构造之前要先帮助父类先进行构造,而此时父类B当中的构造方法调用了func方法,那么此时这个调用的func方法是子类D重写的func方法呢,还是父类B当中的func方法呢?
运行截图
一篇文章,带你了解Java中的继承与多态特性_第38张图片
通过运行截图我们可以发现,调用的是子类D重写的方法,我们仔细分析一下,在父类B当中的构造方法调用被重写func时,此时也会发生动态绑定,所以调用的是子类D重写的方法,但此时父类B还没构造完成,也就是说,此时子类D自身还没有被构造,那么num也是默认值的状态0,所以在调用重写的funcnum并没有被赋值成1,所以最后打印在屏幕上的num0

结论:在父类构造方法中,如果调用了重写的方法,也是会发生动态绑定的,所以在编程当中用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.

理解多态

有了前面向上转型与动态绑定的只是铺垫,那么现在我们可以使用多态思想来进行编程了,所谓多态,无非就是通过一个引用来呈现出不同的形态。代码中的多态也是如此. 一个引用到底是指向父类对象, 还是某个子类对象(可能有多个), 也是要根据上下文的代码来确定.

来看以下代码
一篇文章,带你了解Java中的继承与多态特性_第39张图片
按照以往的方法,假如要输出所有的图形,那我们就要创建三个对象,分别调用打印图形的方法,如下图
一篇文章,带你了解Java中的继承与多态特性_第40张图片
但我们知道,这三个子类当中都重写了父类的draw方法,并且这三个子类都是同一个父类,那么我们此时就能将代码改成以下:
一篇文章,带你了解Java中的继承与多态特性_第41张图片
可以看到,达到的效果都是一样的,当类的调用者在编写 DisplayPattern 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态。

多态的好处

类调用者对类的使用成本进一步降低

  • 封装是让类的调用者不需要知道类的实现细节.
  • 多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.

能够降低代码的 “圈复杂度”, 避免使用大量的 if - else

  • 圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
  • 因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需要考虑重构.

比如我们现在打算打印多个图案,如果不基于多态,则需要使用大量的if else语句来逐步判断,实现代码如下:
一篇文章,带你了解Java中的继承与多态特性_第42张图片
但如果使用多态的话,则可以不用这么多if else分支语句,只需要一个循环即可实现,代码如下。
一篇文章,带你了解Java中的继承与多态特性_第43张图片
可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低
一篇文章,带你了解Java中的继承与多态特性_第44张图片
对于类的调用者来说(DisplayPattern方法), 只要创建一个新类的实例就可以了, 改动成本很低。
而对于不用多态的情况, 就要把 DisplayPattern中的 if - else 进行一定的修改, 改动成本更高.

多态总结

多态是面向对象程序设计中比较难理解的部分, 重点是多态带来的编码上的好处,另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 “继承” 这样的语法并没有必然的联系.

  • C++ 中的 “动态多态” 和 Java 的多态类似. 但是 C++ 还有一种 “静态多态”(模板), 就和继承体系没有关系了.
  • Python 中的多态体现的是 “鸭子类型”, 也和继承体系没有关系.
  • Go 语言中没有 “继承” 这样的概念, 同样也能表示多态.

无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式


以上就是本章介绍的内容,希望能给读者带来帮助!!!感兴趣的读者可以看看博主其它博客哦。
Java类和对象的学习
Java数据结构 : 顺序表
Java数据结构 :单链表

你可能感兴趣的:(java,封装,继承,多态,java)