目录
1. 前言
在前面的学习中我们了解了面向对象语言的两种特性,继承和封装。也是Java语言的特性,今天我们来详细介绍一下面向对象的三大特性中,多态这一特性。我们首先用一个简单的例子来解释一下多态的概念。所谓的多态就是多种形态,当不同的对象去完成时会产生不同的姿态。比如小动物们都会吃东西。但是狗会吃狗粮,猫会吃猫粮。
总的来说,同一种事情发生在不同的对象身上,就会有不同的效果。
2.多态的实现条件
在Java语言中,要想实现多态。必须满足以下几个条件。
1:必须在继承体系下。
2:子类必须要对父类的方法进行重写。
3:通过父类的引用来调用重写的方法。
多态的体现:在代码运行时,当传递不同的对象,则会传递不同的方法。
package demp;
class Animal {
String name;
int age;
public Animal(String name, int age){
this.name = name;
this.age = age;
}
public void eat(){
System.out.println(name + "吃饭");
}
}
class Cat extends Animal{
public Cat(String name, int age){
super(name, age);
} @Override
public void eat(){
System.out.println(name+"吃鱼~~~");
}
}
class Dog extends Animal {
public Dog(String name, int age){
super(name, age);
}
@Override
public void eat(){
System.out.println(name+"吃骨头~~~");
}
}
public class Test {
// 编译器在编译代码时,并不知道要调用Dog 还是 Cat 中eat的方法
// 等程序运行起来后,形参a引用的具体对象确定后,才知道调用那个方法
// 注意:此处的形参类型必须时父类类型才可以
public static void eat(Animal a){
a.eat();
}
public static void Test(Animal animal){
animal.eat();
}
public static void main(String[] args) {
/*Cat cat = new Cat("元宝",2);
Dog dog = new Dog("小七", 1);
eat(cat);
eat(dog);
cat.eat();
Animal animal=new Cat("圆圆",2);
animal.eat();*/
Test(new Cat("图图",145));
Test(new Dog("王",11));
}
}
运行结果:
可以看到,当我们调用eat方法的时候,可以通过eat调用不同的子类方法。当我们,通过a的引用调用eat方法会有不同的表现(和a的引用实例相关),这种行为就叫做多态。
我们通过一组图来分析一下这个过程:
3.重写
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法
规则:
1:子类在重写父类方法的时候,方法名要相同,返回值类型,参数列表都要相同。
2:返回值类型可以不同,但是必须是具有父子关系的
3:访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected
4:父类被static、private修饰的方法、构造方法都不能被重写。
5:重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写。
方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法
4.向上转型和向下转型
4.1向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型()
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换。
向上转型有三种使用场景分别是:
1. 直接赋值
2. 方法传参
3. 方法返回
public class TestAnimal {
// 2. 方法传参:形参为父类型引用,可以接收任意子类的对象
public static void eatFood(Animal a){
a.eat();
} // 3. 作返回值:返回任意子类对象
public static Animal buyAnimal(String var){
if("狗".equals(var) ){
return new Dog("狗狗",1);
}else if("猫" .equals(var)){
return new Cat("猫猫", 1);
}else{
return null;
}
}
public static void main(String[] args) {
Animal cat = new Cat("元宝",2); // 1. 直接赋值:子类对象赋值给父类对象
Dog dog = new Dog("小七", 1);
eatFood(cat);
eatFood(dog);
Animal animal = buyAnimal("狗");
animal.eat();
animal = buyAnimal("猫");
animal.eat();
}
}
4.2向下转型
我们刚刚聊到了向上转型,它可以调用父类中被子类对象重写的方法,但是如果我想调用子类中特有的方法呢?如果直接使用向上转型的引用来调用,我们会发现调不出来。那么我们可以通过向下转型来调用。
abstract class Animal{
String name;
int age;
public abstract void eat();
}
class Dog extends Animal{
@Override
public void eat() {
System.out.println(this.name+"正在吃饭");
}
public void wangwang(){
System.out.println(this.name+"正在汪汪叫");
}
}
public class Test {
public static void main(String[] args) {
Animal animal=new Dog();
animal.name="圆圆";
Dog dog=(Dog)animal;
dog.wangwang();
}
}
可以看到,我们将父类引用又转化成了子类的引用。这样我们就可以调用子类特有的方法了。但是向下转型很不安全,比如我们有一个cat类,当我们使用它来通过animal来向下转型的时候:
public static void main(String[] args) {
Animal animal=new Dog();
/*animal.name="圆圆";
Dog dog=(Dog)animal;
dog.wangwang();*/
Cat cat=(Cat)animal;
}
看似没什么问题,编译器也没报错。但当我们运行的时候:
就会运行时报错了,这是因为animal引用本身所指向的对象是Dog类,所以它能被转换回去,当我们用它来转换成Cat类,自然就不行了。为了安全性质,我们引用了一个新的关键字
这个instanceof关键字,是用来判断该引用是否引用了Cat的对象,如果引用了,就不会报错,代码就能执行下去,如果没有引用,自然不会执行了。
5.多态的优缺点
5.1使用多态的好处
1. 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
什么叫 "圈复杂度" ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度“。如果一个方法的圈复杂度太高, 就需要考虑重构.不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .
例如我们现在需要打印的不是一个形状了, 而是多个形状的拼接. 如果不基于多态, 实现代码如下:
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("❀");
}
}
public class Test{
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();
}
}
}
}
这样写的话 代码会非常臃肿,但如果我们使用多态来写:
public static void drawShapes(){
Shape[] shapes={new Cycle(),new Cycle(),
new Rect(),new Rect()};
for (Shape shape:shapes){
shape.draw();
}
}
public static void main(String[] args) {
drawShapes();
}
}
这样来通过多态,然后遍历这些父类引用指向的子类对象然后调用各自的方法。代码就很简洁了。
2. 可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
5.2多态缺陷:代码的运行效率降低。
1. 属性没有多态性
当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
2. 构造方法没有多态性
6.避免在构造方法中调用重写的方法
我们先来看一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func
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 .构造 D 对象的同时, 会调用 B 的构造方法.
2 .B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
3 .此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0. 如果具备多态性,num的值应该是1.
4 .所以在构造函数内,尽量避免使用实例方法,除了final和private方法
结论:用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.