本文介绍了Java面向对象多态特性, 多态的介绍. 多态的实现条件–1.发生继承.2.发生重写(重写与重载的区别)3.向上转型与向下转型.4.静态绑定和动态绑定5. 实现多态 举例总结多态的优缺点 避免在构造方法内调用被重写的方法…

Java面向对象:多态特性的学习

  • 一.什么是多态?
  • 二.多态实现条件
    • 1.认识多层继承
    • 2.认识重写
      • ①.重写和重载的区别
    • 3.向上转型和向下转型
      • ①.认识向上转型
      • ②.认识向下转型
    • 4.静态绑定和动态绑定
      • ①.认识静态绑定
      • ②.认识动态绑定
    • 5.多态的实现
  • 三.多态的优缺点
  • 四.避免在构造方法内调用被重写的方法

一.什么是多态?

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成某一个行为时会产生出不同的状态。


彩色打印机 和黑白打印机 都具有打印行为,它们分别去打印图片,最后会产生不同的状态

总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果。

二.多态实现条件

在java中要实现多态,必须要满足如下几个条件,缺一不可:

  1. 必须在继承体系下(多个子类继承一个父类)
  2. 发生向上转型(父类引用接受子类对象地址)
  3. 子类必须要对父类中方法进行重写(子类里有父类同名的方法)
  4. 动态绑定(通过父类的引用调用被重写的方法)

多态体现:在代码运行时,当父类引用接收不同子类对象时,调用父类方法实际会运行对应不同子类中的重写的该父类方法

1.认识多层继承

在这篇博客中讲到了继承->继承特性的学习
在继承体系下 也就是要有一个父类 其派生出多个子类,或者子类还派生出自己的子类
发生多态前提至少要有两个子类,才能体现出不同的状态!

示例:

class Animal{ //父类 动物类    String name;    int age;     void eat(){        System.out.println(this.age+"岁的"+this.name+"正在吃食物");    }    Animal(String name,int age){        this.name=name;        this.age=age;    }    Animal(){        this.eat();     }}class Dog extends Animal{ //子类 :狗类继承动物类    @Override //注解  表示下面的方法需要重写父类的方法 如果达到重写条件会报错, 帮你检查重写的错误       void eat(){        System.out.println(this.age+"岁的"+this.name+"正在吃狗粮");    }    }    void bark(){        System.out.println(this.age+"岁的"+this.name+"正在犬吠");    }    Dog(String name, int age){        super(name,age);    }    Dog(){        super();    }}class Bird extends Animal{  //子类: 鸟类继承动物类    @Override    void eat(){        System.out.println(this.age+"岁的"+this.name+"正在吃稻谷");    }    void fly(){        System.out.println(this.age+"岁的"+this.name+"正在飞");    }    Bird(String name,int age){        super(name,age);    }}class Huskies extends Dog{  //子类: 哈士奇类继承狗类 狗类又继承动物类 (多层继承)    Huskies(String name, int age){        super(name,age);    }    void pullOf(){        System.out.println(this.age+"岁的"+this.name+"正在拆家");    }    @Override    void eat(){        System.out.println(this.age+"岁的"+this.name+"正在吃主人食物");    }    Huskies(){        super();    }}

上面代码实现了一个Animal父类有两个子类 Dog类和Bird类 而Dog类还有子类Huskies类
在此继承体系上可以发生多态,但是下一步还需满足 子类重写父类的方法,可以看到上面代码子类里有和父类同名的方法也就是发生了重写,而重写具体是什么呢” />
【向上转型使用场景】

