Java——多态
1. 多态的概念2. 多态的实现条件2.1 向上转型2.2 常见的可以发生向上转型的3个时机2.3 方法重写2.3.1 方法重写的条件2.3.2 方法重写与方法重载的比较2.3.3 方法重写的快捷键2.3.4 toString( )重写的原理 2.4 动态绑定2.5 静态绑定2.6 向下转型2.6.1 instanceof 判断向下转型是否正确 3. 多态的优缺点&多态的一个应用举例3.1 多态的应用3.2 多态的优缺点3.3 避免在构造方法中调用重写的方法
1. 多态的概念
多态:通俗来说,就是多种形态。具体点来说,就是同一件事,不同的对象去完成时会表现出不同的状态。
比如见下面这张图:
同样是打印的行为,但是打印机不同(对象不同),所表现的结果也不相同。
2. 多态的实现条件
在java中要实现多态,必须要满足以下的几个条件: 必须在继承的体系下,且是向上转型 子类必须要有对父类方法的重写 通过父类的引用调用重写的方法多态的体现:在代码运行时,当传递不同类的对象的时候,会调用对应子类中重写的方法,而不是调用父类的
(看到这里还是没有明白多态是正常的,必须要通过代码才能够更清晰的了解)
2.1 向上转型
向上转型:把子类的对象给到父类,或者说是 父类的引用指向子类的对象
这里Cat类是继承Animal类,我们可以看到,animal这个引用的类型明明是Animal类型,但是指向的却是Cat这个类创建的对象。
虽然我们平常都说“=”运算符,左右两边的数据类型必须是要相同的,但是这个继承比较特殊。
这个例子就是父类的引用指向了子类的对象,也就是向上转型。
2.2 常见的可以发生向上转型的3个时机
直接赋值Animal animal = new Dog("圆圆",19);
方法的参数,传参的时候进行向上转型 public static void func1(Animal animal){}public static void main(String[] args){ Dog dog = new Dog("圆圆",19); func1(dog);}
返回值 public static Animal func2(){ Dog dog = new Dog("圆圆",19); return dog;}
总之,总的来说就是父类的引用指向了子类的对象
2.3 方法重写
重写也称为 覆盖
2.3.1 方法重写的条件
构成方法重写的条件: 发生在继承关系中,指的是子类对父类的方法进行重写 方法的返回值一样 方法名一样 方法的参数列表一样重写的注意事项:
子类中重写方法的访问权限不能够比父类中被重写方法的访问权限更低——重写方法的访问权限 >= 子类方法的访问权限父类中被static、private、final修饰的方法、构造方法都不能被重写重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.被重写的方法返回值类型可以不同,但是必须是具有父子关系的2.3.2 方法重写与方法重载的比较
重写是 方法名 、 返回类型 、 参数列表 都必须一样;
重载是要求方法的 参数列表必须改变 ,另外两个可变、可不变。
2.3.3 方法重写的快捷键
在子类中右键鼠标点击Override Methods
选择父类中要重写的方法即可
2.3.4 toString( )重写的原理
在前面类和对象的时候,我们学习了可以在类中快速创建一个toString()方法,用来打印子类的数据。
链接: 类和对象
其实这个过程就运用了方法的重写。
原理如下:
首先, Object默认是所有类的父类
这里println的方法原型是下图:
然后再看valueOf的方法原型:
可以看到这里调用了toString()方法,只不过是父类Object的,因此若是我们在类里面重写了toString()方法,就会调用自己的。
下面是原理图合集:
2.4 动态绑定
动态绑定对应的是方法重写,也就是多态。
在上述代码之后,若是我们在main方法里面写这样的代码:
Animal animal = new Dog("圆圆",19);animal.eat;
我们会发现animal这个父类的引用,调用的是子类的eat方法。 我们把这个过程就叫做动态绑定。
所谓动态绑定就是指在程序运行的时候,将eat这个方法绑定到了子类的eat方法,因此调用的就是子类的eat
我们再通过汇编代码来看看动态绑定
2.5 静态绑定
静态绑定对应的是方法重载。
见下面代码:
add(int a,int b)add(int a,int b,int c)add(int a,int b,int c,int d)main(){ add(1,2); add(1,2,5); add(1,5,6,8);}
编译器通过传入的参数个数和类型就能够找到对应的方法,这就是静态绑定。
2.6 向下转型
前面我们学习到向上转型就是父类的引用指向了子类的对象,可是向上转型有一个缺点,就是通过向上转型创建的变量只能够调用子类和父类共有的方法(重写的方法),但是不能够调用子类特有的方法。
为了解决这个问题就有了向下转型。向下转型也就是将父类引用再还原为子类对象即可,即向下转换
因此向下转型的发生必须有向上转型,并且这个父类引用只能够还原为该引用所指向的对象,不能是另外一个类的对象此外向下转型必须要进行强制类型转换
见代码:
public class test { public static void main(String[] args) { Animal animal = new Dog("圆圆",1); Dog dog = (Dog) animal; //向下转型 dog.bark(); }}
但是若是Cat cat = (Cat)animal;
这个就不可以,因为还原错误,animal指的是Dog类,不能还原为Cat类。
2.6.1 instanceof 判断向下转型是否正确
public class test { public static void main(String[] args) { Animal animal = new Dog("圆圆",1); //如果animal引用的对象是Cat对象的实例 if(animal instanceof Cat){ Cat cat = (Cat) animal; cat.miaomiao(); }else{ System.out.println("error"); } }}
这样就可以更加安全
3. 多态的优缺点&多态的一个应用举例
3.1 多态的应用
直接见代码:
class Shape{ public void draw(){ System.out.println("画一个图形"); }}class Rect extends Shape{ @Override public void draw() { System.out.println("矩形"); }}class Triangle extends Shape{ @Override public void draw() { System.out.println("△"); }}class Cycle extends Shape{ @Override public void draw() { System.out.println("○"); }}class Flower extends Shape{ @Override public void draw() { System.out.println("❀"); }}public class test { public static void drawMap(Shape shape){ shape.draw(); } public static void main(String[] args) { Shape[] shapes = {new Cycle(),new Flower(),new Cycle(),new Rect(),new Triangle()}; for (Shape shape:shapes) { drawMap(shape); } }}
关于这个应用有几个巧妙的地方要说明: Shape[] shapes = {new Cycle(),new Flower(),new Cycle(),new Rect(),new Triangle()};这个形式是可以的,创建了一个Shape数组 用foreach进行遍历 使用了多态,是根据函数参数来实现向上转型,在drawMap()这个方法里面实现了多态 3.2 多态的优缺点
优点: 能够降低圈复杂度,避免使用大量的 if-else什么叫 “圈复杂度” ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如
果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”.
如果一个方法的圈复杂度太高, 就需要考虑重构.
不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .
比如在上面多态的应用中,如果我们需要额外多加一个形状,只需要再写一个子类,对方法进行重写就可以了,由于在 ↓ 面这个代码里面会发生动态绑定,因此这个不需要修改,所以修改的成本就低
public static void drawMap(Shape shape){ shape.draw(); }
缺点: 代码的运行效率降低。 属性没有多态性当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性 构造方法没有多态性
3.3 避免在构造方法中调用重写的方法
见下面的代码:
class B { public B() { // do nothing 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.func() 0
1. 这里父类的构造方法中func()调用的是子类中重写的方法,也就是发生了动态绑定
2. 由于父类构造方法先执行,因此在调用子类的重写方法的时候,num还没有初始化,这也是为什么执行结果num=0,因为子类此时还有没完成初始化
结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.