三、多态
多态(字面意思):一种事物多种形态
多态中有三种重要的语法基础:向上转型、动态绑定、重写。缺一不可~
理解多态就需要理解:向上转型,即(父类对象引用子类对象)
(1)向上转型
什么情况下会发生向上转型
- 直接赋值
- 作为函数的参数
- 作为函数的返回值
(2)动态绑定
动态绑定的条件:
父类 引用 子类的对象
通过父类这个引用 调用 父类 和 子类
同名的覆盖(重写) 方法
重写条件:
方法名相同
参数的 个数 和 类型 相同
最好返回值相同(协变类型:返回值可以不同。返回值的关系为父子类关系)
父子类的关系
注:
如果父类中包含的方法在子类中有对应的同名同参数的方法,就会进行动态绑定。由运行时决定调用哪个方法。
一般动态/静态分别指的是编译时/运行时,和static无关。
示例
eat()
我们可以看到这里调用的时父类的方法
再看一个代码: ↓↓↓(当子类中也有同名的 eat()方法)
狼吞虎咽的eat()
我们看到这里调用的是子类的方法,这是为什么呢
因为这里发生了动态绑定
利用 javap -c
打开文件的汇编代码,可以看到这里调用的还是 Animal 的 eat 方法,这是为什么呢?
在编译的时候不能够确定此时到底调用谁的方法,在运行的时候才知道调用谁的方法,称其为运行时绑定--------即我们的动态绑定
动态绑定的两个前提:
- 父类引用 引用子类对象
- 通过这个父类引用调用父类和子类同名的覆盖方法
那么什么是同名的覆盖
👉 同名的覆盖又被叫做重写
,重写要满足以下几种情况
- 方法名相同
- 参数列表相同(个数+类型)
- 返回值相同
- 父子类的情况下
重写 与 重载的区别
方法名相同
参数的 个数 和 类型 必须有一个不同(重写:参数的类型和个数都相同)
返回值可以不同 (重写:协变类型可以返回值不同,一般情况返回值是一样)
重载涉及静态绑定(重写:动态绑定)
重写的注意事项:
- 方法不可以是静态的,静态的方法不可以重写
- 子类的访问修饰限定符范围一定要大于等于父类的访问修饰限定符
- private 方法不能重写
- 被 final 修饰的(关键字/方法)不可以被重写
- 协变类型也可以构成重写
协变类型(科普一下)
在父类引用子类的时候有一个注意事项:
通过父类引用只可以访问父类自己的成员
(3)静态绑定
静态绑定:根据你给定的参数个数和类型,判断调用哪个方法(又被称为编译式多态)
动态绑定也称为运行时绑定。静态绑定也有编译时绑定的说法。为什么会有这种说法呢?
我们看他的汇编代码:
我们可以看到这里在编译的时候已经规定好了调用哪个 func 方法,这就是静态绑定
(5)向下转型
向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象。相比于向上转型来说, 向下转型没那么常见,但是也有一定的用途。
编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法。
虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的。
但是这样的向下转型有时是不太可靠的,也不太安全。
animal 本质上引用的是一个 Cat 对象, 是不能转成 Bird 对象的,运行时就会抛出异常,所以,为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换。
instanceof 可以判定一个引用是否是某个类的实例
,如果是, 则返回 true。这时再进行向下转型就比较安全了。
(4)使用多态的好处
有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了.
我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况
利用一个代码加以说明
在这个代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的,
当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为多态
使用多态的好处是什么?
类调用者对类的使用成本进一步降低.
封装是让类的调用者不需要知道类的实现细节.
多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.
这也贴合了 <<代码大全>> 中关于 “管理代码复杂程度” 的初衷.能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下↓↓↓可扩展能力更强.
如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单,利用 foreach 进行打印
什么叫 “圈复杂度”
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需
进行重构
为什么拓展更好?
对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低. 而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高
※多态使用案例※
案例(使用多态,打印多种形状)
代码1:
代码2(实现多态)
代码三,通过方法和向上转型,来实现多态。让你们更加直观
代码如下
从另一个方面来说:通过一个引用来调用不同的draw方法,会呈现出不同的表现形式。表现的形式取决于将来它引用那个对象。这就是动态。而且实现多态的大前提,就是一定要向上转型,且实现 父类和子类的重写方法。
总结:
在这个代码中, 上方的代码(矩形、花、继承)是 类的实现者 编写的, 下方的代码(main所在的类)是 类的调用者 编写的。
当类的调用者在编写 Paint 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现。(和 shape 对应的实例相关), 这种行为就称为 多态。
多态 顾名思义, 就是 “一个引用, 能表现出多种不同形态”。
多态是面向对象程序设计中比较难理解的部分. 我们会在后面的抽象类和接口中进一步体会多态的使用. 重点是多态带来的编码上的好处.
另一方面,
如果抛开 Java, 多态其实是一个更广泛的概念, 和 “继承” 这样的语法并没有必然的联系.
C++ 中的 “动态多态” 和 Java 的多态类似. 但是 C++ 还有一种 “静态多态”(模板), 就和继承体系没有关系了.
Python 中的多态体现的是 “鸭子类型”, 也和继承体系没有关系.
Go 语言中没有 “继承” 这样的概念, 同样也能表示多态.
无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要 方式.
重写方法中的一个大坑
在构造方法中调用重写的方法(一个坑)
一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func
- 构造 D 对象的同时, 会调用 B 的构造方法.
- B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
- 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.
结论:
“用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.
抽象类
(1)什么是抽象类
抽象的反义词是具体,越不具体,就越抽象。abstract
修饰的类就叫做抽象类
,除了不能被实例化之外!其他语法规则和普通类都一样。
abstract关键字存在的意义,就是让程序员明确的告诉编译器,这个类是一个抽象的类,不应该进行实例化,于是编译器就要做好相关检查工作。
(2)什么是抽象方法
- 给方法前头加上 abstract 此时这就是一个抽象方法了.
- 抽象方法不需要方法体.
抽象方法只能在抽象类中存在
(也可以在接口中存在), 不能在普通的类中存在.抽象方法
存在的意义就是为了让子类进行重写
.
(3)抽象类中的规则及注意事项
- 抽象类不能够被实例化
例如上面的Shape类,不能写为下面这种形式:但是可以向上转型。
- 抽象方法不能是 private 的
如果一个方法被private修饰,则这个方法不能再被private修饰。
- 抽象类中可以包含其他的非抽象方法, 也可以包含字段。这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用。
- 如果一个普通类继承了一个抽象类,则这个普通类需要重写这个抽象类所有的抽象方法。除非普通类也加上abstract才不用重写抽象父类的所有抽象方法。
- 在4的基础上,如果已经有了一个抽象类(子类)继承了一个抽象类(父类),此时还有一个普通类继承抽象类(子类)时,需要重写抽象类(子类)和抽象类(父类)的所有抽象方法。
- 抽象方法与抽象类都不能被final修饰。
既然知道了 抽象类不能被 final修饰的原理,那么由此推论出:抽象方法也不可以被 final修饰。.
总结抽象类:
- 包含抽象方法的类,叫做抽象类
- 什么是抽象方法,即没有具体实现的方法,被 abstract 修饰
- 抽象类不可以实例化
- 由于不能被实例化,所以抽象类只能被继承(最大的作用)
- 抽象类当中也可以包括和普通一样的成员和方法(静态的也可以)
- 一个普通类继承了一个抽象类,那么这个普通类需要重写抽象类的所有抽象方法
- 一个抽象类A如果继承一个抽象类B,那么这个抽象类A可以不实现抽象父类B的抽象方法
- 结合第7点当A类被一个普通类继承后,A和B这两个抽象类当中的抽象方法必须被重写
- 抽象类不能被 final 修饰,抽象方法也不可以被 final 修饰