  1. 直接赋值
Animal animal1=new Dog("大白",4); //父类引用 接受子类对象地址 发生向上转型
  1. 方法传参
 void func1(Animal animal){        System.out.println(animal.name);//发生向上转型 只能访问子类继承的父类自己的部分 此时name在之前也被修改了     }//....  Dog dog=new Dog("小白",3);  //狗类对象 func1(dog);  //dog是子类引用存放子类对象地址 传参过去被父类引用接受 发生向上转型
  1. 方法返回
static Animal func2(){        return new Dog("小黄",5); //返回的是子类对象 但是通过返回类型是Animal 在返回的时候会编译器会将其转换为Animal类型 也是向上转型    }

向上转型的优点:让代码实现更简单灵活。当一个方法内可能会出现不同子类对象地址,但返回值类型只能有一个,而使用到向上转型即可以使不同子类对象转型为其父类对象返回

向上转型的缺陷:不能调用到子类特有的方法。
向上转型后,其只能访问当前子类对象里继承父类引用其自身成员以及其父类以上的父类成员,不能再访问子类对象自身的方法

如果向上转型后要想访问到子类特有的方法还需要用到向下转型…

②.认识向下转型

将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。

向上转型由子类转为父类,而子类本身就是由父类派生进化而来,狗类对象转换为动物是安全的.

但是向下转型是由父类转换为子类,其前提必须得先发生向上转型,且向下转型的类型必须是对应的向上转型的子类,不能是其他子类,

哈士奇类转换为动物.其向下转型必须是动物转换为狗类或者哈士奇类,但是不能是动物转换为猫类… 所以向下转型也会有不安全的情况

 public static void main(String[] args) {        Animal animal=new Dog("小白",1);  //发生向上转型        Dog dog=(Dog) animal;  //通过强转 发生向下转型         dog.eat();//可以访问狗类自己的行为//        上面这种写法虽然可以,但是这样向下转型不安全↓!!        Dog dog1=(Dog)new Animal("小狗",2);        dog1.eat();//        在未经过向上转型时 不能直接向下转型 !!!//        Animal animal1=new Bird("小飞",2);       Dog dog3=(Dog)animal1;//        在向上转型后 不能向下转型为其他不同子类类型!!!        Animal animal2 =new Huskies("小哈",3); }


可以看到,当没有发生向上转型前 通过强转发生向下转型 和 发生向下转型 但是强转为另外的子类类型 都会抛出 ClassCastException –类型转换异常 …

所以 向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了 instanceof 关键字,如果该表达式为true,则可以安全转换。

示例:

 public static void main(String[] args) {Animal animal2 =new Huskies("小哈",3);        if(animal instanceof Dog){  // animal父类引用 指向的对象 是由Dog类向上转型            //在向下转型之前 先进行判断 左边这个引用变量指向的对象是否是由右边类实例化的对象向上转型的 ,如果是则为true可以在里面进行强转 否则false跳过           Dog dog1=(Dog) animal;            dog.eat();        }        Animal animal1=new Bird("小飞",2);        if(animal1 instanceof  Dog){  //animal1 是由Bird类对象 向上转型 此处表达式为false            Dog dog2=(Dog)animal1;   //        }        if(animal2 instanceof Dog){// 虽然是哈士奇类转到动物类 但是期间经过了狗类也属于是狗类转型上去的 但是不能是转型的类的子类转型上去的                        Dog dog3=(Dog) animal2; // 当向下转型时如果转的类型是原来向上转型前的对象的父类又是当前父类引用的子类时也可以转,            dog3.eat(); //  此时之前转型发生的重写 绑定关系不会变 dog类使用的eat方法是 哈士奇类的        }


可以看到通过instanceof 关键字 来判断 父类引用内接受的是否是由对应子类向上转型而来的, 是则返回true 即可发生向下转型 不是则false跳过,避免了向下转型因为一些疏忽而造成不安全的行为抛出了异常
同时看到当huskies对象给 Animal 接受 虽然不是直接父类,但是其能转型为 huskies的父类->狗类 , 但调用其狗类的方法 发现执行的是huskies自身的方法,并不是狗类自身的,这里也就发生了下面要讲的 动态绑定!

4.静态绑定和动态绑定

①.认识静态绑定

静态绑定也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表方法重载。

在此篇博客中讲到了方法重载->方法重载
学了方法重载后,可以知道即便有多个方法名相同的方法 编译器判定方法名实际上是根据方法签名,也就是最后的方法名,在编译期间就能确定所调用的是哪个重载方法,最后运行时即运行对应的方法体

②.认识动态绑定

动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。

动态绑定发生在继承关系,并且子类重写的父类的方法,而此时站在父类角度下调用父类的方法,最后并不会执行父类自身的方法,而会执行子类里重写的方法

class Animal{    String name;    int age;     void eat(){        System.out.println(this.age+"岁的"+this.name+"正在吃食物");    }}class Dog extends Animal{void eat(){        System.out.println(this.age+"岁的"+this.name+"正在吃狗粮");    }}//main... Animal animal1=new Dog("大白",4); //父类引用 接受子类对象地址 发生向上转型 animal1.eat();  // 父类引用 调用父类的方法 当父类方法 在子类里被重写了 此时会发生动态绑定 调用的是被子类重写后的父类方法!


可以看到,当父类引用接受子类对象地址,调用父类方法时,本应该执行的是父类自身的方法xx岁在吃食物, 但是因为子类又重写的父类的此方法,而此时实际上运行的是子类重写的这个方法,执行到4岁大白正在吃狗粮 这也就是发生了动态绑定

而当在运行窗口 中通过反汇编指令 javap -c 字节码名 查看到 在编译阶段,实际上是执行的Animal 类的 eat方法 ,但是最后运行的是Dog类的eat方法

可以看到,动态绑定即在编译时并不确定要执行哪个方法,只有在运行程序的时候,才会知道具体是执行哪个方法,因为子类的方法重写了父类的方法,在运行时,虽然调用的是父类的方法,但是最后会执行子类里的方法体,这也是发生多态的最后一点…

5.多态的实现

实现多态需要满足上面的条件: 发生继承关系,发生子类重写父类方法 ,发生向上转型,发生动态绑定 ,通过一个父类引用接受不同子类对象的地址,使用父类引用调用父类被重写的方法,最后会运行不同子类所重写的父类的方法…
即多个对象执行同一个行为,呈现出不同的状态结果…

示例:

static void func(Animal animal){ //多态: 当一个父类引用 存放不同子类对象地址时,可以表现出不同的子类对象行为!!        animal.eat();//调用父类自身的eat方法 执行的是不同子类自身的方法    }public static void main(String[] args) {        Dog dog=new Dog("小狗",1);        Bird bird=new Bird("小鸟",2);        Huskies huskies=new Huskies("哈士奇",3);        func(dog);  //传狗类对象        func(bird); //传鸟类对象        func(huskies); //传哈士奇对象    }


通过一个方法,一个形参,实现了不同对象的行为,
不同对象通过同一个行为,展现出了不同的状态,这便是多态!!!

三.多态的优缺点

假设有如下代码:

class Shape {//属性....public void draw() {System.out.println("画图形!");   }}class Rect extends Shape{@Overridepublic void draw() {System.out.println("♦");   }}class Cycle extends Shape{@Overridepublic void draw() {System.out.println("●");   }}class Flower extends Shape{@Overridepublic void draw() {System.out.println("❀");   }}

当想输出对应的图像即实例化对应的对象然后调用其draw即可,
当我们想一次性输出多个不同的图形呢,我们可以写对应个数的实例化对象语句依次调用,但是这些重复性代码可以通过数组循环来优化↓

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) { //foreach 遍历字符串数组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()};// Shape数组类型 根据顺序接受不同子类对象的地址                  // 即数组每个元素 父类引用变量都接受了子类对象地址发生向上转型for (Shape shape : shapes) { //通过foreach遍历shape.draw();  //每个父类引用 调用自身的draw方法 发生动态绑定执行子类的draw方法               //实现了 调用同一个方法 执行不同对象的重写方法 展现出不同的结果}}

上面代码使用多态的思想也可以做到输出指定个数的图形,且想输出其它图形甚至新图形,新图型类需继承Shape类重写其draw方法后在数组位置新增子类对象地址即可,
最后通过父类引用调用同一个方法会执行其对应子类重写的方法, 简写了代码去除了大量的if else 代码可读性高, 扩展性强

【使用多态的好处】

  1. 能够降低代码的 “圈复杂度”, 避免使用大量的 if – else
    什么叫 “圈复杂度” ” />
    可以看到 最后输出了D.func() 0
    在实例化子类对象D时,此时子类对象和父类对象有同名的方法此时已经发生了重写,
    而在给子类构造前会先给父类构造,给父类构造调用父类构造方法 在里面又调用了func()方法
    而此时站在父类的角度下调用子类和父类同名的方法(被子类重写的方法)实际上执行的是子类的方法,
    而此时子类还没有构造, num虽然申请了空间 还没有就地初始化,所以里面是默认值0
    最后输出了D.func()0

    构造 D 对象的同时, 会调用 B 的构造方法. B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的func 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0. 如果构造方法具备多态性会先执行子类构造方法给num初始化,num的值应该是1.但是构造方法不具备多态性,
    所以在构造函数内,尽量避免使用实例方法,除了final和private方法(使子类不能重写父类方法)。因为其在调用前可能被子类重写了,而导致动态绑定执行子类的方法

    结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 站在父类角度调用被重写的方法就会触发动态绑定执行子类的方法, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题