多态是Java三大基本特征中最抽象也是最重要的特征。多态是建立在封装和继承衍生之上的。
1. 多态的基本介绍
多(多种)态(状态)。 多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。
举个例子:动物有很多种类,狗、猫等等,在吃这个条件下;猫吃猫粮,狗吃狗粮。这就是多态的具体体现。
总之就是:同一件事情,发生在不同对象身上,就会产生不同的结果。
2. 多态实现条件
在java中要实现多态,必须要满足如下几个条件,缺一不可:
1. 必须在继承体系下
2. 子类必须要对父类中方法进行重写
3. 通过父类的引用调用重写的方法
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
案例:
public class demo { public static void main(String[] args) { // 编译器在编译代码时,并不知道要调用Dog 还是 Cat 中eat的方法 // 等程序运行起来后,形参a引用的具体对象确定后,才知道调用那个方法 // 注意:此处的形参类型必须时父类类型才可以 Animal dog = new Dog(); dog.eat(); Animal cat = new Cat(); cat.eat(); } } class Animal { public void eat() { System.out.println("吃饭"); } } class Dog extends Animal { public String name; public void bark() { System.out.println("汪汪汪!"); } } class Cat extends Animal { public void miaow() { System.out.println("喵喵喵!"); } }
这个就是一个多态的向上转型,Dog dog = new Dog() ; 这没问题,而现在dog的类型为Animal了,类型不一样;理论上来说类型不一样的不可以这么赋值,为什么这里可以写,那是因为二者构成了父子类关系。
那现在我们去试着调用子类中特有的方法:
编译似乎不然通过,这是为什么呢?
当我们的 " . " 之前的类型里,是必须包含 " . " 之后的成员或方法,否则编译不通过。当我们调用bark的时候,Animal中找不到Dog指向的bark,所以无法运行。
结论: 当我们发生向上转型的时候,我们只能调用父类有的成员和方法,不能调用子类特有的成员和方法。
向上转型是多态发生的基础。
还有其他向上转型的其他例子:
public static void func(Animal animal){ } public static void main(String[] args) { Dog dog = new Dog(); func(dog); }
public static Animal func(){ return new Dog(); } public static void main(String[] args) { Dog dog = new Dog(); func(); }
3. 重写
我们上面猫和狗吃饭有点不太符合我们的实际需求,狗就吃狗粮,猫就吃猫粮,并且我不想在父类中对eat进行重写。
我们将代码进行修改:
public class demo { public static void main(String[] args) { Animal dog = new Dog(); dog.name = "小金毛"; dog.eat(); Animal cat = new Cat(); cat.name = "小折耳"; cat.eat(); } } class Animal { public String name; public int age; public void eat() { System.out.println("吃饭"); } } class Dog extends Animal { public void bark() { System.out.println("汪汪汪!"); } @Override public void eat() { System.out.println(name + "吃狗粮"); } } class Cat extends Animal { public void miaow() { System.out.println("喵喵喵!"); } @Override public void eat() { System.out.println(name + "吃猫粮"); } }
注意看:
子类中有一个与父类同名的方法上面一行有一个 " @Override " 这个就是注解,注解有很多中,这里的 " @Override " 是指对重写的方法进行检查,检查你是否发生了重写,如果没有发生会报错。
重写的介绍:
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程
进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定
于自己的行为。 也就是说子类能够根据需要实现父类的方法。
【方法重写的规则】
1. 子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致
2. 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
3. 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected
4. 父类被static、private修饰的方法、构造方法都不能被重写。
5. 重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
【重写和重载的区别】
区别点 | 重写(override) | 重载(override) |
参数列表 | 一定不能修改 | 必须修改 |
返回类型 | 一定不能修改【除非可以构成父子类关系】 | 可以修改 |
访问限定符 | 一定不能做更严格的限制(可以降低限制) | 可以修改 |
动、静态绑定机制
至于重写是怎么发生的,这里需要讲一个动态绑定机制。
Java的动态绑定机制(非常重要):
1. 当调用对象方法时,该方法会和该对象的内存地址(运行类型)绑定
2. 当调用对象属性时,没有动态绑定机制,哪里声明,那里使用。
动态绑定发生条件:
1. 向上转型
2. 重写
3. 通过父类引用调用父类和子类重写的方法
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表:方法的重载。
5 向上转型和向下转型
向上转型
向上转型的本质:父类的引用指向了子类的对象。
语法:父类类型 对象名 = new 子类类型()
例如:
Animal animal = new Cat("元宝",2);
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换。
猫属于动物,但是动物不仅仅只有猫。
【使用场景】
1. 直接赋值
2. 方法传参
3. 方法返回
案例不做演示,和上面的案例差不多。
向上转型的特点(总结):
1. 可以调用父类的所有成员,但是需要遵守权限。
2. 不能调用子类特有的成员、方法。//因为在编译阶段,能调用哪些成员是由编译类型决定的
3. 最终运行效果看子类的具体表现,即调用时从子类开始查找方法,然后调用。
向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。
向下转型
语法:子类类型 引用名 = (子类类型) 父类引用
例如: Cat cat = (Cat) animal;
要求:父类的引用必须指向的是当前目标类型的对象。
向上转型是在堆上创建一个子类的对象把地址赋给了父类的引用,向下转型就是这个父类的引用把开辟的子类的空间的地址回到了子类的引用,所以可以使用子类独有的方法。
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
public class TestAnimal { public static void main(String[] args) { Cat cat = new Cat("元宝",2); Dog dog = new Dog("小七", 1); // 向上转型 Animal animal = cat; animal.eat(); animal = dog; animal.eat(); // 编译失败,编译时编译器将animal当成Animal对象处理 // 而Animal类中没有bark方法,因此编译失败 // animal.bark(); // 向上转型 // 程序可以通过编程,但运行时抛出异常---因为:animal实际指向的是狗 // 现在要强制还原为猫,无法正常还原,运行时抛出:ClassCastException cat = (Cat)animal; cat.mew(); // animal本来指向的就是狗,因此将animal还原为狗也是安全的 dog = (Dog)animal; dog.bark(); } }
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为true,则可以安全转换。
public class TestAnimal { public static void main(String[] args) { Cat cat = new Cat("元宝",2); Dog dog = new Dog("小七", 1); // 向上转型 Animal animal = cat; animal.eat(); animal = dog; animal.eat(); if(animal instanceof Cat){ cat = (Cat)animal; cat.mew(); } if( animal instanceof Dog){ dog = (Dog)animal; dog.bark(); } } }
nstanceof 关键词官方介绍:Chapter 15. Expressions
多态的优缺点
假设有如下代码:
class Shape { //属性.... public void draw() { System.out.println("画图形!"); } } class Rect 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("❀"); } }
【使用多态的好处】
1. 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
什么叫 "圈复杂度" ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度".
如果一个方法的圈复杂度太高, 就需要考虑重构.
不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10
例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:
public static void drawShapes() { Rect rect = new Rect(); Cycle cycle = new Cycle(); Flower flower = new Flower(); String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"}; for (String shape : shapes) { if (shape.equals("cycle")) { cycle.draw(); } else if (shape.equals("rect")) { rect.draw(); } else if (shape.equals("flower")) { flower.draw(); } } }
如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单
public static void drawShapes() { // 我们创建了一个 Shape 对象的数组. Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), new Rect(), new Flower()}; for (Shape shape : shapes) { shape.draw(); } }
2. 可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
class Triangle extends Shape { @Override public void draw() { System.out.println("△"); } }
对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.
而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.
多态缺陷:代码的运行效率降低。
1. 属性没有多态性
当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
2. 构造方法没有多态性