一、多态的概念
多态是同一个行为具有多个不同表现形式或形态的能力。就比如人吃饭,对于中国人使用筷子吃饭,美国人使用刀叉,印度人用手,不同的对象对同一个方法的调用表现出的行为是不一样的。
假如现在有一个Shap的基类,同时有一个drow()方法用来画图形,Shap是一个抽象的类,又可以派生出一些子类如Circle-画圆、Triangle-画三角形、Square-画正方形。如何将Shap-画图这件事情,发生在不同对象身上,产生不同的结果,这就是我们多态
要研究的内容。
二、多态的条件
在java中要实现多态,必须要满足如下几个条件,缺一不可:
- 必须在继承体系下
- 子类必须要对父类中方法进行重写
- 向上转型:父类引用指向子类对象Parent p = new Child();
- 通过父类的引用调用重写的方法
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。——动态绑定
上面的4种条件中,继承和方法的调用我们已经很熟悉了,下面就围绕,重写和转型进行详细介绍:
三、重写
重写(override)
:也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
方法重写的规则
- 子类在重写父类的方法时,一般必须与父类方法原型一致:
返回值类型 方法名 (参数列表)
要完全一致 被重写的方法返回值类型可以不同,但是必须是具有父子关系的。- 访问
权限
不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected.- 父类被
static、private、final
修饰的方法、构造
方法都不能被重写。- 重写的方法, 可以使用
@Override
注解来显式指定。有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写。
重写的设计原则
对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动(重写)新的内容。比如新旧手机:
重写和重载的区别
区别点 | 重写(override) | 重载(override) |
---|---|---|
参数列表 | 一定不能修改 | 必须修改 |
返回类型 | 一定不能修改【除非可以构成父子类关系】 | 可以修改 |
访问限定符 | 一定不能做更严格的限制(可以降低限制) | 可以修改 |
通常认为:方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
其实重写的返回值类型一般是不做修改的,但是也有可修改的情况,这里我查阅了一下,大家可以看一下这个例子:
class Test1 {
public Object workO() {
return new Object();
}
}
class Test2 extends Test1 {
@Override
public String workO() {
return new String();
}
} //其中String是Object的子类型.
☆特别注意☆:避免在构造方法中调用重写的方法
class B {
public B() {
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
分析: 我们已知程序执行顺序为:先执行父类静态代码块–>执行子类静态代码块–>执行父类实例代码块–>执行父类构造方法–>执行子类实例代码块–>执行子类构造方法。实例化对象d时会调用子类d的构造方法,在子类构造方法中需要先对基类B的构造方法进行构造,此时会调用方法func()由于func()为重写方法,此时发生动态绑定又回去调用子类D中重写的func()方法,但是此时的num还未赋值默认值为0,所以输出结果为:D.func() 0
结论: 在实际开发中"用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题。
四、向上转型和向下转型
1、向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
基本语法格式:父类类型 对象名 = new 子类类型()
向上转型的几种应用场景:
1. 直接赋值
shap是父类类型,但可以引用一个子类对象,此时是从子类向父类转换,从小范围向大范围的转换。
Shap shap=new Circle();
2. 父类数组存放子类对象
父类数组可以放子类类型的对象-自动发生向上转型
Shape[] shapes = {cycle,rect,cycle,rect,flower};
3. 方法传参
形参为父类型引用,可以接收任意子类的对象
public static void drowshap(Shap a){
a.drow();
}
4. 方法返回
父类接收返回任意子类对象
public static Shap shap() {
return new Circle;
}
注意:向上转型后不能调用子类特有的方法。
2、向下转型
向下转型:将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
//向上转型
Shap shap = new Circle();
//向下转型,本来指向的就是圆,因此将shap还原为圆也是安全的
Circle cir = (Circle)shap;//非父子关系,需要强制类型转换
//此时可以访问子类的方法
cir.drow();
//向上转型
Shap shap = new Circle();
//向下转型
//shap实际指向的是圆,现在要强制还原为三角,
//无法正常还原,运行时抛出:ClassCastException
Triangle tri = (Triangle)shap;//error
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。如上代码。Java中为了提高向下转型的安全性,引入了 instanceof
,如果该表达式为true,则可以安全转换。
instanceof
关键字:的作用是判断左边对象是否是右边类的实例,返回的boolean类型,true
和false
.
所以我们可以在代码中加入这些代码保障向下转型的安全:
if(shap instanceof Circle){
Circle cir = (Circle)shap;
cir.drow();
}
if(shap instanceof Triangle){
Triangle tri = (Triangle)shap;
tri.drow();
}
五、再谈多态
前面我们一直在谈论多态,对于多态,是同一个行为具有多个不同表现形式或形态的能力,而这种能力实现的本质其实是动态绑定,谈到这我们有必要谈谈起动态绑定和静态绑定:
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序
运行
时,才能够确定具体调用那个类的方法。
静态绑定:也称为前期绑定(早绑定),即在编译
时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表方法重载
。
一顿操作下来,实现多态的条件我们以经介绍完毕,对于多态的本质也有了一定的了解。下面我们以本文开头的Shap
类简单的实现一下多态:
class Shape {
public void draw() {
System.out.println("画图形!");
}
}
class Square extends Shape {
@Override
public void draw() {
System.out.println("□!");
}
}
class Circle extends Shape {
@Override
public void draw() {
System.out.println("○!");
}
}
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("△!");
}
}
public static void drawMap() {
Square rect = new Square();
Circle cycle = new Circle();
Flower triangle = new Triangle();
//父类数组可以放子类类型的对象-自动发生向上转型
Shape[] shapes = {circle,square,triangle};
//调用重写方法
for(Shape shape : shapes) {
shape.draw();
}
}
public static void main(String[] args) {
drawMap();
}
六、多态的优缺点
介绍完多态后,我们简单做个总结,盘点一下多态的优缺点。
多态优点:
1.能够降低代码的 “圈复杂度”, 避免使用大量的 if - else(圆圈复杂度:一种代码复杂度的衡量标准)
例如将如上多态写出循环,其中就会出现大量的if-else
public static void drawMap2() {
Square square = new Square();
Circle circle = new Circle();
Triangle triangle = new Triangle();
String[] shapes = {"circle", "square", "triangle"};
for (String s : shapes) {
if(s.equals("circle")) {
circle.draw();
}else if(s.equals("square")) {
square.draw();
}else {
triangle.draw();
}
}
}
2.可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低。对于类的调用者来说, 只要创建一个新类的实例就可以了, 改动成本很低。而对于不用多态的情况, 就要把 drawShapes
中的 if - else
进行一定的修改, 改动成本更高。
class Flower extends Shape {
@Override
public void draw() {
System.out.println("❀!");
}
}
多态缺点:代码运行效率降低
总结
本章主要探讨多态的使用,重点介绍了转型和重写的概念,至此面向对象的三大特性就全部讲完了,这部分内容比较抽象,希望大家多总结,多思考,我们一起快乐编程!🥰